1: <?php
2: /**
3: * CDateFormatter class file.
4: *
5: * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6: * @author Qiang Xue <qiang.xue@gmail.com>
7: * @link http://www.yiiframework.com/
8: * @copyright 2008-2013 Yii Software LLC
9: * @license http://www.yiiframework.com/license/
10: */
11:
12: /**
13: * CDateFormatter provides date/time localization functionalities.
14: *
15: * CDateFormatter allows you to format dates and times in a locale-sensitive manner.
16: * Patterns are interpreted in the locale that the CDateFormatter instance
17: * is associated with. For example, month names and weekday names may vary
18: * under different locales, which yields different formatting results.
19: * The patterns that CDateFormatter recognizes are as defined in
20: * {@link http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns CLDR}.
21: *
22: * CDateFormatter supports predefined patterns as well as customized ones:
23: * <ul>
24: * <li>The method {@link formatDateTime()} formats date or time or both using
25: * predefined patterns which include 'full', 'long', 'medium' (default) and 'short'.</li>
26: * <li>The method {@link format()} formats datetime using the specified pattern.
27: * See {@link http://www.unicode.org/reports/tr35/#Date_Format_Patterns} for
28: * details about the recognized pattern characters.</li>
29: * </ul>
30: *
31: * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
32: * @author Qiang Xue <qiang.xue@gmail.com>
33: * @package system.i18n
34: * @since 1.0
35: */
36: class CDateFormatter extends CComponent
37: {
38: /**
39: * @var array pattern characters mapping to the corresponding translator methods
40: */
41: private static $_formatters=array(
42: 'G'=>'formatEra',
43: 'y'=>'formatYear',
44: 'M'=>'formatMonth',
45: 'L'=>'formatMonth',
46: 'd'=>'formatDay',
47: 'h'=>'formatHour12',
48: 'H'=>'formatHour24',
49: 'm'=>'formatMinutes',
50: 's'=>'formatSeconds',
51: 'E'=>'formatDayInWeek',
52: 'c'=>'formatDayInWeek',
53: 'e'=>'formatDayInWeek',
54: 'D'=>'formatDayInYear',
55: 'F'=>'formatDayInMonth',
56: 'w'=>'formatWeekInYear',
57: 'W'=>'formatWeekInMonth',
58: 'a'=>'formatPeriod',
59: 'k'=>'formatHourInDay',
60: 'K'=>'formatHourInPeriod',
61: 'z'=>'formatTimeZone',
62: 'Z'=>'formatTimeZone',
63: 'v'=>'formatTimeZone',
64: );
65:
66: private $_locale;
67:
68: /**
69: * Constructor.
70: * @param mixed $locale locale ID (string) or CLocale instance
71: */
72: public function __construct($locale)
73: {
74: if(is_string($locale))
75: $this->_locale=CLocale::getInstance($locale);
76: else
77: $this->_locale=$locale;
78: }
79:
80: /**
81: * Formats a date according to a customized pattern.
82: * @param string $pattern the pattern (See {@link http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns})
83: * @param mixed $time UNIX timestamp or a string in strtotime format
84: * @return string formatted date time. Null if $time is null. (the null value check is available since Yii 1.1.11)
85: */
86: public function format($pattern,$time)
87: {
88: if($time===null)
89: return null;
90:
91: if(is_string($time))
92: {
93: if(ctype_digit($time) || ($time{0}=='-' && ctype_digit(substr($time, 1))))
94: $time=(int)$time;
95: else
96: $time=strtotime($time);
97: }
98: $date=CTimestamp::getDate($time,false,false);
99: $tokens=$this->parseFormat($pattern);
100: foreach($tokens as &$token)
101: {
102: if(is_array($token)) // a callback: method name, sub-pattern
103: $token=$this->{$token[0]}($token[1],$date);
104: }
105: return implode('',$tokens);
106: }
107:
108: /**
109: * Formats a date according to a predefined pattern.
110: * The predefined pattern is determined based on the date pattern width and time pattern width.
111: * @param mixed $timestamp UNIX timestamp or a string in strtotime format
112: * @param string $dateWidth width of the date pattern. It can be 'full', 'long', 'medium' and 'short'.
113: * If null, it means the date portion will NOT appear in the formatting result
114: * @param string $timeWidth width of the time pattern. It can be 'full', 'long', 'medium' and 'short'.
115: * If null, it means the time portion will NOT appear in the formatting result
116: * @return string formatted date time.
117: */
118: public function formatDateTime($timestamp,$dateWidth='medium',$timeWidth='medium')
119: {
120: if(!empty($dateWidth))
121: $date=$this->format($this->_locale->getDateFormat($dateWidth),$timestamp);
122:
123: if(!empty($timeWidth))
124: $time=$this->format($this->_locale->getTimeFormat($timeWidth),$timestamp);
125:
126: if(isset($date) && isset($time))
127: {
128: $dateTimePattern=$this->_locale->getDateTimeFormat();
129: return strtr($dateTimePattern,array('{0}'=>$time,'{1}'=>$date));
130: }
131: elseif(isset($date))
132: return $date;
133: elseif(isset($time))
134: return $time;
135: }
136:
137: /**
138: * Parses the datetime format pattern.
139: * @param string $pattern the pattern to be parsed
140: * @return array tokenized parsing result
141: */
142: protected function parseFormat($pattern)
143: {
144: static $formats=array(); // cache
145: if(isset($formats[$pattern]))
146: return $formats[$pattern];
147: $tokens=array();
148: $n=strlen($pattern);
149: $isLiteral=false;
150: $literal='';
151: for($i=0;$i<$n;++$i)
152: {
153: $c=$pattern[$i];
154: if($c==="'")
155: {
156: if($i<$n-1 && $pattern[$i+1]==="'")
157: {
158: $tokens[]="'";
159: $i++;
160: }
161: elseif($isLiteral)
162: {
163: $tokens[]=$literal;
164: $literal='';
165: $isLiteral=false;
166: }
167: else
168: {
169: $isLiteral=true;
170: $literal='';
171: }
172: }
173: elseif($isLiteral)
174: $literal.=$c;
175: else
176: {
177: for($j=$i+1;$j<$n;++$j)
178: {
179: if($pattern[$j]!==$c)
180: break;
181: }
182: $p=str_repeat($c,$j-$i);
183: if(isset(self::$_formatters[$c]))
184: $tokens[]=array(self::$_formatters[$c],$p);
185: else
186: $tokens[]=$p;
187: $i=$j-1;
188: }
189: }
190: if($literal!=='')
191: $tokens[]=$literal;
192:
193: return $formats[$pattern]=$tokens;
194: }
195:
196: /**
197: * Get the year.
198: * "yy" will return the last two digits of year.
199: * "y...y" will pad the year with 0 in the front, e.g. "yyyyy" will generate "02008" for year 2008.
200: * @param string $pattern a pattern.
201: * @param array $date result of {@link CTimestamp::getdate}.
202: * @return string formatted year
203: */
204: protected function formatYear($pattern,$date)
205: {
206: $year=$date['year'];
207: if($pattern==='yy')
208: return str_pad($year%100,2,'0',STR_PAD_LEFT);
209: else
210: return str_pad($year,strlen($pattern),'0',STR_PAD_LEFT);
211: }
212:
213: /**
214: * Get the month.
215: * "M" will return integer 1 through 12;
216: * "MM" will return two digits month number with necessary zero padding, e.g. 05;
217: * "MMM" will return the abrreviated month name, e.g. "Jan";
218: * "MMMM" will return the full month name, e.g. "January";
219: * "MMMMM" will return the narrow month name, e.g. "J";
220: * @param string $pattern a pattern.
221: * @param array $date result of {@link CTimestamp::getdate}.
222: * @throws CException if "month" pattern is unknown
223: * @return string month name
224: */
225: protected function formatMonth($pattern,$date)
226: {
227: $month=$date['mon'];
228: switch($pattern)
229: {
230: case 'M':
231: return $month;
232: case 'MM':
233: return str_pad($month,2,'0',STR_PAD_LEFT);
234: case 'MMM':
235: return $this->_locale->getMonthName($month,'abbreviated');
236: case 'MMMM':
237: return $this->_locale->getMonthName($month,'wide');
238: case 'MMMMM':
239: return $this->_locale->getMonthName($month,'narrow');
240: case 'L':
241: return $month;
242: case 'LL':
243: return str_pad($month,2,'0',STR_PAD_LEFT);
244: case 'LLL':
245: return $this->_locale->getMonthName($month,'abbreviated', true);
246: case 'LLLL':
247: return $this->_locale->getMonthName($month,'wide', true);
248: case 'LLLLL':
249: return $this->_locale->getMonthName($month,'narrow', true);
250: default:
251: throw new CException(Yii::t('yii','The pattern for month must be "M", "MM", "MMM", "MMMM", "L", "LL", "LLL" or "LLLL".'));
252: }
253: }
254:
255: /**
256: * Get the day of the month.
257: * "d" for non-padding, "dd" will always return 2 digits day numbers, e.g. 05.
258: * @param string $pattern a pattern.
259: * @param array $date result of {@link CTimestamp::getdate}.
260: * @throws CException if "day" pattern is unknown
261: * @return string day of the month
262: */
263: protected function formatDay($pattern,$date)
264: {
265: $day=$date['mday'];
266: if($pattern==='d')
267: return $day;
268: elseif($pattern==='dd')
269: return str_pad($day,2,'0',STR_PAD_LEFT);
270: else
271: throw new CException(Yii::t('yii','The pattern for day of the month must be "d" or "dd".'));
272: }
273:
274: /**
275: * Get the day in the year, e.g. [1-366]
276: * @param string $pattern a pattern.
277: * @param array $date result of {@link CTimestamp::getdate}.
278: * @throws CException is "dayInYear" pattern is unknown
279: * @return integer hours in AM/PM format.
280: */
281: protected function formatDayInYear($pattern,$date)
282: {
283: $day=$date['yday'];
284: if(($n=strlen($pattern))<=3)
285: return str_pad($day,$n,'0',STR_PAD_LEFT);
286: else
287: throw new CException(Yii::t('yii','The pattern for day in year must be "D", "DD" or "DDD".'));
288: }
289:
290: /**
291: * Get day of week in the month, e.g. 2nd Wed in July.
292: * @param string $pattern a pattern.
293: * @param array $date result of {@link CTimestamp::getdate}.
294: * @throws CException if "dayInMonth" pattern is unknown
295: * @return integer day in month
296: * @see http://www.unicode.org/reports/tr35/#Date_Format_Patterns
297: */
298: protected function formatDayInMonth($pattern,$date)
299: {
300: if($pattern==='F')
301: return (int)(($date['mday']+6)/7);
302: else
303: throw new CException(Yii::t('yii','The pattern for day in month must be "F".'));
304: }
305:
306: /**
307: * Get the day of the week.
308: * "E", "EE", "EEE" will return abbreviated week day name, e.g. "Tues";
309: * "EEEE" will return full week day name;
310: * "EEEEE" will return the narrow week day name, e.g. "T";
311: * @param string $pattern a pattern.
312: * @param array $date result of {@link CTimestamp::getdate}.
313: * @throws CException if "dayInWeek" pattern is unknown
314: * @return string day of the week.
315: * @see http://www.unicode.org/reports/tr35/#Date_Format_Patterns
316: */
317: protected function formatDayInWeek($pattern,$date)
318: {
319: $day=$date['wday'];
320: switch($pattern)
321: {
322: case 'E':
323: case 'EE':
324: case 'EEE':
325: case 'eee':
326: return $this->_locale->getWeekDayName($day,'abbreviated');
327: case 'EEEE':
328: case 'eeee':
329: return $this->_locale->getWeekDayName($day,'wide');
330: case 'EEEEE':
331: case 'eeeee':
332: return $this->_locale->getWeekDayName($day,'narrow');
333: case 'e':
334: case 'ee':
335: case 'c':
336: return $day ? $day : 7;
337: case 'ccc':
338: return $this->_locale->getWeekDayName($day,'abbreviated',true);
339: case 'cccc':
340: return $this->_locale->getWeekDayName($day,'wide',true);
341: case 'ccccc':
342: return $this->_locale->getWeekDayName($day,'narrow',true);
343: default:
344: throw new CException(Yii::t('yii','The pattern for day of the week must be "E", "EE", "EEE", "EEEE", "EEEEE", "e", "ee", "eee", "eeee", "eeeee", "c", "cccc" or "ccccc".'));
345: }
346: }
347:
348: /**
349: * Get the AM/PM designator, 12 noon is PM, 12 midnight is AM.
350: * @param string $pattern a pattern.
351: * @param array $date result of {@link CTimestamp::getdate}.
352: * @throws CException if "period" pattern is unknown
353: * @return string AM or PM designator
354: */
355: protected function formatPeriod($pattern,$date)
356: {
357: if($pattern==='a')
358: {
359: if(intval($date['hours']/12))
360: return $this->_locale->getPMName();
361: else
362: return $this->_locale->getAMName();
363: }
364: else
365: throw new CException(Yii::t('yii','The pattern for AM/PM marker must be "a".'));
366: }
367:
368: /**
369: * Get the hours in 24 hour format, i.e. [0-23].
370: * "H" for non-padding, "HH" will always return 2 characters.
371: * @param string $pattern a pattern.
372: * @param array $date result of {@link CTimestamp::getdate}.
373: * @throws CException if "hour24" pattern is unknown
374: * @return string hours in 24 hour format.
375: */
376: protected function formatHour24($pattern,$date)
377: {
378: $hour=$date['hours'];
379: if($pattern==='H')
380: return $hour;
381: elseif($pattern==='HH')
382: return str_pad($hour,2,'0',STR_PAD_LEFT);
383: else
384: throw new CException(Yii::t('yii','The pattern for 24 hour format must be "H" or "HH".'));
385: }
386:
387: /**
388: * Get the hours in 12 hour format, i.e., [1-12]
389: * "h" for non-padding, "hh" will always return 2 characters.
390: * @param string $pattern a pattern.
391: * @param array $date result of {@link CTimestamp::getdate}.
392: * @throws CException if "hour12" pattern is unknown
393: * @return string hours in 12 hour format.
394: */
395: protected function formatHour12($pattern,$date)
396: {
397: $hour=$date['hours'];
398: $hour=($hour==12|$hour==0)?12:($hour)%12;
399: if($pattern==='h')
400: return $hour;
401: elseif($pattern==='hh')
402: return str_pad($hour,2,'0',STR_PAD_LEFT);
403: else
404: throw new CException(Yii::t('yii','The pattern for 12 hour format must be "h" or "hh".'));
405: }
406:
407: /**
408: * Get the hours [1-24].
409: * 'k' for non-padding, and 'kk' with 2 characters padding.
410: * @param string $pattern a pattern.
411: * @param array $date result of {@link CTimestamp::getdate}.
412: * @throws CException if "hourInDay" pattern is unknown
413: * @return integer hours [1-24]
414: */
415: protected function formatHourInDay($pattern,$date)
416: {
417: $hour=$date['hours']==0?24:$date['hours'];
418: if($pattern==='k')
419: return $hour;
420: elseif($pattern==='kk')
421: return str_pad($hour,2,'0',STR_PAD_LEFT);
422: else
423: throw new CException(Yii::t('yii','The pattern for hour in day must be "k" or "kk".'));
424: }
425:
426: /**
427: * Get the hours in AM/PM format, e.g [0-11]
428: * "K" for non-padding, "KK" will always return 2 characters.
429: * @param string $pattern a pattern.
430: * @param array $date result of {@link CTimestamp::getdate}.
431: * @throws CException if "hourInPeriod" pattern is unknown
432: * @return integer hours in AM/PM format.
433: */
434: protected function formatHourInPeriod($pattern,$date)
435: {
436: $hour=$date['hours']%12;
437: if($pattern==='K')
438: return $hour;
439: elseif($pattern==='KK')
440: return str_pad($hour,2,'0',STR_PAD_LEFT);
441: else
442: throw new CException(Yii::t('yii','The pattern for hour in AM/PM must be "K" or "KK".'));
443: }
444:
445: /**
446: * Get the minutes.
447: * "m" for non-padding, "mm" will always return 2 characters.
448: * @param string $pattern a pattern.
449: * @param array $date result of {@link CTimestamp::getdate}.
450: * @throws CException if "minutes" pattern is unknown
451: * @return string minutes.
452: */
453: protected function formatMinutes($pattern,$date)
454: {
455: $minutes=$date['minutes'];
456: if($pattern==='m')
457: return $minutes;
458: elseif($pattern==='mm')
459: return str_pad($minutes,2,'0',STR_PAD_LEFT);
460: else
461: throw new CException(Yii::t('yii','The pattern for minutes must be "m" or "mm".'));
462: }
463:
464: /**
465: * Get the seconds.
466: * "s" for non-padding, "ss" will always return 2 characters.
467: * @param string $pattern a pattern.
468: * @param array $date result of {@link CTimestamp::getdate}.
469: * @throws CException if "seconds" pattern is unknown
470: * @return string seconds
471: */
472: protected function formatSeconds($pattern,$date)
473: {
474: $seconds=$date['seconds'];
475: if($pattern==='s')
476: return $seconds;
477: elseif($pattern==='ss')
478: return str_pad($seconds,2,'0',STR_PAD_LEFT);
479: else
480: throw new CException(Yii::t('yii','The pattern for seconds must be "s" or "ss".'));
481: }
482:
483: /**
484: * Get the week in the year.
485: * @param string $pattern a pattern.
486: * @param array $date result of {@link CTimestamp::getdate}.
487: * @throws CException if "weekInYear" pattern is unknown
488: * @return integer week in year
489: */
490: protected function formatWeekInYear($pattern,$date)
491: {
492: if($pattern==='w')
493: return @date('W',@mktime(0,0,0,$date['mon'],$date['mday'],$date['year']));
494: else
495: throw new CException(Yii::t('yii','The pattern for week in year must be "w".'));
496: }
497:
498: /**
499: * Get week in the month.
500: * @param array $pattern result of {@link CTimestamp::getdate}.
501: * @param string $date a pattern.
502: * @throws CException if "weekInMonth" pattern is unknown
503: * @return integer week in month
504: */
505: protected function formatWeekInMonth($pattern,$date)
506: {
507: if($pattern==='W')
508: {
509: $weekDay=date('N',mktime(0,0,0,$date['mon'],1,$date['year']));
510: return floor(($weekDay+$date['mday']-2)/7)+1;
511: }
512: else
513: throw new CException(Yii::t('yii','The pattern for week in month must be "W".'));
514: }
515:
516: /**
517: * Get the timezone of the server machine.
518: * @param string $pattern a pattern.
519: * @param array $date result of {@link CTimestamp::getdate}.
520: * @throws CException if "timeZone" pattern is unknown
521: * @return string time zone
522: * @todo How to get the timezone for a different region?
523: */
524: protected function formatTimeZone($pattern,$date)
525: {
526: if($pattern[0]==='z' || $pattern[0]==='v')
527: return @date('T', @mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']));
528: elseif($pattern[0]==='Z')
529: return @date('P', @mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']));
530: else
531: throw new CException(Yii::t('yii','The pattern for time zone must be "z" or "v".'));
532: }
533:
534: /**
535: * Get the era. i.e. in gregorian, year > 0 is AD, else BC.
536: * @param string $pattern a pattern.
537: * @param array $date result of {@link CTimestamp::getdate}.
538: * @throws CException if "era" pattern is unknown
539: * @return string era
540: * @todo How to support multiple Eras?, e.g. Japanese.
541: */
542: protected function formatEra($pattern,$date)
543: {
544: $era=$date['year']>0 ? 1 : 0;
545: switch($pattern)
546: {
547: case 'G':
548: case 'GG':
549: case 'GGG':
550: return $this->_locale->getEraName($era,'abbreviated');
551: case 'GGGG':
552: return $this->_locale->getEraName($era,'wide');
553: case 'GGGGG':
554: return $this->_locale->getEraName($era,'narrow');
555: default:
556: throw new CException(Yii::t('yii','The pattern for era must be "G", "GG", "GGG", "GGGG" or "GGGGG".'));
557: }
558: }
559: }
560: