1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11: 12:
13:
14: namespace Jyxo\Time;
15:
16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32:
33: class Time implements \Serializable
34: {
35: 36: 37: 38: 39:
40: const SECOND = 'second';
41:
42: 43: 44: 45: 46:
47: const MINUTE = 'minute';
48:
49: 50: 51: 52: 53:
54: const HOUR = 'hour';
55:
56: 57: 58: 59: 60:
61: const DAY = 'day';
62:
63: 64: 65: 66: 67:
68: const WEEK = 'week';
69:
70: 71: 72: 73: 74:
75: const MONTH = 'month';
76:
77: 78: 79: 80: 81:
82: const YEAR = 'year';
83:
84: 85: 86: 87: 88:
89: const INTERVAL_SECOND = 1;
90:
91: 92: 93: 94: 95:
96: const INTERVAL_MINUTE = 60;
97:
98: 99: 100: 101: 102:
103: const INTERVAL_HOUR = 3600;
104:
105: 106: 107: 108: 109:
110: const INTERVAL_DAY = 86400;
111:
112: 113: 114: 115: 116:
117: const INTERVAL_WEEK = 604800;
118:
119: 120: 121: 122: 123:
124: const INTERVAL_MONTH = 2592000;
125:
126: 127: 128: 129: 130:
131: const INTERVAL_YEAR = 31536000;
132:
133: 134: 135: 136: 137:
138: private $dateTime;
139:
140: 141: 142: 143: 144:
145: private $originalTimeZone;
146:
147: 148: 149: 150: 151: 152: 153: 154: 155:
156: public function __construct($time, $timeZone = null)
157: {
158: if (!is_object($time)) {
159: $timeZone = $this->createTimeZone($timeZone ? $timeZone : date_default_timezone_get());
160:
161: if (is_numeric($time)) {
162:
163: $this->dateTime = new \DateTime(null, $timeZone);
164: $this->dateTime->setTimestamp($time);
165: } elseif (is_string($time)) {
166:
167: try {
168: $this->dateTime = new \DateTime($time, $timeZone);
169: } catch (\Exception $e) {
170: throw new \InvalidArgumentException(sprintf('Provided textual date/time definition "%s" is invalid', $time), 0, $e);
171: }
172: } else {
173: throw new \InvalidArgumentException('Provided date/time must be a number, \Jyxo\Time\Time or \DateTime instance or a parameter compatible with PHP function strtotime().');
174: }
175: } elseif ($time instanceof self) {
176:
177: $this->dateTime = new \DateTime($time->format('Y-m-d H:i:s'), $time->getTimeZone());
178: if ($timeZone) {
179: $this->dateTime->setTimezone($this->createTimeZone($timeZone));
180: }
181: } elseif ($time instanceof \DateTime) {
182:
183: $this->dateTime = clone ($time);
184: if ($timeZone) {
185: $this->dateTime->setTimezone($this->createTimeZone($timeZone));
186: }
187: } else {
188: throw new \InvalidArgumentException('Provided date/time must be a number, \Jyxo\Time\Time or \DateTime instance or a parameter compatible with PHP function strtotime().');
189: }
190: }
191:
192: 193: 194: 195: 196: 197: 198:
199: protected function createTimeZone($definition)
200: {
201: if (is_string($definition)) {
202: try {
203: return new \DateTimeZone($definition);
204: } catch (\Exception $e) {
205: throw new \InvalidArgumentException(sprintf('Invalid timezone definition "%s"', $definition), 0, $e);
206: }
207: } elseif (!$definition instanceof \DateTimeZone) {
208: throw new \InvalidArgumentException('Invalid timezone definition');
209: }
210:
211: return $definition;
212: }
213:
214: 215: 216: 217: 218: 219: 220: 221: 222:
223: public static function get($time, $timeZone = null)
224: {
225: return new self($time, $timeZone);
226: }
227:
228: 229: 230: 231: 232:
233: public static function now()
234: {
235: return new self(time());
236: }
237:
238: 239: 240: 241: 242: 243: 244:
245: public static function createFromFormat($format, $time)
246: {
247: return new self(\DateTime::createFromFormat($format, $time));
248: }
249:
250: 251: 252: 253: 254: 255: 256:
257: public function __get($name)
258: {
259: switch ($name) {
260: case 'sql':
261: return $this->dateTime->format(\DateTime::ISO8601);
262: case 'email':
263: return $this->dateTime->format(\DateTime::RFC822);
264: case 'web':
265: return $this->dateTime->format(\DateTime::W3C);
266: case 'cookie':
267: return $this->dateTime->format(\DateTime::COOKIE);
268: case 'rss':
269: return $this->dateTime->format(\DateTime::RSS);
270: case 'unix':
271:
272: return $this->dateTime->getTimestamp();
273: case 'http':
274: $this->setTemporaryTimeZone('GMT');
275: $result = $this->dateTime->format('D, d M Y H:i:s') . ' GMT';
276: $this->revertOriginalTimeZone();
277: return $result;
278: case 'extended':
279: return $this->formatExtended();
280: case 'interval':
281: return $this->formatAsInterval();
282: case 'full':
283: if ((int) $this->dateTime->diff(new \DateTime())->format('%y%m%d%h') > 0) {
284:
285: return $this->formatExtended();
286: } else {
287: return $this->formatAsInterval();
288: }
289: default:
290: throw new \InvalidArgumentException(sprintf('Unknown format %s.', $name));
291: }
292: }
293:
294: 295: 296: 297: 298: 299: 300:
301: public function __call($method, $args)
302: {
303: return call_user_func_array(array($this->dateTime, $method), $args);
304: }
305:
306: 307: 308: 309: 310:
311: public function __toString()
312: {
313: return (string) $this->dateTime->getTimestamp();
314: }
315:
316: 317: 318: 319: 320:
321: public function getTimeZone()
322: {
323: return $this->dateTime->getTimezone();
324: }
325:
326: 327: 328: 329: 330: 331:
332: public function setTimeZone($timeZone)
333: {
334: $this->dateTime->setTimezone($this->createTimeZone($timeZone));
335: return $this;
336: }
337:
338: 339: 340: 341: 342: 343:
344: protected function setTemporaryTimeZone($timeZone)
345: {
346: $this->originalTimeZone = $this->dateTime->getTimezone();
347: try {
348: $this->setTimeZone($this->createTimeZone($timeZone));
349: } catch (\InvalidArgumentException $e) {
350: $this->originalTimeZone = null;
351: throw $e;
352: }
353: }
354:
355: 356: 357: 358: 359: 360:
361: protected function revertOriginalTimeZone()
362: {
363: if (null !== $this->originalTimeZone) {
364: $this->dateTime->setTimezone($this->originalTimeZone);
365: $this->originalTimeZone = null;
366: }
367:
368: return $this;
369: }
370:
371: 372: 373: 374: 375: 376: 377:
378: public function format($format, $timeZone = null)
379: {
380:
381: if ($timeZone) {
382: $this->setTemporaryTimeZone($timeZone);
383: }
384:
385:
386: if (preg_match('~(?:^|[^\\\])[lDFM]~', $format)) {
387: static $days = array();
388: if (empty($days)) {
389: $days = array(_('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday'), _('Sunday'));
390: }
391:
392: static $daysShort = array();
393: if (empty($daysShort)) {
394: $daysShort = array(_('Mon'), _('Tue'), _('Wed'), _('Thu'), _('Fri'), _('Sat'), _('Sun'));
395: }
396:
397: static $months = array();
398: if (empty($months)) {
399: $months = array(
400: _('January'), _('February'), _('March'), _('April'), _('May'), _('June'), _('July'), _('August'),
401: _('September'), _('October'), _('November'), _('December')
402: );
403: }
404: static $monthsGen = array();
405: if (empty($monthsGen)) {
406: $monthsGen = array(
407: _('January#~Genitive'), _('February#~Genitive'), _('March#~Genitive'), _('April#~Genitive'), _('May#~Genitive'),
408: _('June#~Genitive'), _('July#~Genitive'), _('August#~Genitive'), _('September#~Genitive'),
409: _('October#~Genitive'), _('November#~Genitive'), _('December#~Genitive')
410: );
411: }
412: static $monthsShort = array();
413: if (empty($monthsShort)) {
414: $monthsShort = array(_('Jan'), _('Feb'), _('Mar'), _('Apr'), _('May#~Shortcut'), _('Jun'), _('Jul'), _('Aug'), _('Sep'), _('Oct'), _('Nov'), _('Dec'));
415: }
416:
417:
418: $search = array('~(^|[^\\\])l~', '~(^|[^\\\])D~', '~(^|[^\\\])F~', '~(^|[^\\\])M~');
419: $replace = array('$1<===>', '$1<___>', '$1<--->', '$1<...>');
420: $format = preg_replace($search, $replace, $format);
421:
422:
423: $date = $this->dateTime->format($format);
424:
425:
426: $day = $this->dateTime->format('N') - 1;
427: $month = $this->dateTime->format('n') - 1;
428:
429:
430: $monthName = 0 !== strpos($format, '<--->') ? mb_strtolower($monthsGen[$month], 'utf-8') : $months[$month];
431:
432:
433: $result = strtr(
434: $date,
435: array(
436: '<===>' => $days[$day],
437: '<___>' => $daysShort[$day],
438: '<--->' => $monthName,
439: '<...>' => $monthsShort[$month]
440: )
441: );
442: } else {
443:
444: $result = $this->dateTime->format($format);
445: }
446:
447:
448: if ($timeZone) {
449: $this->revertOriginalTimeZone();
450: }
451:
452: return $result;
453: }
454:
455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467:
468: public function formatExtended($dateFormat = 'j. F Y', $timeFormat = 'G:i', $timeZone = null)
469: {
470:
471: if ($timeZone) {
472: $this->setTemporaryTimeZone($timeZone);
473: }
474:
475: if (($this->dateTime < new \DateTime('midnight - 6 days', $this->dateTime->getTimezone())) || ($this->dateTime >= new \DateTime('midnight + 24 hours', $this->dateTime->getTimezone()))) {
476:
477: $date = $this->format($dateFormat);
478: } elseif ($this->dateTime >= new \DateTime('midnight', $this->dateTime->getTimezone())) {
479:
480: $date = _('Today');
481: } elseif ($this->dateTime >= new \DateTime('midnight - 24 hours', $this->dateTime->getTimezone())) {
482:
483: $date = _('Yesterday');
484: } else {
485:
486: static $days = array();
487: if (empty($days)) {
488: $days = array(_('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday'), _('Sunday'));
489: }
490: $date = $days[$this->dateTime->format('N') - 1];
491: }
492:
493:
494: if (empty($timeFormat)) {
495: $result = $date;
496: } else {
497:
498: $result = $date . ' ' . _('at') . ' ' . $this->dateTime->format($timeFormat);
499: }
500:
501:
502: if ($timeZone) {
503: $this->revertOriginalTimeZone();
504: }
505:
506: return $result;
507: }
508:
509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524:
525: public function formatAsInterval($useTense = true, $timeZone = null)
526: {
527: static $intervalList = array(
528: self::YEAR => self::INTERVAL_YEAR,
529: self::MONTH => self::INTERVAL_MONTH,
530: self::WEEK => self::INTERVAL_WEEK,
531: self::DAY => self::INTERVAL_DAY,
532: self::HOUR => self::INTERVAL_HOUR,
533: self::MINUTE => self::INTERVAL_MINUTE,
534: self::SECOND => self::INTERVAL_SECOND
535: );
536:
537:
538: $timeZone = $timeZone ? $this->createTimeZone($timeZone) : $this->dateTime->getTimezone();
539:
540:
541: $differenceObject = $this->dateTime->diff(new \DateTime(null, $timeZone));
542: $diffArray = array_combine(
543: array_keys($intervalList),
544: explode('-', $differenceObject->format('%y-%m-0-%d-%h-%i-%s'))
545: );
546:
547:
548: $diff = 0;
549: foreach ($diffArray as $interval => $intervalCount) {
550: $diff += $intervalList[$interval] * $intervalCount;
551: }
552:
553:
554: if ($diff < 10) {
555: return _('Now');
556: }
557:
558:
559: foreach ($intervalList as $interval => $seconds) {
560: if ($seconds <= $diff) {
561: $num = round($diff / $seconds);
562: break;
563: }
564: }
565:
566:
567: $period = '+' === $differenceObject->format('%R') ? 'past' : 'future';
568:
569:
570: $tense = $useTense ? $period : 'infinitive';
571: switch ($tense) {
572:
573: case 'past':
574: switch ($interval) {
575: case self::YEAR:
576: return sprintf(ngettext('Year ago', '%s years ago', $num), $num);
577: case self::MONTH:
578: return sprintf(ngettext('Month ago', '%s months ago', $num), $num);
579: case self::WEEK:
580: return sprintf(ngettext('Week ago', '%s weeks ago', $num), $num);
581: case self::DAY:
582: return sprintf(ngettext('Day ago', '%s days ago', $num), $num);
583: case self::HOUR:
584: return sprintf(ngettext('Hour ago', '%s hours ago', $num), $num);
585: case self::MINUTE:
586: return sprintf(ngettext('Minute ago', '%s minutes ago', $num), $num);
587: case self::SECOND:
588: default:
589: return sprintf(ngettext('Second ago', '%s seconds ago', $num), $num);
590: }
591: break;
592:
593:
594: case 'future':
595: switch ($interval) {
596: case self::YEAR:
597: return sprintf(ngettext('In year', 'In %s years', $num), $num);
598: case self::MONTH:
599: return sprintf(ngettext('In month', 'In %s months', $num), $num);
600: case self::WEEK:
601: return sprintf(ngettext('In week', 'In %s weeks', $num), $num);
602: case self::DAY:
603: return sprintf(ngettext('In day', 'In %s days', $num), $num);
604: case self::HOUR:
605: return sprintf(ngettext('In hour', 'In %s hours', $num), $num);
606: case self::MINUTE:
607: return sprintf(ngettext('In minute', 'In %s minutes', $num), $num);
608: case self::SECOND:
609: default:
610: return sprintf(ngettext('In second', 'In %s seconds', $num), $num);
611: }
612: break;
613:
614:
615: case 'infinitive':
616: switch ($interval) {
617: case self::YEAR:
618: return sprintf(ngettext('Year', '%s years', $num), $num);
619: case self::MONTH:
620: return sprintf(ngettext('Month', '%s months', $num), $num);
621: case self::WEEK:
622: return sprintf(ngettext('Week', '%s weeks', $num), $num);
623: case self::DAY:
624: return sprintf(ngettext('Day', '%s days', $num), $num);
625: case self::HOUR:
626: return sprintf(ngettext('Hour', '%s hours', $num), $num);
627: case self::MINUTE:
628: return sprintf(ngettext('Minute', '%s minutes', $num), $num);
629: case self::SECOND:
630: default:
631: return sprintf(ngettext('Second', '%s seconds', $num), $num);
632: }
633: break;
634: default:
635: break;
636: }
637: }
638:
639: 640: 641: 642: 643: 644:
645: public function plus($interval)
646: {
647: if (is_numeric($interval)) {
648: $interval .= ' seconds';
649: }
650:
651: $dateTime = clone $this->dateTime;
652: $dateTime->modify('+' . (string) $interval);
653:
654: return new self($dateTime);
655: }
656:
657: 658: 659: 660: 661: 662:
663: public function minus($interval)
664: {
665: if (is_numeric($interval)) {
666: $interval .= ' seconds';
667: }
668:
669: $dateTime = clone $this->dateTime;
670: $dateTime->modify('-' . (string) $interval);
671:
672: return new self($dateTime);
673: }
674:
675: 676: 677: 678: 679: 680: 681:
682: public function hasHappened()
683: {
684: return '+' === $this->dateTime->diff(\DateTime::createFromFormat('U', time(), $this->dateTime->getTimezone()))->format('%R');
685: }
686:
687: 688: 689: 690: 691: 692: 693:
694: public function truncate($unit)
695: {
696: $dateTime = array(
697: self::YEAR => 0,
698: self::MONTH => 1,
699: self::DAY => 1,
700: self::HOUR => 0,
701: self::MINUTE => 0,
702: self::SECOND => 0
703: );
704:
705: switch ((string) $unit) {
706: case self::SECOND:
707: $dateTime[self::SECOND] = $this->dateTime->format('s');
708:
709: case self::MINUTE:
710: $dateTime[self::MINUTE] = $this->dateTime->format('i');
711:
712: case self::HOUR:
713: $dateTime[self::HOUR] = $this->dateTime->format('H');
714:
715: case self::DAY:
716: $dateTime[self::DAY] = $this->dateTime->format('d');
717:
718: case self::MONTH:
719: $dateTime[self::MONTH] = $this->dateTime->format('m');
720:
721: case self::YEAR:
722: $dateTime[self::YEAR] = $this->dateTime->format('Y');
723: break;
724: default:
725: throw new \InvalidArgumentException(sprintf('Time unit %s is not defined.', $unit));
726: }
727:
728: return new self(vsprintf('%s-%s-%sT%s:%s:%s', $dateTime), $this->dateTime->getTimezone());
729: }
730:
731: 732: 733: 734: 735:
736: public function serialize()
737: {
738: return $this->dateTime->format('Y-m-d H:i:s ') . $this->dateTime->getTimezone()->getName();
739: }
740:
741: 742: 743: 744: 745: 746:
747: public function unserialize($serialized)
748: {
749: try {
750: $data = explode(' ', $serialized);
751: if (count($data) != 3) {
752: throw new \Exception('Serialized data have to be in the "Y-m-d H:i:s TimeZone" format');
753: }
754:
755: if (preg_match('~([+-]\d{2}):?([\d]{2})~', $data[2], $matches)) {
756:
757:
758: if ($matches[2] < 0 || $matches[2] > 59 || intval($matches[1] . $matches[2]) < -1200 || intval($matches[1] . $matches[2]) > 1200) {
759:
760: throw new \Exception(sprintf('Invalid time zone UTC offset definition: %s', $matches[0]));
761: }
762:
763: $data[1] .= ' ' . $matches[1] . $matches[2];
764: $this->dateTime = new \DateTime($data[0] . ' ' . $data[1]);
765: } else {
766: $this->dateTime = new \DateTime($data[0] . ' ' . $data[1], $this->createTimeZone($data[2]));
767: }
768:
769: } catch (\Exception $e) {
770: throw new \InvalidArgumentException('Deserialization error', 0, $e);
771: }
772: }
773: }
774: