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