CDateTimeParser.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <?php
  2. /**
  3. * CDateTimeParser class file
  4. *
  5. * @author Wei Zhuo <weizhuo[at]gamil[dot]com>
  6. * @author Qiang Xue <qiang.xue@gmail.com>
  7. * @author Tomasz Suchanek <tomasz[dot]suchanek[at]gmail[dot]com>
  8. * @link http://www.yiiframework.com/
  9. * @copyright 2008-2013 Yii Software LLC
  10. * @license http://www.yiiframework.com/license/
  11. */
  12. /**
  13. * CDateTimeParser converts a date/time string to a UNIX timestamp according to the specified pattern.
  14. *
  15. * The following pattern characters are recognized:
  16. * <pre>
  17. * Pattern | Description
  18. * ----------------------------------------------------
  19. * d | Day of month 1 to 31, no padding
  20. * dd | Day of month 01 to 31, zero leading
  21. * M | Month digit 1 to 12, no padding
  22. * MM | Month digit 01 to 12, zero leading
  23. * MMM | Abbreviation representation of month (available since 1.1.11; locale aware since 1.1.13)
  24. * MMMM | Full name representation (available since 1.1.13; locale aware)
  25. * yy | 2 year digit, e.g., 96, 05
  26. * yyyy | 4 year digit, e.g., 2005
  27. * h | Hour in 0 to 23, no padding
  28. * hh | Hour in 00 to 23, zero leading
  29. * H | Hour in 0 to 23, no padding
  30. * HH | Hour in 00 to 23, zero leading
  31. * m | Minutes in 0 to 59, no padding
  32. * mm | Minutes in 00 to 59, zero leading
  33. * s | Seconds in 0 to 59, no padding
  34. * ss | Seconds in 00 to 59, zero leading
  35. * a | AM or PM, case-insensitive (since version 1.1.5)
  36. * ? | matches any character (wildcard) (since version 1.1.11)
  37. * ----------------------------------------------------
  38. * </pre>
  39. * All other characters must appear in the date string at the corresponding positions.
  40. *
  41. * For example, to parse a date string '21/10/2008', use the following:
  42. * <pre>
  43. * $timestamp=CDateTimeParser::parse('21/10/2008','dd/MM/yyyy');
  44. * </pre>
  45. *
  46. * Locale specific patterns such as MMM and MMMM uses {@link CLocale} for retrieving needed information.
  47. *
  48. * To format a timestamp to a date string, please use {@link CDateFormatter}.
  49. *
  50. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  51. * @author Qiang Xue <qiang.xue@gmail.com>
  52. * @package system.utils
  53. * @since 1.0
  54. */
  55. class CDateTimeParser
  56. {
  57. /**
  58. * @var boolean whether 'mbstring' PHP extension available. This static property introduced for
  59. * the better overall performance of the class functionality. Checking 'mbstring' availability
  60. * through static property with predefined status value is much faster than direct calling
  61. * of function_exists('...').
  62. * Intended for internal use only.
  63. * @since 1.1.13
  64. */
  65. private static $_mbstringAvailable;
  66. /**
  67. * Converts a date string to a timestamp.
  68. * @param string $value the date string to be parsed
  69. * @param string $pattern the pattern that the date string is following
  70. * @param array $defaults the default values for year, month, day, hour, minute and second.
  71. * The default values will be used in case when the pattern doesn't specify the
  72. * corresponding fields. For example, if the pattern is 'MM/dd/yyyy' and this
  73. * parameter is array('minute'=>0, 'second'=>0), then the actual minute and second
  74. * for the parsing result will take value 0, while the actual hour value will be
  75. * the current hour obtained by date('H'). This parameter has been available since version 1.1.5.
  76. * @return integer timestamp for the date string. False if parsing fails.
  77. */
  78. public static function parse($value,$pattern='MM/dd/yyyy',$defaults=array())
  79. {
  80. if(self::$_mbstringAvailable===null)
  81. self::$_mbstringAvailable=extension_loaded('mbstring');
  82. $tokens=self::tokenize($pattern);
  83. $i=0;
  84. $n=self::$_mbstringAvailable ? mb_strlen($value,Yii::app()->charset) : strlen($value);
  85. foreach($tokens as $token)
  86. {
  87. switch($token)
  88. {
  89. case 'yyyy':
  90. {
  91. if(($year=self::parseInteger($value,$i,4,4))===false)
  92. return false;
  93. $i+=4;
  94. break;
  95. }
  96. case 'yy':
  97. {
  98. if(($year=self::parseInteger($value,$i,1,2))===false)
  99. return false;
  100. $i+=strlen($year);
  101. break;
  102. }
  103. case 'MMMM':
  104. {
  105. $monthName='';
  106. if(($month=self::parseMonth($value,$i,'wide',$monthName))===false)
  107. return false;
  108. $i+=self::$_mbstringAvailable ? mb_strlen($monthName,Yii::app()->charset) : strlen($monthName);
  109. break;
  110. }
  111. case 'MMM':
  112. {
  113. $monthName='';
  114. if(($month=self::parseMonth($value,$i,'abbreviated',$monthName))===false)
  115. return false;
  116. $i+=self::$_mbstringAvailable ? mb_strlen($monthName,Yii::app()->charset) : strlen($monthName);
  117. break;
  118. }
  119. case 'MM':
  120. {
  121. if(($month=self::parseInteger($value,$i,2,2))===false)
  122. return false;
  123. $i+=2;
  124. break;
  125. }
  126. case 'M':
  127. {
  128. if(($month=self::parseInteger($value,$i,1,2))===false)
  129. return false;
  130. $i+=strlen($month);
  131. break;
  132. }
  133. case 'dd':
  134. {
  135. if(($day=self::parseInteger($value,$i,2,2))===false)
  136. return false;
  137. $i+=2;
  138. break;
  139. }
  140. case 'd':
  141. {
  142. if(($day=self::parseInteger($value,$i,1,2))===false)
  143. return false;
  144. $i+=strlen($day);
  145. break;
  146. }
  147. case 'h':
  148. case 'H':
  149. {
  150. if(($hour=self::parseInteger($value,$i,1,2))===false)
  151. return false;
  152. $i+=strlen($hour);
  153. break;
  154. }
  155. case 'hh':
  156. case 'HH':
  157. {
  158. if(($hour=self::parseInteger($value,$i,2,2))===false)
  159. return false;
  160. $i+=2;
  161. break;
  162. }
  163. case 'm':
  164. {
  165. if(($minute=self::parseInteger($value,$i,1,2))===false)
  166. return false;
  167. $i+=strlen($minute);
  168. break;
  169. }
  170. case 'mm':
  171. {
  172. if(($minute=self::parseInteger($value,$i,2,2))===false)
  173. return false;
  174. $i+=2;
  175. break;
  176. }
  177. case 's':
  178. {
  179. if(($second=self::parseInteger($value,$i,1,2))===false)
  180. return false;
  181. $i+=strlen($second);
  182. break;
  183. }
  184. case 'ss':
  185. {
  186. if(($second=self::parseInteger($value,$i,2,2))===false)
  187. return false;
  188. $i+=2;
  189. break;
  190. }
  191. case 'a':
  192. {
  193. if(($ampm=self::parseAmPm($value,$i))===false)
  194. return false;
  195. if(isset($hour))
  196. {
  197. if($hour==12 && $ampm==='am')
  198. $hour=0;
  199. elseif($hour<12 && $ampm==='pm')
  200. $hour+=12;
  201. }
  202. $i+=2;
  203. break;
  204. }
  205. default:
  206. {
  207. $tn=self::$_mbstringAvailable ? mb_strlen($token,Yii::app()->charset) : strlen($token);
  208. if($i>=$n || ($token{0}!='?' && (self::$_mbstringAvailable ? mb_substr($value,$i,$tn,Yii::app()->charset) : substr($value,$i,$tn))!==$token))
  209. return false;
  210. $i+=$tn;
  211. break;
  212. }
  213. }
  214. }
  215. if($i<$n)
  216. return false;
  217. if(!isset($year))
  218. $year=isset($defaults['year']) ? $defaults['year'] : date('Y');
  219. if(!isset($month))
  220. $month=isset($defaults['month']) ? $defaults['month'] : date('n');
  221. if(!isset($day))
  222. $day=isset($defaults['day']) ? $defaults['day'] : date('j');
  223. if(strlen($year)===2)
  224. {
  225. if($year>=70)
  226. $year+=1900;
  227. else
  228. $year+=2000;
  229. }
  230. $year=(int)$year;
  231. $month=(int)$month;
  232. $day=(int)$day;
  233. if(
  234. !isset($hour) && !isset($minute) && !isset($second)
  235. && !isset($defaults['hour']) && !isset($defaults['minute']) && !isset($defaults['second'])
  236. )
  237. $hour=$minute=$second=0;
  238. else
  239. {
  240. if(!isset($hour))
  241. $hour=isset($defaults['hour']) ? $defaults['hour'] : date('H');
  242. if(!isset($minute))
  243. $minute=isset($defaults['minute']) ? $defaults['minute'] : date('i');
  244. if(!isset($second))
  245. $second=isset($defaults['second']) ? $defaults['second'] : date('s');
  246. $hour=(int)$hour;
  247. $minute=(int)$minute;
  248. $second=(int)$second;
  249. }
  250. if(CTimestamp::isValidDate($year,$month,$day) && CTimestamp::isValidTime($hour,$minute,$second))
  251. return CTimestamp::getTimestamp($hour,$minute,$second,$month,$day,$year);
  252. else
  253. return false;
  254. }
  255. /*
  256. * @param string $pattern the pattern that the date string is following
  257. */
  258. private static function tokenize($pattern)
  259. {
  260. if(!($n=self::$_mbstringAvailable ? mb_strlen($pattern,Yii::app()->charset) : strlen($pattern)))
  261. return array();
  262. $tokens=array();
  263. $c0=self::$_mbstringAvailable ? mb_substr($pattern,0,1,Yii::app()->charset) : substr($pattern,0,1);
  264. for($start=0,$i=1;$i<$n;++$i)
  265. {
  266. $c=self::$_mbstringAvailable ? mb_substr($pattern,$i,1,Yii::app()->charset) : substr($pattern,$i,1);
  267. if($c!==$c0)
  268. {
  269. $tokens[]=self::$_mbstringAvailable ? mb_substr($pattern,$start,$i-$start,Yii::app()->charset) : substr($pattern,$start,$i-$start);
  270. $c0=$c;
  271. $start=$i;
  272. }
  273. }
  274. $tokens[]=self::$_mbstringAvailable ? mb_substr($pattern,$start,$n-$start,Yii::app()->charset) : substr($pattern,$start,$n-$start);
  275. return $tokens;
  276. }
  277. /**
  278. * @param string $value the date string to be parsed
  279. * @param integer $offset starting offset
  280. * @param integer $minLength minimum length
  281. * @param integer $maxLength maximum length
  282. * @return string parsed integer value
  283. */
  284. protected static function parseInteger($value,$offset,$minLength,$maxLength)
  285. {
  286. for($len=$maxLength;$len>=$minLength;--$len)
  287. {
  288. $v=self::$_mbstringAvailable ? mb_substr($value,$offset,$len,Yii::app()->charset) : substr($value,$offset,$len);
  289. if(ctype_digit($v) && (self::$_mbstringAvailable ? mb_strlen($v,Yii::app()->charset) : strlen($v))>=$minLength)
  290. return $v;
  291. }
  292. return false;
  293. }
  294. /**
  295. * @param string $value the date string to be parsed
  296. * @param integer $offset starting offset
  297. * @return string parsed day period value
  298. */
  299. protected static function parseAmPm($value, $offset)
  300. {
  301. $v=strtolower(self::$_mbstringAvailable ? mb_substr($value,$offset,2,Yii::app()->charset) : substr($value,$offset,2));
  302. return $v==='am' || $v==='pm' ? $v : false;
  303. }
  304. /**
  305. * @param string $value the date string to be parsed.
  306. * @param integer $offset starting offset.
  307. * @param string $width month name width. It can be 'wide', 'abbreviated' or 'narrow'.
  308. * @param string $monthName extracted month name. Passed by reference.
  309. * @return string parsed month name.
  310. * @since 1.1.13
  311. */
  312. protected static function parseMonth($value,$offset,$width,&$monthName)
  313. {
  314. $valueLength=self::$_mbstringAvailable ? mb_strlen($value,Yii::app()->charset) : strlen($value);
  315. for($len=1; $offset+$len<=$valueLength; $len++)
  316. {
  317. $monthName=self::$_mbstringAvailable ? mb_substr($value,$offset,$len,Yii::app()->charset) : substr($value,$offset,$len);
  318. if(!preg_match('/^[\p{L}\p{M}]+$/u',$monthName)) // unicode aware replacement for ctype_alpha($monthName)
  319. {
  320. $monthName=self::$_mbstringAvailable ? mb_substr($monthName,0,-1,Yii::app()->charset) : substr($monthName,0,-1);
  321. break;
  322. }
  323. }
  324. $monthName=self::$_mbstringAvailable ? mb_strtolower($monthName,Yii::app()->charset) : strtolower($monthName);
  325. $monthNames=Yii::app()->getLocale()->getMonthNames($width,false);
  326. foreach($monthNames as $k=>$v)
  327. $monthNames[$k]=rtrim(self::$_mbstringAvailable ? mb_strtolower($v,Yii::app()->charset) : strtolower($v),'.');
  328. $monthNamesStandAlone=Yii::app()->getLocale()->getMonthNames($width,true);
  329. foreach($monthNamesStandAlone as $k=>$v)
  330. $monthNamesStandAlone[$k]=rtrim(self::$_mbstringAvailable ? mb_strtolower($v,Yii::app()->charset) : strtolower($v),'.');
  331. if(($v=array_search($monthName,$monthNames))===false && ($v=array_search($monthName,$monthNamesStandAlone))===false)
  332. return false;
  333. return $v;
  334. }
  335. }