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