CDateTimeParser.php 11 KB

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