1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie\CLDR;
13
14 use ICanBoogie\DateTime;
15 use ICanBoogie\PropertyNotDefined;
16
17 /**
18 * Provides date and time localization.
19 *
20 * The class allows you to format dates and times in a locale-sensitive manner using
21 * {@link http://www.unicode.org/reports/tr35/#Date_Format_Patterns Unicode format patterns}.
22 *
23 * @property-read Calendar $calendar The calendar used by the formatter.
24 */
25 class DateTimeFormatter
26 {
27 /**
28 * Pattern characters mapping to the corresponding translator methods.
29 *
30 * @var array
31 */
32 static protected $formatters = array
33 (
34 'G' => 'format_era',
35 'y' => 'format_year',
36 // 'Y' => Year (in "Week of Year" based calendars).
37 // 'u' => Extended year.
38 'Q' => 'format_quarter',
39 'q' => 'format_standalone_quarter',
40 'M' => 'format_month',
41 'L' => 'format_standalone_month',
42 // 'l' => Special symbol for Chinese leap month, used in combination with M. Only used with the Chinese calendar.
43 'w' => 'format_week_of_year',
44 'W' => 'format_week_of_month',
45 'd' => 'format_day_of_month',
46 'D' => 'format_day_of_year',
47 'F' => 'format_day_of_week_in_month',
48
49 'h' => 'format_hour12',
50 'H' => 'format_hour24',
51 'm' => 'format_minutes',
52 's' => 'format_seconds',
53 'E' => 'format_day_in_week',
54 'c' => 'format_day_in_week',
55 'e' => 'format_day_in_week',
56 'a' => 'format_period',
57 'k' => 'format_hour_in_day',
58 'K' => 'format_hour_in_period',
59 'z' => 'format_timezone',
60 'Z' => 'format_timezone',
61 'v' => 'format_timezone'
62 );
63
64 /**
65 * Parses the datetime format pattern.
66 *
67 * @param string $pattern the pattern to be parsed
68 *
69 * @return array tokenized parsing result
70 */
71 static protected function tokenize($pattern)
72 {
73 static $formats = array();
74
75 if (isset($formats[$pattern]))
76 {
77 return $formats[$pattern];
78 }
79
80 $tokens = array();
81 $is_literal = false;
82 $literal = '';
83
84 for ($i = 0, $n = strlen($pattern) ; $i < $n ; ++$i)
85 {
86 $c = $pattern{$i};
87
88 if ($c === "'")
89 {
90 if ($i < $n-1 && $pattern{$i+1} === "'")
91 {
92 $tokens[] = "'";
93 $i++;
94 }
95 else if ($is_literal)
96 {
97 $tokens[] = $literal;
98 $literal = '';
99 $is_literal = false;
100 }
101 else
102 {
103 $is_literal = true;
104 $literal = '';
105 }
106 }
107 else if ($is_literal)
108 {
109 $literal .= $c;
110 }
111 else
112 {
113 for ($j = $i + 1 ; $j < $n ; ++$j)
114 {
115 if ($pattern{$j} !== $c) break;
116 }
117
118 $l = $j-$i;
119 $p = str_repeat($c, $l);
120
121 $tokens[] = isset(self::$formatters[$c]) ? array(self::$formatters[$c], $p, $l) : $p;
122
123 $i = $j - 1;
124 }
125 }
126
127 if ($literal)
128 {
129 $tokens[] = $literal;
130 }
131
132 return $formats[$pattern] = $tokens;
133 }
134
135 /**
136 * The calendar used to format the datetime.
137 *
138 * @var Calendar
139 */
140 protected $calendar;
141
142 /**
143 * Initializes the {@link $calendar} property.
144 *
145 * @param Calendar $calendar
146 */
147 public function __construct(Calendar $calendar)
148 {
149 $this->calendar = $calendar;
150 }
151
152 /**
153 * Support of the {@link $calendar} magic property.
154 *
155 * @param string $property
156 *
157 * @throws PropertyNotDefined in attempt to get an undefined and unsupported property.
158 *
159 * @return mixed
160 */
161 public function __get($property)
162 {
163 if ($property === 'calendar')
164 {
165 return $this->calendar;
166 }
167
168 throw new PropertyNotDefined(array($property, $this));
169 }
170
171 /**
172 * Formats a date according to a pattern.
173 *
174 * <pre>
175 * <?php
176 *
177 * $datetime_formatter = $repository->locales['en']->calendar->datetime_formatter;
178 * $datetime = '2013-11-02 22:23:45';
179 *
180 * echo $datetime_formatter($datetime, "MMM d, y"); // November 2, 2013 at 10:23:45 PM
181 * echo $datetime_formatter($datetime, "MMM d, y 'at' hh:mm:ss a"); // November 2, 2013 at 10:23:45 PM
182 * echo $datetime_formatter($datetime, 'full'); // Saturday, November 2, 2013 at 10:23:45 PM CET
183 * echo $datetime_formatter($datetime, 'long'); // November 2, 2013 at 10:23:45 PM CET
184 * echo $datetime_formatter($datetime, 'medium'); // Nov 2, 2013, 10:23:45 PM
185 * echo $datetime_formatter($datetime, 'short'); // 11/2/13, 10:23 PM
186 * echo $datetime_formatter($datetime, ':Ehm'); // Sat 10:23 PM
187 * </pre>
188 *
189 * @param \DateTime|string|int $datetime The datetime to format.
190 * @param string $pattern_or_width_or_skeleton The datetime can be formatted using a pattern,
191 * a width or a skeleton. The following width are defined: "full", "long", "medium" and "short".
192 * To format the datetime using a so-called "skeleton", the skeleton identifier must be
193 * prefixed with the colon sign ":" e.g. ":Ehm". The skeleton identifies the patterns defined
194 * under `availableFormats`.
195 *
196 * @return string The formatted date time.
197 *
198 * @see http://www.unicode.org/reports/tr35/#Date_Format_Patterns
199 */
200 public function format($datetime, $pattern_or_width_or_skeleton)
201 {
202 $datetime = DateTime::from(is_numeric($datetime) ? "@$datetime" : $datetime);
203 $pattern = $this->resolve_pattern($pattern_or_width_or_skeleton);
204 $tokens = self::tokenize($pattern);
205
206 $rc = '';
207
208 foreach ($tokens as $token)
209 {
210 if (is_array($token)) // a callback: method name, sub-pattern
211 {
212 $token = $this->{$token[0]}($datetime, $token[1], $token[2]);
213 }
214
215 $rc .= $token;
216 }
217
218 return $rc;
219 }
220
221 /**
222 * Resolves the specified pattern, which can be a width, a skeleton or an actual pattern.
223 *
224 * @param string $pattern_or_width_or_skeleton
225 *
226 * @return string
227 */
228 protected function resolve_pattern($pattern_or_width_or_skeleton)
229 {
230 $pattern = $pattern_or_width_or_skeleton;
231
232 if ($pattern_or_width_or_skeleton{0} === ':')
233 {
234 $skeleton = substr($pattern, 1);
235 $available_formats = $this->calendar['dateTimeFormats']['availableFormats'];
236
237 if (isset($available_formats[$skeleton]))
238 {
239 $pattern = $available_formats[$skeleton];
240 }
241 }
242 else if (in_array($pattern = $pattern_or_width_or_skeleton, array('full', 'long', 'medium', 'short')))
243 {
244 $calendar = $this->calendar;
245 $width = $pattern = $pattern_or_width_or_skeleton;
246 $datetime_pattern = $calendar['dateTimeFormats'][$width];
247 $date_pattern = $calendar['dateFormats'][$width];
248 $time_pattern = $calendar['timeFormats'][$width];
249 $pattern = strtr($datetime_pattern, array('{1}' => $date_pattern, '{0}' => $time_pattern));
250 }
251
252 return $pattern;
253 }
254
255 /**
256 * Alias to the {@link format()} method.
257 */
258 public function __invoke($datetime, $pattern_or_width_or_skeleton)
259 {
260 return $this->format($datetime, $pattern_or_width_or_skeleton);
261 }
262
263 /*
264 * era (G)
265 */
266
267 /**
268 * Era - Replaced with the Era string for the current date. One to three letters for the
269 * abbreviated form, four letters for the long form, five for the narrow form. [1..3,4,5]
270 *
271 * @param DateTime $datetime
272 * @param string $pattern a pattern.
273 * @param int $length Number of repetition.
274 *
275 * @return string era
276 * @todo How to support multiple Eras?, e.g. Japanese.
277 */
278 protected function format_era(DateTime $datetime, $pattern, $length)
279 {
280 $era = ($datetime->year > 0) ? 1 : 0;
281
282 switch($length)
283 {
284 case 1:
285 case 2:
286 case 3: return $this->calendar->abbreviated_eras[$era];
287 case 4: return $this->calendar->wide_eras[$era];
288 case 5: return $this->calendar->narrow_eras[$era];
289 }
290 }
291
292 /*
293 * year (y)
294 */
295
296 /**
297 * Year. Normally the length specifies the padding, but for two letters it also specifies the
298 * maximum length. [1..n]
299 *
300 * @param Datetime $datetime
301 * @param string $pattern a pattern.
302 * @param int $length Number of repetition.
303 *
304 * @return string formatted year
305 */
306 protected function format_year(Datetime $datetime, $pattern, $length)
307 {
308 $year = $datetime->year;
309
310 if ($length == 2)
311 {
312 $year = $year % 100;
313 }
314
315 return str_pad($year, $length, '0', STR_PAD_LEFT);
316 }
317
318 /*
319 * quarter (Q,q)
320 */
321
322 /**
323 * Quarter - Use one or two "Q" for the numerical quarter, three for the abbreviation, or four
324 * for the full (wide) name. [1..2,3,4]
325 *
326 * @param \DateTime $datetime Datetime.
327 * @param string $pattern Pattern.
328 * @param int $length Number of repetition.
329 *
330 * @return string
331 */
332 protected function format_quarter(DateTime $datetime, $pattern, $length)
333 {
334 $quarter = $datetime->quarter;
335
336 switch ($length)
337 {
338 case 1: return $quarter;
339 case 2: return str_pad($quarter, 2, '0', STR_PAD_LEFT);
340 case 3: return $this->calendar->abbreviated_quarters[$quarter];
341 case 4: return $this->calendar->wide_quarters[$quarter];
342 }
343 }
344
345 /**
346 * Stand-Alone Quarter - Use one or two "q" for the numerical quarter, three for the
347 * abbreviation, or four for the full (wide) name. [1..2,3,4]
348 *
349 * @param array $date result of getdate().
350 * @param string $pattern a pattern.
351 * @param int $length Number of repetition.
352 *
353 * @return string
354 */
355 protected function format_standalone_quarter(DateTime $datetime, $pattern, $length)
356 {
357 $quarter = $datetime->quarter;
358
359 switch ($length)
360 {
361 case 1: return $quarter;
362 case 2: return str_pad($quarter, 2, '0', STR_PAD_LEFT);
363 case 3: return $this->calendar->standalone_abbreviated_quarters[$quarter];
364 case 4: return $this->calendar->standalone_wide_quarters[$quarter];
365 }
366 }
367
368 /*
369 * month (M|L)
370 */
371
372 /**
373 * Month - Use one or two "M" for the numerical month, three for the abbreviation, four for
374 * the full name, or five for the narrow name. [1..2,3,4,5]
375 *
376 * @param DateTime $datetime
377 * @param string $pattern a pattern.
378 * @param int $length Number of repetition.
379 *
380 * @return string
381 */
382 protected function format_month(DateTime $datetime, $pattern, $length)
383 {
384 $month = $datetime->month;
385
386 switch ($length)
387 {
388 case 1: return $month;
389 case 2: return str_pad($month, 2, '0', STR_PAD_LEFT);
390 case 3: return $this->calendar->abbreviated_months[$month];
391 case 4: return $this->calendar->wide_months[$month];
392 case 5: return $this->calendar->narrow_months[$month];
393 }
394 }
395
396 /**
397 * Stand-Alone Month - Use one or two "L" for the numerical month, three for the abbreviation,
398 * or four for the full (wide) name, or 5 for the narrow name. [1..2,3,4,5]
399 *
400 * @param DateTime $datetime
401 * @param string $pattern a pattern.
402 * @param int $length Number of repetition.
403 *
404 * @return string formatted month.
405 */
406 protected function format_standalone_month(DateTime $datetime, $pattern, $length)
407 {
408 $month = $datetime->month;
409
410 switch ($length)
411 {
412 case 1: return $month;
413 case 2: return str_pad($month, 2, '0', STR_PAD_LEFT);
414 case 3: return $this->calendar->standalone_abbreviated_months[$month];
415 case 4: return $this->calendar->standalone_wide_months[$month];
416 case 5: return $this->calendar->standalone_narrow_months[$month];
417 }
418 }
419
420 /*
421 * week (w|W)
422 */
423
424 /**
425 * Week of Year. [1..2]
426 *
427 * @param DateTime $datetime
428 * @param string $pattern a pattern.
429 * @param int $length Number of repetition.
430 *
431 * @return integer
432 */
433 protected function format_week_of_year(DateTime $datetime, $pattern, $length)
434 {
435 if ($length > 2)
436 {
437 return;
438 }
439
440 $week = $datetime->week;
441
442 return $length == 1 ? $week : str_pad($week, 2, '0', STR_PAD_LEFT);
443 }
444
445 /**
446 * Week of Month. [1]
447 *
448 * @param DateTime $datetime
449 * @param string $pattern a pattern.
450 * @param int $length Number of repetition.
451 *
452 * @return integer week of month
453 */
454 protected function format_week_of_month(DateTime $datetime, $pattern, $length)
455 {
456 if ($length == 1)
457 {
458 return ceil($datetime->day / 7);
459 }
460 }
461
462 /*
463 * day (d,D,F)
464 */
465
466 /**
467 * Date - Day of the month. [1..2]
468 *
469 * @param DateTime $datetime
470 * @param string $pattern a pattern.
471 * @param int $length Number of repetition.
472 *
473 * @return string day of the month
474 */
475 protected function format_day_of_month(DateTime $datetime, $pattern, $length)
476 {
477 $day = $datetime->day;
478
479 if ($length == 1)
480 {
481 return $day;
482 }
483 else if ($length == 2)
484 {
485 return str_pad($day, 2, '0', STR_PAD_LEFT);
486 }
487 }
488
489 /**
490 * Day of year. [1..3]
491 *
492 * @param DateTime $datetime
493 * @param string $pattern a pattern.
494 * @param int $length Number of repetition.
495 *
496 * @return string
497 */
498 protected function format_day_of_year(DateTime $datetime, $pattern, $length)
499 {
500 $day = $datetime->year_day;
501
502 if ($length > 3)
503 {
504 return;
505 }
506
507 return str_pad($day, $length, '0', STR_PAD_LEFT);
508 }
509
510 /**
511 * Day of Week in Month. The example is for the 2nd Wed in July. [1]
512 *
513 * @param DateTime $datetime
514 * @param string $pattern a pattern.
515 * @param int $length Number of repetition.
516 *
517 * @return int
518 */
519 protected function format_day_of_week_in_month(DateTime $datetime, $pattern, $length)
520 {
521 if ($length == 1)
522 {
523 return floor(($datetime->day + 6) / 7);
524 }
525 }
526
527 /*
528 * weekday (E,e,c)
529 */
530
531 /**
532 * Day of week - Use one through three letters for the short day, or four for the full name,
533 * five for the narrow name, or six for the short name. [1..3,4,5,6]
534 *
535 * @param DateTime $datetime
536 * @param string $pattern a pattern.
537 * @param int $length Number of repetition.
538 *
539 * @return string
540 */
541 protected function format_day_in_week(DateTime $datetime, $pattern)
542 {
543 static $translate = array
544 (
545 1 => 'mon',
546 2 => 'tue',
547 3 => 'wed',
548 4 => 'thu',
549 5 => 'fri',
550 6 => 'sat',
551 7 => 'sun'
552 );
553
554 $day = $datetime->weekday;
555
556 switch ($pattern)
557 {
558 case 'E':
559 case 'EE':
560 case 'EEE':
561 case 'eee':
562 return $this->calendar->abbreviated_days[$translate[$day]];
563
564 case 'EEEE':
565 case 'eeee':
566 return $this->calendar->wide_days[$translate[$day]];
567
568 case 'EEEEE':
569 case 'eeeee':
570 return $this->calendar->narrow_days[$translate[$day]];
571
572 case 'EEEEEE':
573 case 'eeeeee':
574 return $this->calendar->short_days[$translate[$day]];
575
576 case 'e':
577 case 'ee':
578 case 'c':
579 return $day;
580
581 case 'ccc':
582 return $this->calendar->standalone_abbreviated_days[$translate[$day]];
583
584 case 'cccc':
585 return $this->calendar->standalone_wide_days[$translate[$day]];
586
587 case 'ccccc':
588 return $this->calendar->standalone_narrow_days[$translate[$day]];
589
590 case 'cccccc':
591 return $this->calendar->standalone_short_days[$translate[$day]];
592 }
593 }
594
595 /*
596 * period (a)
597 */
598
599 /**
600 * AM or PM. [1]
601 *
602 * @param DateTime $datetime
603 * @param string $pattern a pattern.
604 * @param int $length Number of repetition.
605 *
606 * @return string AM or PM designator
607 */
608 protected function format_period(DateTime $datetime, $pattern, $length)
609 {
610 return $this->calendar['dayPeriods']['format']['abbreviated'][$datetime->hour < 12 ? 'am' : 'pm'];
611 }
612
613 /*
614 * hour (h,H,K,k)
615 */
616
617 /**
618 * Hour [1-12]. When used in skeleton data or in a skeleton passed in an API for flexible data
619 * pattern generation, it should match the 12-hour-cycle format preferred by the locale
620 * (h or K); it should not match a 24-hour-cycle format (H or k). Use hh for zero
621 * padding. [1..2]
622 *
623 * @param DateTime $datetime
624 * @param string $pattern a pattern.
625 * @param int $length Number of repetition.
626 *
627 * @return string
628 */
629 protected function format_hour12(DateTime $datetime, $pattern, $length)
630 {
631 $hour = $datetime->hour;
632 $hour = ($hour == 12) ? 12 : $hour % 12;
633
634 if ($length == 1)
635 {
636 return $hour;
637 }
638 else if ($length == 2)
639 {
640 return str_pad($hour, 2, '0', STR_PAD_LEFT);
641 }
642 }
643
644 /**
645 * Hour [0-23]. When used in skeleton data or in a skeleton passed in an API for flexible
646 * data pattern generation, it should match the 24-hour-cycle format preferred by the
647 * locale (H or k); it should not match a 12-hour-cycle format (h or K). Use HH for zero
648 * padding. [1..2]
649 *
650 * @param DateTime $datetime
651 * @param string $pattern a pattern.
652 * @param int $length Number of repetition.
653 *
654 * @return string
655 */
656 protected function format_hour24(DateTime $datetime, $pattern, $length)
657 {
658 $hour = $datetime->hour;
659
660 if ($length == 1)
661 {
662 return $hour;
663 }
664 else if ($length == 2)
665 {
666 return str_pad($hour, 2, '0', STR_PAD_LEFT);
667 }
668 }
669
670 /**
671 * Hour [0-11]. When used in a skeleton, only matches K or h, see above. Use KK for zero
672 * padding. [1..2]
673 *
674 * @param DateTime $datetime.
675 * @param string $pattern a pattern.
676 * @param int $length Number of repetition.
677 *
678 * @return integer hours in AM/PM format.
679 */
680 protected function format_hour_in_period(DateTime $datetime, $pattern, $length)
681 {
682 $hour = $datetime->hour % 12;
683
684 if ($length == 1)
685 {
686 return $hour;
687 }
688 else if ($length == 2)
689 {
690 return str_pad($hour, 2, '0', STR_PAD_LEFT);
691 }
692 }
693
694 /**
695 * Hour [1-24]. When used in a skeleton, only matches k or H, see above. Use kk for zero
696 * padding. [1..2]
697 *
698 * @param DateTime $datetime
699 * @param string $pattern a pattern.
700 * @param int $length Number of repetition.
701 *
702 * @return integer
703 */
704 protected function format_hour_in_day(DateTime $datetime, $pattern, $length)
705 {
706 $hour = $datetime->hour;
707
708 if ($hour == 0)
709 {
710 $hour = 24;
711 }
712
713 if ($length == 1)
714 {
715 return $hour;
716 }
717 else if ($length == 2)
718 {
719 return str_pad($hour, 2, '0', STR_PAD_LEFT);
720 }
721 }
722
723 /*
724 * minute (m)
725 */
726
727 /**
728 * Minute. Use one or two "m" for zero padding.
729 *
730 * @param DateTime $datetime
731 * @param string $pattern a pattern.
732 * @param int $length Number of repetition
733 *
734 * @return string minutes.
735 */
736 protected function format_minutes(DateTime $datetime, $pattern, $length)
737 {
738 $minutes = $datetime->minute;
739
740 if ($length == 1)
741 {
742 return $minutes;
743 }
744 else if ($length == 2)
745 {
746 return str_pad($minutes, 2, '0', STR_PAD_LEFT);
747 }
748 }
749
750 /*
751 * second
752 */
753
754 /**
755 * Second. Use one or two "s" for zero padding.
756 *
757 * @param DateTime $datetime
758 * @param string $pattern a pattern.
759 * @param int $length Number of repetition.
760 *
761 * @return string seconds
762 */
763 protected function format_seconds(DateTime $datetime, $pattern, $length)
764 {
765 $seconds = $datetime->second;
766
767 if ($length == 1)
768 {
769 return $seconds;
770 }
771 else if ($length == 2)
772 {
773 return str_pad($seconds, 2, '0', STR_PAD_LEFT);
774 }
775 }
776
777 /*
778 * zone (z,Z,v)
779 */
780
781 /**
782 * Time Zone.
783 *
784 * @param DateTime $datetime.
785 * @param string $pattern a pattern.
786 * @param int $length Number of repetition.
787 *
788 * @return string time zone
789 */
790 protected function format_timezone(DateTime $datetime, $pattern, $length)
791 {
792 if ($pattern{0} === 'z' || $pattern{0} === 'v')
793 {
794 return $datetime->format('T');
795 }
796 else if ($pattern{0} === 'Z')
797 {
798 return $datetime->format('O');
799 }
800 }
801 }