0012540: upgrade to PHP 7.1.x (Tinebase_DateTime)
[tine20] / tine20 / Tinebase / DateTime.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL3
7  * @copyright   Copyright (c) 2010 Metaways Infosystems GmbH (http://www.metaways.de)
8  * @author      Goekmen Ciyiltepe <g.ciyiltepe@metaways.de>
9  */
10
11 /**
12  * Tinebase_DateTime - Extensions around native php DateTime object
13  * 
14  * @package     Tinebase
15  * 
16  * @TODO: recheck addDay/Week ... rework dst fixes
17  * @TODO: recheck 32 bit issues create / compare
18  * 
19  * NOTE: Tinebase_DateTime was introduced as replacement for Zend_Date in normal
20  *       datetime operations for perfomance reasons. Most of the functions in this
21  *       class are modeled after the Zend_Date class
22  * 
23  * NOTE: This class has nothing to do with localisation! If you need localisation
24  *       use Zend_Date!
25  */
26 class Tinebase_DateTime extends DateTime
27 {
28     const TIMEZONE_UTC = "UTC";
29     
30     const MODIFIER_SECOND   = 'sec';
31     const MODIFIER_MINUTE   = 'min';
32     const MODIFIER_HOUR     = 'hour';
33     const MODIFIER_DAY      = 'day';
34     const MODIFIER_WEEK     = 'week';
35     const MODIFIER_MONTH    = 'month';
36     const MODIFIER_YEAR     = 'year';
37     
38     /**
39      * holds datetime string when being serialised (needed for php < 5.3)
40      * @see http://bugs.php.net/bug.php?id=46891
41      * 
42      * @var string
43      */
44     private $__sDT;
45     
46     /**
47      * holds datetimezone string when being serialised (needed for php < 5.3)
48      * @see http://bugs.php.net/bug.php?id=46891
49      * 
50      * @var string
51      */
52     private $__sDTZ;
53     
54     /**
55      * this datetime represents a date only
56      * 
57      * @var bool
58      */
59     protected $_hasTime = TRUE;
60     
61     /**
62      * @see http://bugs.php.net/bug.php?id=46891
63      */
64     public function __sleep(){
65         $this->__sDT = $this->format('Y-m-d H:i:s');
66         $tz = $this->getTimezone();
67         $this->__sDTZ = $tz? $tz->getName() : 'UTC';
68         return array('__sDT', '__sDTZ');
69     }
70
71     /**
72      * @see http://bugs.php.net/bug.php?id=46891
73      */
74     public function __wakeup() {
75         $this->__construct($this->__sDT, new DateTimeZone($this->__sDTZ ? $this->__sDTZ : 'UTC'));
76         $this->__sDT = $this->__sDTZ = NULL;
77     }
78     
79     /**
80      * Returns new DateTime object
81      * 
82      * @param string|int|DateTime $_time
83      * @param DateTimeZone $_timezone
84      */
85     public function __construct($_time = "now", $_timezone = NULL)
86     {
87         // allow to pass instanceof DateTime
88         if ($_time instanceof DateTime) {
89             if (! $_timezone) {
90                 $_timezone = $_time->getTimezone();
91             } else {
92                 $_time = clone $_time;
93                 $_time->setTimezone($_timezone);
94             }
95             
96             $time = $_time->format("Y-m-d H:i:s");
97         } else {
98             $time = (is_numeric($_time)) ? "@" . floor($_time) : $_time;
99         }
100         
101         if ($_timezone) {
102             if (! $_timezone instanceof DateTimeZone) {
103                 $_timezone = new DateTimeZone($_timezone);
104             }
105             
106             parent::__construct($time, $_timezone);
107         } else {
108             parent::__construct($time);
109         }
110
111         // Normalize Timezonename, as sometimes +00:00 is taken
112         if (is_numeric($_time)) {
113             $this->setTimezone('UTC');
114         }
115     }
116     
117     /**
118      * returns clone to be used with right hand operators
119      * 
120      * @return Tinebase_DateTime
121      */
122     public function getClone()
123     {
124         return clone $this;
125     }
126     
127     /**
128      * call interceptor
129      * 
130      * @param string $name
131      * @param array $arguments
132      */
133     public function __call($name, $arguments)
134     {
135         if (strpos($name, 'php52compat_') === FALSE) {
136             return call_user_func_array(array($this, "php52compat_$name"), $arguments);
137         }
138         throw new Tinebase_Exception_InvalidArgument('unknown method: ' . str_replace('php52compat_', '', $name));
139     }
140     
141     /**
142      * Adds a date or datepart to the existing date. For PHP < 5.3 supported parts are:
143      *  sec, min, hour, day, week, month, year. For later versions check DateTime::add docu.
144      *  Returns the modified DateTime object or FALSE on failure.
145      *  
146      *  For compability with Zend_Date, if $_interval is a DateTime, the timestamp of this datetime 
147      *  will be added
148      *
149      * @param  integer|DateTime|DateInterval    $_interval    Date or datepart to add
150      * @param  string                           $part    OPTIONAL Part of the date to add, if null the timestamp is added
151      * @return Tinebase_DateTime
152      */
153     public function add($_interval, $_part = self::MODIFIER_SECOND)
154     {
155         if ($_interval instanceof DateInterval) {
156             return parent::add($_interval);
157 //        } else if ($_interval instanceof DateIntervalLight) {
158 //            
159         } else if ($_interval instanceof DateTime) {
160             $_interval = $_interval->get('U');
161             $_part = self::MODIFIER_SECOND;
162         }
163         
164         $sign = $_interval < 0 ? '-' : '+';
165         return $this->modify($sign . abs($_interval) . $_part);
166     }
167     
168     /**
169      * Adds days. The parameter is always a number.
170      * Returns the modified DateTime object or FALSE on failure.
171      *
172      * @param  integer              $number days to add
173      * @return Tinebase_DateTime
174      */
175     public function addDay($number)
176     {
177         return $this->add($number*86400, self::MODIFIER_SECOND);
178     }
179     
180     /**
181      * Adds hours. The parameter is always a number.
182      * Returns the modified DateTime object or FALSE on failure.
183      *
184      * @param  integer              $number hours to add
185      * @return Tinebase_DateTime
186      */
187     public function addHour($number)
188     {
189         return $this->add($number*3600, self::MODIFIER_SECOND);
190     }
191     
192     /**
193      * Adds minutes. The parameter is always a number.
194      * Returns the modified DateTime object or FALSE on failure.
195      *
196      * @param  integer              $number minutes to add
197      * @return Tinebase_DateTime
198      */
199     public function addMinute($number)
200     {
201         return $this->add($number, self::MODIFIER_MINUTE);
202     }
203     
204     /**
205      * Adds months. The parameter is always a number.
206      * Returns the modified DateTime object or FALSE on failure.
207      *
208      * @param  integer              $number months to add
209      * @return Tinebase_DateTime
210      */
211     public function addMonth($number)
212     {
213         return $this->add($number, self::MODIFIER_MONTH);
214     }
215     
216     /**
217      * Adds seconds. The parameter is always a number.
218      * Returns the modified DateTime object or FALSE on failure.
219      *
220      * @param  integer              $number seconds to add
221      * @return Tinebase_DateTime
222      */
223     public function addSecond($number)
224     {
225         return $this->add($number, self::MODIFIER_SECOND);
226     }
227     
228     /**
229      * Adds weeks. The parameter is always a number.
230      * Returns the modified DateTime object or FALSE on failure.
231      *
232      * @param  integer              $number weeks to add
233      * @return Tinebase_DateTime
234      */
235     public function addWeek($number)
236     {
237         return $this->add($number*7*86400, self::MODIFIER_SECOND);
238     }
239     
240     /**
241      * Adds years. The parameter is always a number.
242      * Returns the modified DateTime object or FALSE on failure.
243      *
244      * @param  integer              $number years to add
245      * @return Tinebase_DateTime
246      */
247     public function addYear($number)
248     {
249         return $this->add($number, self::MODIFIER_YEAR);
250     }
251     
252     /**
253      * Compares date or datepart of this with an other.
254      * Returns -1 if earlier, 0 if equal and 1 if later.
255      *
256      * @param  DateTime                        $_date    Date to compare with the date object
257      * @param  string                          $_part    OPTIONAL Part of the date to compare, if null the timestamp is subtracted
258      * @return integer  0 = equal, 1 = later, -1 = earlier
259      */
260     public function compare($_date, $_part = 'c')
261     {
262         if (! $_date instanceof DateTime) {
263             throw new Tinebase_Exception_Date(var_export($_date, TRUE) . ' is not an instance of DateTime');
264         }
265         
266         $cmpTZ = $this->getTimezone();
267         
268         if ($cmpTZ != $_date->getTimezone()) {
269             $_date = clone $_date;
270             $_date->setTimezone($cmpTZ);
271         };
272         
273         $thisValue = $this->format($_part);
274         $dateValue = $_date->format($_part);
275         
276         if ($thisValue == $dateValue) {
277             return 0;
278         } elseif ($thisValue > $dateValue) {
279             return 1;
280         } else {
281             return -1;
282         }
283     }
284     
285     /**
286      * Compares this month with the other.
287      * Returns -1 if earlier, 0 if equal and 1 if later.
288      *
289      * @param  DateTime                        $_date    Date to compare with the date object
290      * @return integer  0 = equal, 1 = later, -1 = earlier
291      */
292     public function compareMonth($_date)
293     {
294         return $this->compare($_date, 'n');
295     }
296     
297     /**
298      * Returns a representation of a date or datepart
299      * This could be for example a monthname, the time without date,
300      * the era or only the fractional seconds. For a list of supported part identifiers
301      * look into the php date docu. 
302      * 
303      * NOTE: This method does not use locales. All output is in English.
304      *
305      * @param  string  $part    OPTIONAL Part of the date to return, if null the timestamp is returned
306      * @return string  date or datepart
307      */
308     public function get($_part = null)
309     {
310         return is_null($_part) ? $this->getTimestamp() : $this->format($_part);
311     }
312     
313     /**
314      * Returns the full ISO 8601 date from the this object.
315      * Always the complete ISO 8601 specifiction is used. If an other ISO date is needed
316      * (ISO 8601 defines several formats) use toString() instead.
317      *
318      * @return string
319      */
320     public function getIso()
321     {
322         return $this->format("c");
323     }
324     
325     /**
326      * Returns the UNIX timestamp representation of this datetime.
327      * 
328      * NOTE: PHP < 5.3.0 dosn't have this fn, so we overwrite it
329      *
330      * @return string  UNIX timestamp
331      */
332     public function getTimestamp()
333     {
334         return $this->format('U');
335     }
336     
337     /**
338      * Returns true when both date objects or date parts are equal.
339      *
340      * @param  DateTime                        $date    Date to equal with
341      * @param  string                          $part    OPTIONAL Part of the date to compare, if null the timestamp is used
342      * @return boolean
343      */
344     public function equals($_date, $_part = 'c')
345     {
346         return $this->compare($_date, $_part) == 0;
347     }
348     
349     /**
350      * set/get the hasTime flag 
351      * 
352      * @param bool optional
353      */
354     public function hasTime()
355     {
356         $currValue = $this->_hasTime;
357         if (func_num_args() === 1) {
358             $paramValue = (bool) func_get_arg(0);
359             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Resetting _hasTime to ' . (int) $paramValue);
360             $this->_hasTime = $paramValue;
361             
362             if ($this->_hasTime === FALSE) {
363                 $this->setTime(0,0,0);
364             }
365         }
366         
367         return $currValue;
368     }
369     
370     /**
371      * Returns true if this date or datepart is earlier than the given date
372      * 
373      * @param  DateTime                        $date    Date to compare with
374      * @param  string                          $part    OPTIONAL Part of the date to compare, if null the timestamp is used
375      * @return boolean
376      */
377     public function isEarlier($_date, $_part = 'c')
378     {
379         return $this->compare($_date, $_part) < 0;
380     }
381     
382     /**
383      * Returns true if this date or datepart is later than the given date
384      * 
385      * @param  DateTime                        $date    Date to compare with
386      * @param  string                          $part    OPTIONAL Part of the date to compare, if null the timestamp is used
387      * @return boolean
388      */
389     public function isLater($_date, $_part = 'c')
390     {
391         return $this->compare($_date, $_part) > 0;
392     }
393     
394     /**
395      * Returns true if this date or datepart is earlier or equals than the given date
396      *
397      * @param  DateTime                        $date    Date to compare with
398      * @param  string                          $part    OPTIONAL Part of the date to compare, if null the timestamp is used
399      * @return boolean
400      */
401     public function isEarlierOrEquals($_date, $_part = 'c')
402     {
403         return $this->compare($_date, $_part) < 1;
404     }
405     
406     /**
407      * Returns true if this date or datepart is later or equals than the given date
408      *
409      * @param  DateTime                        $date    Date to compare with
410      * @param  string                          $part    OPTIONAL Part of the date to compare, if null the timestamp is used
411      * @return boolean
412      */
413     public function isLaterOrEquals($_date, $_part = 'c')
414     {
415         return $this->compare($_date, $_part) > -1;
416     }
417     
418     /**
419      * (non-PHPdoc)
420      * @see DateTime::setDate()
421      * @note PHP 5.3.0 changed the return value on success from NULL to DateTime.
422      */
423     public function setDate($year ,$month ,$day)
424     {
425         parent::setDate($year ,$month ,$day);
426         return $this;
427     }
428     
429     /**
430      * (non-PHPdoc)
431      * @see DateTime::setTime()
432      * @note PHP 5.3.0 changed the return value on success from NULL to DateTime.
433      * @note PHP 7.1 added param $microseconds
434      */
435     public function setTime($hour, $minute, $second = 0, $microseconds = null)
436     {
437         if (PHP_VERSION_ID < 70100) {
438             parent::setTime($hour, $minute, $second);
439         } else {
440             parent::setTime($hour, $minute, $second, $microseconds);
441         }
442         return $this;
443     }
444     
445     /**
446      * (non-PHPdoc)
447      * @see DateTime::setISODate()
448      * @note PHP 5.3.0 changed the return value on success from NULL to DateTime.
449      */
450     public function setISODate($year ,$week, $day = 1)
451     {
452         parent::setISODate($year ,$week, $day);
453         return $this;
454     }
455     
456     /**
457      * Substracts a date or datepart to the existing date. For PHP < 5.3 supported parts are:
458      *  sec, min, hour, day, week, month, year. For later versions check DateTime::sub docu.
459      *  Returns the modified DateTime object or FALSE on failure.
460      *  
461      *  For compability with Zend_Date, if $_interval is a DateTime, the timestamp of this datetime 
462      *  will be substracted
463      *
464      * @param  integer|DateTime|DateInterval    $_interval    Date or datepart to substract
465      * @param  string                           $part    OPTIONAL Part of the date to substract, if null the timestamp is substracted
466      * @return Tinebase_DateTime
467      */
468     public function sub($_interval, $_part = self::MODIFIER_SECOND)
469     {
470         if ($_interval instanceof DateInterval) {
471             return parent::sub($_interval);
472         }
473         
474         $_interval = is_numeric($_interval) ? -1 * $_interval : $_interval;
475         return $this->add($_interval, $_part);
476     }
477     
478     /**
479      * Substracts days. The parameter is always a number.
480      * Returns the modified DateTime object or FALSE on failure.
481      *
482      * @param  integer              $number days to substract
483      * @return Tinebase_DateTime
484      */
485     public function subDay($number)
486     {
487         return $this->addDay(-1 * $number);
488     }
489     
490     /**
491      * Substracts hours. The parameter is always a number.
492      * Returns the modified DateTime object or FALSE on failure.
493      *
494      * @param  integer              $number hours to substract
495      * @return Tinebase_DateTime
496      */
497     public function subHour($number)
498     {
499         return $this->addHour(-1 * $number);
500     }
501     
502     /**
503      * Substracts minutes. The parameter is always a number.
504      * Returns the modified DateTime object or FALSE on failure.
505      *
506      * @param  integer              $number minutes to substract
507      * @return Tinebase_DateTime
508      */
509     public function subMinute($number)
510     {
511         return $this->addMinute(-1 * $number);
512     }
513     
514     /**
515      * Substracts months. The parameter is always a number.
516      * Returns the modified DateTime object or FALSE on failure.
517      *
518      * @param  integer              $number months to substract
519      * @return Tinebase_DateTime
520      */
521     public function subMonth($number)
522     {
523         return $this->addMonth(-1 * $number);
524     }
525     
526     /**
527      * Substracts seconds. The parameter is always a number.
528      * Returns the modified DateTime object or FALSE on failure.
529      *
530      * @param  integer              $number seconds to substract
531      * @return Tinebase_DateTime
532      */
533     public function subSecond($number)
534     {
535         return $this->addSecond(-1 * $number);
536     }
537     
538     /**
539      * Substracts minutes. The parameter is always a number.
540      * Returns the modified DateTime object or FALSE on failure.
541      *
542      * @param  integer              $number minutes to substract
543      * @return Tinebase_DateTime
544      */
545     public function subWeek($number)
546     {
547         return $this->addWeek(-1 * $number);
548     }
549     
550     /**
551      * Substracts minutes. The parameter is always a number.
552      * Returns the modified DateTime object or FALSE on failure.
553      *
554      * @param  integer              $number minutes to substract
555      * @return Tinebase_DateTime
556      */
557     public function subYear($number)
558     {
559         return $this->addYear(-1 * $number);
560     }
561     
562     /**
563      * Alters the timestamp
564      * NOTE: PHP 5.3.0 Changelog: Changed the return value on success from NULL to DateTime
565      *
566      * @param  string $modify A date/time string. Valid formats are explained in Date and Time Formats.
567      * @return Tinebase_DateTime $this
568      */
569     public function modify($modify)
570     {
571         parent::modify($modify);
572         return $this;
573     }
574     
575     /**
576      * Returns the difference between two DateTime objects
577      * 
578      * @param  DateTime $datetime2
579      * @param  bool     $absolute
580      * @return DateInterval
581      */
582     public function php52compat_diff(DateTime $datetime2, $absolute = false)
583     {
584         // use Zend_Date for 32 bit compat
585         $thisZD = new Zend_Date($this->format('U'));
586         $datetime2ZD = new Zend_Date($datetime2->format('U'));
587         
588         return $datetime2ZD->sub($thisZD, Zend_Date::TIMESTAMP);
589     }
590     
591     /**
592      * Returns a string representation of the object
593      * For Supported format tokens see: php.net/date
594      * 
595      * @return String
596      */
597     public function __toString()
598     {
599         return $this->format("Y-m-d H:i:s");
600     }
601     
602     /**
603      * Returns a string representation of the object
604      * For Supported format tokens see: php.net/date
605      * 
606      * @param  String $_format  OPTIONAL Rule for formatting output. If null the default date format is used
607      * @return String
608      */
609     public function toString($_format = "Y-m-d H:i:s")
610     {
611         return $this->format($_format);
612     }
613     
614     /**
615      * Sets a new hour
616      * The hour is always a number.
617      * Returned is the new date object
618      * Example: 04.May.1993 13:07:25 -> setHour(7); -> 04.May.1993 07:07:25
619      *
620      * @param  string|integer $modify    Hour to set
621      * @return Tinebase_DateTime  $this
622      */
623     public function setHour($modify)
624     {
625         list ($i, $s) = explode(' ', $this->format('i s'));
626         
627         $this->setTime($modify, $i, $s);
628         return $this;
629     }
630     
631     /**
632      * Sets a new minute
633      * The minute is always a number.
634      * Returned is the new date object
635      * Example: 04.May.1993 13:23:25 -> setMinute(7); -> 04.May.1993 13:07:25
636      *
637      * @param  string|integer $modify    Minute to set
638      * @return Tinebase_DateTime  $this
639      */
640     public function setMinute($modify)
641     {
642         list ($h, $s) = explode(' ', $this->format('H s'));
643         
644         $this->setTime($h, $modify, $s);
645         return $this;
646     }
647     
648     /**
649      * Sets a new second
650      * The second is always a number.
651      * Returned is the new date object
652      * Example: 04.May.1993 13:07:25 -> setSecond(7); -> 04.May.1993 13:07:07
653      *
654      * @param  string|integer $modify    Second to set
655      * @return Tinebase_DateTime  $this
656      */
657     public function setSecond($modify)
658     {
659         list ($h, $i) = explode(' ', $this->format('H i'));
660         
661         $this->setTime($h, $i, $modify);
662         return $this;
663     }
664     
665     /**
666      * Sets a new timezone.
667      * For a list of supported timezones look here: http://php.net/timezones
668      *
669      * @param  string|DateTimeZone  $_timezone  timezone for date calculation
670      * @return Tinebase_DateTime    $this
671      */
672     public function setTimezone($_timezone)
673     {
674         if ($this->_hasTime === FALSE) $date = $this->format('Y-m-d');
675         
676         $timezone = $_timezone instanceof DateTimeZone ? $_timezone : new DateTimeZone($_timezone);
677         parent::setTimezone($timezone);
678         
679         // if we contain no time info, we are timezone invariant
680         if ($this->_hasTime === FALSE) {
681             call_user_func_array(array($this, 'setDate'), explode('-', $date));
682             $this->setTime(0,0,0);
683         }
684         
685         return $this;
686     }
687     
688     /**
689      * Sets a new week. The week is always a number. The day of week is not changed.
690      * Returned is the new date object
691      * Example: 09.Jan.2007 13:07:25 -> setWeek(1); -> 02.Jan.2007 13:07:25
692      *
693      * @param  string|integer     $week    Week to set
694      * @return Tinebase_DateTime  $this
695      */
696     public function setWeek($week)
697     {
698         $currentWeek = (int)$this->format("W");
699         $operator = ($currentWeek > $week) ? "-" : "+";
700         return $this->modify($operator . abs($currentWeek - $week) . "week");
701     }
702     
703     /**
704      * Sets a new weekday
705      * The weekday can be a number or a string.
706      * Returned is the new date object.
707      * Example: setWeekday(3); will set the wednesday of this week as day.
708      *
709      * @param  string|integer                  $weekDay   Weekday to set
710      * @return Tinebase_DateTime  $this
711      */
712     public function setWeekDay($weekDay)
713     {
714         $currentWeekDay = (int)$this->format("N");
715         $operator = ($currentWeekDay > $weekDay) ? "-" : "+";
716         return $this->modify($operator . abs($currentWeekDay - $weekDay) . "day");
717     }
718     
719     /**
720      * Checks if the given date is a real date or datepart.
721      * Returns false if a expected datepart is missing or a datepart exceeds its possible border.
722      *
723      * @param  string             $_date   Date to parse for correctness
724      * @return boolean            True when all date parts are correct
725      */
726     public static function isDate($_date)
727     {
728         if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
729             $result = date_parse($_date);
730             return empty($result['warning_count']) && empty($result['error_count']);
731         } else {
732             return Zend_Date::isDate($_date, 'yyyy-MM-dd HH:mm:ss');
733         }
734     }
735     
736     /**
737      * Returns an array representation of the object
738      *
739      * @return array
740      */
741     public function toArray()
742     {
743         return array(
744             'day'       => $this->format('j'),
745             'month'     => $this->format('n'),
746             'year'      => $this->format('Y'),
747             'hour'      => $this->format('G'),
748             'minute'    => $this->format('i'),
749             'second'    => $this->format('s'),
750             'timezone'  => $this->format('e'),
751             'timestamp' => $this->format('U'),
752             'weekday'   => $this->format('N'),
753             'dayofyear' => $this->format('z'),
754             'week'      => $this->format('W'),
755             'gmtsecs'   => $this->getOffset()
756         );
757     }
758     
759     /**
760      * Returns the actual date as new date object
761      *
762      * @return Tinebase_DateTime
763      */
764     public static function now()
765     {
766         return new Tinebase_DateTime();
767     }
768     
769 }
770