* @author Qiang Xue * @link http://www.yiiframework.com/ * @copyright 2008-2013 Yii Software LLC * @license http://www.yiiframework.com/license/ */ /** * CDateFormatter provides date/time localization functionalities. * * CDateFormatter allows you to format dates and times in a locale-sensitive manner. * Patterns are interpreted in the locale that the CDateFormatter instance * is associated with. For example, month names and weekday names may vary * under different locales, which yields different formatting results. * The patterns that CDateFormatter recognizes are as defined in * {@link http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns CLDR}. * * CDateFormatter supports predefined patterns as well as customized ones: * * * @author Wei Zhuo * @author Qiang Xue * @package system.i18n * @since 1.0 */ class CDateFormatter extends CComponent { /** * @var array pattern characters mapping to the corresponding translator methods */ private static $_formatters=array( 'G'=>'formatEra', 'y'=>'formatYear', 'M'=>'formatMonth', 'L'=>'formatMonth', 'd'=>'formatDay', 'h'=>'formatHour12', 'H'=>'formatHour24', 'm'=>'formatMinutes', 's'=>'formatSeconds', 'E'=>'formatDayInWeek', 'c'=>'formatDayInWeek', 'e'=>'formatDayInWeek', 'D'=>'formatDayInYear', 'F'=>'formatDayInMonth', 'w'=>'formatWeekInYear', 'W'=>'formatWeekInMonth', 'a'=>'formatPeriod', 'k'=>'formatHourInDay', 'K'=>'formatHourInPeriod', 'z'=>'formatTimeZone', 'Z'=>'formatTimeZone', 'v'=>'formatTimeZone', ); private $_locale; /** * Constructor. * @param mixed $locale locale ID (string) or CLocale instance */ public function __construct($locale) { if(is_string($locale)) $this->_locale=CLocale::getInstance($locale); else $this->_locale=$locale; } /** * Formats a date according to a customized pattern. * @param string $pattern the pattern (See {@link http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns}) * @param mixed $time UNIX timestamp or a string in strtotime format * @return string formatted date time. Null if $time is null. (the null value check is available since Yii 1.1.11) */ public function format($pattern,$time) { if($time===null) return null; if(is_string($time)) { if(ctype_digit($time) || ($time{0}=='-' && ctype_digit(substr($time, 1)))) $time=(int)$time; else $time=strtotime($time); } $date=CTimestamp::getDate($time,false,false); $tokens=$this->parseFormat($pattern); foreach($tokens as &$token) { if(is_array($token)) // a callback: method name, sub-pattern $token=$this->{$token[0]}($token[1],$date); } return implode('',$tokens); } /** * Formats a date according to a predefined pattern. * The predefined pattern is determined based on the date pattern width and time pattern width. * @param mixed $timestamp UNIX timestamp or a string in strtotime format * @param string $dateWidth width of the date pattern. It can be 'full', 'long', 'medium' and 'short'. * If null, it means the date portion will NOT appear in the formatting result * @param string $timeWidth width of the time pattern. It can be 'full', 'long', 'medium' and 'short'. * If null, it means the time portion will NOT appear in the formatting result * @return string formatted date time. */ public function formatDateTime($timestamp,$dateWidth='medium',$timeWidth='medium') { if(!empty($dateWidth)) $date=$this->format($this->_locale->getDateFormat($dateWidth),$timestamp); if(!empty($timeWidth)) $time=$this->format($this->_locale->getTimeFormat($timeWidth),$timestamp); if(isset($date) && isset($time)) { $dateTimePattern=$this->_locale->getDateTimeFormat(); return strtr($dateTimePattern,array('{0}'=>$time,'{1}'=>$date)); } elseif(isset($date)) return $date; elseif(isset($time)) return $time; } /** * Parses the datetime format pattern. * @param string $pattern the pattern to be parsed * @return array tokenized parsing result */ protected function parseFormat($pattern) { static $formats=array(); // cache if(isset($formats[$pattern])) return $formats[$pattern]; $tokens=array(); $n=strlen($pattern); $isLiteral=false; $literal=''; for($i=0;$i<$n;++$i) { $c=$pattern[$i]; if($c==="'") { if($i<$n-1 && $pattern[$i+1]==="'") { $tokens[]="'"; $i++; } elseif($isLiteral) { $tokens[]=$literal; $literal=''; $isLiteral=false; } else { $isLiteral=true; $literal=''; } } elseif($isLiteral) $literal.=$c; else { for($j=$i+1;$j<$n;++$j) { if($pattern[$j]!==$c) break; } $p=str_repeat($c,$j-$i); if(isset(self::$_formatters[$c])) $tokens[]=array(self::$_formatters[$c],$p); else $tokens[]=$p; $i=$j-1; } } if($literal!=='') $tokens[]=$literal; return $formats[$pattern]=$tokens; } /** * Get the year. * "yy" will return the last two digits of year. * "y...y" will pad the year with 0 in the front, e.g. "yyyyy" will generate "02008" for year 2008. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @return string formatted year */ protected function formatYear($pattern,$date) { $year=$date['year']; if($pattern==='yy') return str_pad($year%100,2,'0',STR_PAD_LEFT); else return str_pad($year,strlen($pattern),'0',STR_PAD_LEFT); } /** * Get the month. * "M" will return integer 1 through 12; * "MM" will return two digits month number with necessary zero padding, e.g. 05; * "MMM" will return the abrreviated month name, e.g. "Jan"; * "MMMM" will return the full month name, e.g. "January"; * "MMMMM" will return the narrow month name, e.g. "J"; * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "month" pattern is unknown * @return string month name */ protected function formatMonth($pattern,$date) { $month=$date['mon']; switch($pattern) { case 'M': return $month; case 'MM': return str_pad($month,2,'0',STR_PAD_LEFT); case 'MMM': return $this->_locale->getMonthName($month,'abbreviated'); case 'MMMM': return $this->_locale->getMonthName($month,'wide'); case 'MMMMM': return $this->_locale->getMonthName($month,'narrow'); case 'L': return $month; case 'LL': return str_pad($month,2,'0',STR_PAD_LEFT); case 'LLL': return $this->_locale->getMonthName($month,'abbreviated', true); case 'LLLL': return $this->_locale->getMonthName($month,'wide', true); case 'LLLLL': return $this->_locale->getMonthName($month,'narrow', true); default: throw new CException(Yii::t('yii','The pattern for month must be "M", "MM", "MMM", "MMMM", "L", "LL", "LLL" or "LLLL".')); } } /** * Get the day of the month. * "d" for non-padding, "dd" will always return 2 digits day numbers, e.g. 05. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "day" pattern is unknown * @return string day of the month */ protected function formatDay($pattern,$date) { $day=$date['mday']; if($pattern==='d') return $day; elseif($pattern==='dd') return str_pad($day,2,'0',STR_PAD_LEFT); else throw new CException(Yii::t('yii','The pattern for day of the month must be "d" or "dd".')); } /** * Get the day in the year, e.g. [1-366] * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException is "dayInYear" pattern is unknown * @return integer hours in AM/PM format. */ protected function formatDayInYear($pattern,$date) { $day=$date['yday']; if(($n=strlen($pattern))<=3) return str_pad($day,$n,'0',STR_PAD_LEFT); else throw new CException(Yii::t('yii','The pattern for day in year must be "D", "DD" or "DDD".')); } /** * Get day of week in the month, e.g. 2nd Wed in July. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "dayInMonth" pattern is unknown * @return integer day in month * @see http://www.unicode.org/reports/tr35/#Date_Format_Patterns */ protected function formatDayInMonth($pattern,$date) { if($pattern==='F') return (int)(($date['mday']+6)/7); else throw new CException(Yii::t('yii','The pattern for day in month must be "F".')); } /** * Get the day of the week. * "E", "EE", "EEE" will return abbreviated week day name, e.g. "Tues"; * "EEEE" will return full week day name; * "EEEEE" will return the narrow week day name, e.g. "T"; * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "dayInWeek" pattern is unknown * @return string day of the week. * @see http://www.unicode.org/reports/tr35/#Date_Format_Patterns */ protected function formatDayInWeek($pattern,$date) { $day=$date['wday']; switch($pattern) { case 'E': case 'EE': case 'EEE': case 'eee': return $this->_locale->getWeekDayName($day,'abbreviated'); case 'EEEE': case 'eeee': return $this->_locale->getWeekDayName($day,'wide'); case 'EEEEE': case 'eeeee': return $this->_locale->getWeekDayName($day,'narrow'); case 'e': case 'ee': case 'c': return $day ? $day : 7; case 'ccc': return $this->_locale->getWeekDayName($day,'abbreviated',true); case 'cccc': return $this->_locale->getWeekDayName($day,'wide',true); case 'ccccc': return $this->_locale->getWeekDayName($day,'narrow',true); default: 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".')); } } /** * Get the AM/PM designator, 12 noon is PM, 12 midnight is AM. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "period" pattern is unknown * @return string AM or PM designator */ protected function formatPeriod($pattern,$date) { if($pattern==='a') { if(intval($date['hours']/12)) return $this->_locale->getPMName(); else return $this->_locale->getAMName(); } else throw new CException(Yii::t('yii','The pattern for AM/PM marker must be "a".')); } /** * Get the hours in 24 hour format, i.e. [0-23]. * "H" for non-padding, "HH" will always return 2 characters. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "hour24" pattern is unknown * @return string hours in 24 hour format. */ protected function formatHour24($pattern,$date) { $hour=$date['hours']; if($pattern==='H') return $hour; elseif($pattern==='HH') return str_pad($hour,2,'0',STR_PAD_LEFT); else throw new CException(Yii::t('yii','The pattern for 24 hour format must be "H" or "HH".')); } /** * Get the hours in 12 hour format, i.e., [1-12] * "h" for non-padding, "hh" will always return 2 characters. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "hour12" pattern is unknown * @return string hours in 12 hour format. */ protected function formatHour12($pattern,$date) { $hour=$date['hours']; $hour=($hour==12|$hour==0)?12:($hour)%12; if($pattern==='h') return $hour; elseif($pattern==='hh') return str_pad($hour,2,'0',STR_PAD_LEFT); else throw new CException(Yii::t('yii','The pattern for 12 hour format must be "h" or "hh".')); } /** * Get the hours [1-24]. * 'k' for non-padding, and 'kk' with 2 characters padding. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "hourInDay" pattern is unknown * @return integer hours [1-24] */ protected function formatHourInDay($pattern,$date) { $hour=$date['hours']==0?24:$date['hours']; if($pattern==='k') return $hour; elseif($pattern==='kk') return str_pad($hour,2,'0',STR_PAD_LEFT); else throw new CException(Yii::t('yii','The pattern for hour in day must be "k" or "kk".')); } /** * Get the hours in AM/PM format, e.g [0-11] * "K" for non-padding, "KK" will always return 2 characters. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "hourInPeriod" pattern is unknown * @return integer hours in AM/PM format. */ protected function formatHourInPeriod($pattern,$date) { $hour=$date['hours']%12; if($pattern==='K') return $hour; elseif($pattern==='KK') return str_pad($hour,2,'0',STR_PAD_LEFT); else throw new CException(Yii::t('yii','The pattern for hour in AM/PM must be "K" or "KK".')); } /** * Get the minutes. * "m" for non-padding, "mm" will always return 2 characters. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "minutes" pattern is unknown * @return string minutes. */ protected function formatMinutes($pattern,$date) { $minutes=$date['minutes']; if($pattern==='m') return $minutes; elseif($pattern==='mm') return str_pad($minutes,2,'0',STR_PAD_LEFT); else throw new CException(Yii::t('yii','The pattern for minutes must be "m" or "mm".')); } /** * Get the seconds. * "s" for non-padding, "ss" will always return 2 characters. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "seconds" pattern is unknown * @return string seconds */ protected function formatSeconds($pattern,$date) { $seconds=$date['seconds']; if($pattern==='s') return $seconds; elseif($pattern==='ss') return str_pad($seconds,2,'0',STR_PAD_LEFT); else throw new CException(Yii::t('yii','The pattern for seconds must be "s" or "ss".')); } /** * Get the week in the year. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "weekInYear" pattern is unknown * @return integer week in year */ protected function formatWeekInYear($pattern,$date) { if($pattern==='w') return @date('W',@mktime(0,0,0,$date['mon'],$date['mday'],$date['year'])); else throw new CException(Yii::t('yii','The pattern for week in year must be "w".')); } /** * Get week in the month. * @param array $pattern result of {@link CTimestamp::getdate}. * @param string $date a pattern. * @throws CException if "weekInMonth" pattern is unknown * @return integer week in month */ protected function formatWeekInMonth($pattern,$date) { if($pattern==='W') { $weekDay=date('N',mktime(0,0,0,$date['mon'],1,$date['year'])); return floor(($weekDay+$date['mday']-2)/7)+1; } else throw new CException(Yii::t('yii','The pattern for week in month must be "W".')); } /** * Get the timezone of the server machine. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "timeZone" pattern is unknown * @return string time zone * @todo How to get the timezone for a different region? */ protected function formatTimeZone($pattern,$date) { if($pattern[0]==='z' || $pattern[0]==='v') return @date('T', @mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year'])); elseif($pattern[0]==='Z') return @date('O', @mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year'])); else throw new CException(Yii::t('yii','The pattern for time zone must be "z" or "v".')); } /** * Get the era. i.e. in gregorian, year > 0 is AD, else BC. * @param string $pattern a pattern. * @param array $date result of {@link CTimestamp::getdate}. * @throws CException if "era" pattern is unknown * @return string era * @todo How to support multiple Eras?, e.g. Japanese. */ protected function formatEra($pattern,$date) { $era=$date['year']>0 ? 1 : 0; switch($pattern) { case 'G': case 'GG': case 'GGG': return $this->_locale->getEraName($era,'abbreviated'); case 'GGGG': return $this->_locale->getEraName($era,'wide'); case 'GGGGG': return $this->_locale->getEraName($era,'narrow'); default: throw new CException(Yii::t('yii','The pattern for era must be "G", "GG", "GGG", "GGGG" or "GGGGG".')); } } }