1: <?php
2: 3:
4: class Horde_Date_Parser_Locale_Base
5: {
6: public $definitions = array();
7: public $args = array();
8: public $now;
9:
10: public function __construct($args)
11: {
12: $this->args = $args;
13: }
14:
15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56:
57: public function parse($text, $specifiedOptions = array())
58: {
59:
60: $defaultOptions = array(
61: 'context' => 'future',
62: 'now' => new Horde_Date(time()),
63: 'return' => 'date',
64: 'ambiguousTimeRange' => 6,
65: );
66: $options = array_merge($defaultOptions, $this->args, $specifiedOptions);
67:
68:
69: foreach (array_keys($specifiedOptions) as $key) {
70: if (!isset($defaultOptions[$key])) {
71: throw new InvalidArgumentException("$key is not a valid option key");
72: }
73: }
74:
75: if (!in_array($options['context'], array('past', 'future', 'none'))) {
76: throw new InvalidArgumentException("Invalid value " . $options['context'] . " for 'context' specified. Valid values are 'past', 'future', and 'none'");
77: }
78:
79:
80: $this->now = $options['now'];
81:
82:
83: $text = $this->preNormalize($text);
84:
85:
86: $tokens = $this->preTokenize($text);
87:
88:
89: foreach (array('Repeater') as $tokenizer) {
90: $tokenizer = $this->componentFactory($tokenizer);
91: $tokens = $tokenizer->scan($tokens, $options);
92: }
93:
94: foreach (array('Grabber', 'Pointer', 'Scalar', 'Ordinal', 'Separator', 'Timezone') as $tokenizer) {
95: $tokenizer = $this->componentFactory($tokenizer);
96: $tokens = $tokenizer->scan($tokens);
97: }
98:
99:
100: $taggedTokens = array_values(array_filter($tokens, create_function('$t', 'return $t->tagged();')));
101:
102:
103:
104: $taggedTokens = $this->postTokenize($taggedTokens);
105:
106:
107: $span = $this->tokensToSpan($taggedTokens, $options);
108:
109:
110: $result = new Horde_Date_Parser_Result($span, $tokens);
111: switch ($options['return']) {
112: case 'result':
113: return $result;
114: case 'span':
115: return $result->span;
116: case 'date':
117: return $result->guess();
118: }
119: }
120:
121: public function componentFactory($component, $args = null)
122: {
123: $locale = isset($this->args['locale']) ? $this->args['locale'] : null;
124: if ($locale && strtolower($locale) != 'base') {
125: $locale = str_replace(' ', '_', ucwords(str_replace('_', ' ', strtolower($locale))));
126: $class = 'Horde_Date_Parser_Locale_' . $locale . '_' . $component;
127: if (class_exists($class)) {
128: return new $class($args);
129: }
130:
131: $language = array_shift(explode('_', $locale));
132: if ($language != $locale) {
133: $class = 'Horde_Date_Parser_Locale_' . $language . '_' . $component;
134: if (class_exists($class)) {
135: return new $class($args);
136: }
137: }
138: }
139:
140: $class = 'Horde_Date_Parser_Locale_Base_' . $component;
141: return new $class($args);
142: }
143:
144: 145: 146: 147: 148: 149:
150: public function preNormalize($text)
151: {
152: $text = strtolower($text);
153: $text = $this->numericizeNumbers($text);
154: $text = preg_replace('/[\'"\.]/', '', $text);
155: $text = preg_replace('/([\/\-\,\@])/', ' \1 ', $text);
156: $text = preg_replace('/\btoday\b/', 'this day', $text);
157: $text = preg_replace('/\btomm?orr?ow\b/', 'next day', $text);
158: $text = preg_replace('/\byesterday\b/', 'last day', $text);
159: $text = preg_replace('/\bnoon\b/', '12:00', $text);
160: $text = preg_replace('/\bmidnight\b/', '24:00', $text);
161: $text = preg_replace('/\bbefore now\b/', 'past', $text);
162: $text = preg_replace('/\bnow\b/', 'this second', $text);
163: $text = preg_replace('/\b(ago|before)\b/', 'past', $text);
164: $text = preg_replace('/\bthis past\b/', 'last', $text);
165: $text = preg_replace('/\bthis last\b/', 'last', $text);
166: $text = preg_replace('/\b(?:in|during) the (morning)\b/', '\1', $text);
167: $text = preg_replace('/\b(?:in the|during the|at) (afternoon|evening|night)\b/', '\1', $text);
168: $text = preg_replace('/\btonight\b/', 'this night', $text);
169: $text = preg_replace('/(?=\w)([ap]m|oclock)\b/', ' \1', $text);
170: $text = preg_replace('/\b(hence|after|from)\b/', 'future', $text);;
171: $text = $this->numericizeOrdinals($text);
172:
173: return $text;
174: }
175:
176: 177: 178:
179: public function numericizeNumbers($text)
180: {
181: return Horde_Support_Numerizer::numerize($text, $this->args);
182: }
183:
184: 185: 186:
187: public function numericizeOrdinals($text)
188: {
189: return $text;
190: }
191:
192: 193: 194: 195: 196: 197: 198:
199: public function preTokenize($text)
200: {
201: return array_map(create_function('$w', 'return new Horde_Date_Parser_Token($w);'), preg_split('/\s+/', $text));
202: }
203:
204: 205: 206: 207: 208: 209: 210:
211: public function postTokenize($tokens)
212: {
213: if (!count($tokens)) {
214: return $tokens;
215: }
216:
217:
218:
219: $first = clone($tokens[0]);
220: $first->untag('separator_at');
221: $first->untag('separator_comma');
222: $first->untag('separator_in');
223: $first->untag('separator_slash_or_dash');
224: if (!$first->tagged()) {
225: array_shift($tokens);
226: }
227:
228:
229:
230: $last = clone($tokens[count($tokens) - 1]);
231: $last->untag('separator_at');
232: $last->untag('separator_comma');
233: $last->untag('separator_in');
234: $last->untag('separator_slash_or_dash');
235: if (!$last->tagged()) {
236: array_pop($tokens);
237: }
238:
239: return $tokens;
240: }
241:
242: public function initDefinitions()
243: {
244: if ($this->definitions) {
245: return;
246: }
247:
248: $this->definitions = array(
249: 'time' => array(
250: new Horde_Date_Parser_Handler(array(':repeater_time', ':repeater_day_portion?'), null),
251: ),
252:
253: 'date' => array(
254: new Horde_Date_Parser_Handler(array(':repeater_day_name', ':repeater_month_name', ':scalar_day', ':repeater_time', ':timezone', ':scalar_year'), 'handle_rdn_rmn_sd_t_tz_sy'),
255: new Horde_Date_Parser_Handler(array(':repeater_month_name', ':scalar_day', ':scalar_year'), 'handle_rmn_sd_sy'),
256: new Horde_Date_Parser_Handler(array(':repeater_month_name', ':scalar_day', ':scalar_year', ':separator_at?', 'time?'), 'handle_rmn_sd_sy'),
257: new Horde_Date_Parser_Handler(array(':repeater_month_name', ':scalar_day', ':separator_at?', 'time?'), 'handle_rmn_sd'),
258: new Horde_Date_Parser_Handler(array(':repeater_month_name', ':ordinal_day', ':separator_at?', 'time?'), 'handle_rmn_od'),
259: new Horde_Date_Parser_Handler(array(':repeater_month_name', ':scalar_year'), 'handle_rmn_sy'),
260: new Horde_Date_Parser_Handler(array(':scalar_day', ':repeater_month_name', ':scalar_year', ':separator_at?', 'time?'), 'handle_sd_rmn_sy'),
261: new Horde_Date_Parser_Handler(array(':scalar_month', ':separator_slash_or_dash', ':scalar_day', ':separator_slash_or_dash', ':scalar_year', ':separator_at?', 'time?'), 'handle_sm_sd_sy'),
262: new Horde_Date_Parser_Handler(array(':scalar_day', ':separator_slash_or_dash', ':scalar_month', ':separator_slash_or_dash', ':scalar_year', ':separator_at?', 'time?'), 'handle_sd_sm_sy'),
263: new Horde_Date_Parser_Handler(array(':scalar_year', ':separator_slash_or_dash', ':scalar_month', ':separator_slash_or_dash', ':scalar_day', ':separator_at?', 'time?'), 'handle_sy_sm_sd'),
264: new Horde_Date_Parser_Handler(array(':scalar_month', ':separator_slash_or_dash', ':scalar_year'), 'handle_sm_sy'),
265: ),
266:
267:
268: 'anchor' => array(
269: new Horde_Date_Parser_Handler(array(':grabber?', ':repeater', ':separator_at?', ':repeater?', ':repeater?'), 'handle_r'),
270: new Horde_Date_Parser_Handler(array(':grabber?', ':repeater', ':repeater', ':separator_at?', ':repeater?', ':repeater?'), 'handle_r'),
271: new Horde_Date_Parser_Handler(array(':repeater', ':grabber', ':repeater'), 'handle_r_g_r'),
272: ),
273:
274:
275: 'arrow' => array(
276: new Horde_Date_Parser_Handler(array(':scalar', ':repeater', ':pointer'), 'handle_s_r_p'),
277: new Horde_Date_Parser_Handler(array(':pointer', ':scalar', ':repeater'), 'handle_p_s_r'),
278: new Horde_Date_Parser_Handler(array(':scalar', ':repeater', ':pointer', 'anchor'), 'handle_s_r_p_a'),
279: ),
280:
281:
282: 'narrow' => array(
283: new Horde_Date_Parser_Handler(array(':ordinal', ':repeater', ':separator_in', ':repeater'), 'handle_o_r_s_r'),
284: new Horde_Date_Parser_Handler(array(':ordinal', ':repeater', ':grabber', ':repeater'), 'handle_o_r_g_r'),
285: ),
286: );
287: }
288:
289: public function tokensToSpan($tokens, $options)
290: {
291: $this->initDefinitions();
292:
293:
294: foreach ($this->definitions['date'] as $handler) {
295: if ($handler->match($tokens, $this->definitions)) {
296: $goodTokens = array_values(array_filter($tokens, create_function('$o', 'return !$o->getTag("separator");')));
297: $this->debug($handler->handlerMethod, $goodTokens, $options);
298: return call_user_func(array($this, $handler->handlerMethod), $goodTokens, $options);
299: }
300: }
301:
302:
303: foreach ($this->definitions['anchor'] as $handler) {
304: if ($handler->match($tokens, $this->definitions)) {
305: $goodTokens = array_values(array_filter($tokens, create_function('$o', 'return !$o->getTag("separator");')));
306: $this->debug($handler->handlerMethod, $goodTokens, $options);
307: return call_user_func(array($this, $handler->handlerMethod), $goodTokens, $options);
308: }
309: }
310:
311:
312: foreach ($this->definitions['arrow'] as $handler) {
313: if ($handler->match($tokens, $this->definitions)) {
314: $goodTokens = array_values(array_filter($tokens, create_function('$o', 'return !$o->getTag("separator_at") && !$o->getTag("separator_slash_or_dash") && !$o->getTag("separator_comma");')));
315: $this->debug($handler->handlerMethod, $goodTokens, $options);
316: return call_user_func(array($this, $handler->handlerMethod), $goodTokens, $options);
317: }
318: }
319:
320:
321: foreach ($this->definitions['narrow'] as $handler) {
322: if ($handler->match($tokens, $this->definitions)) {
323:
324: $this->debug($handler->handlerMethod, $tokens, $options);
325: return call_user_func(array($this, $handler->handlerMethod), $tokens, $options);
326: }
327: }
328:
329: return null;
330: }
331:
332: public function dayOrTime($dayStart, $timeTokens, $options)
333: {
334: $outerSpan = new Horde_Date_Span($dayStart, $dayStart->add(array('day' => 1)));
335:
336: if (!empty($timeTokens)) {
337: $this->now = $outerSpan->begin;
338: return $this->getAnchor($this->dealiasAndDisambiguateTimes($timeTokens, $options), $options);
339: } else {
340: return $outerSpan;
341: }
342: }
343:
344:
345: public function handle_m_d($month, $day, $timeTokens, $options)
346: {
347: $month->now = $this->now;
348: $span = $month->this($options['context']);
349:
350: $dayStart = new Horde_Date($span->begin->year, $span->begin->month, $day);
351: return $this->dayOrTime($dayStart, $timeTokens, $options);
352: }
353:
354: public function handle_rmn_sd($tokens, $options)
355: {
356: return $this->handle_m_d($tokens[0]->getTag('repeater_month_name'), $tokens[1]->getTag('scalar_day'), array_slice($tokens, 2), $options);
357: }
358:
359: public function handle_rmn_od($tokens, $options)
360: {
361: return $this->handle_m_d($tokens[0]->getTag('repeater_month_name'), $tokens[1]->getTag('ordinal_day'), array_slice($tokens, 2), $options);
362: }
363:
364: public function handle_rmn_sy($tokens, $options)
365: {
366: $month = $tokens[0]->getTag('repeater_month_name')->index();
367: $year = $tokens[1]->getTag('scalar_year');
368:
369: try {
370: return new Horde_Date_Span(new Horde_Date($year, $month, 1), new Horde_Date($year, $month + 1, 1));
371: } catch (Exception $e) {
372: return null;
373: }
374: }
375:
376: public function handle_rdn_rmn_sd_t_tz_sy($tokens, $options)
377: {
378: $month = $tokens[1]->getTag('repeater_month_name')->index();
379: $day = $tokens[2]->getTag('scalar_day');
380: $year = $tokens[5]->getTag('scalar_year');
381:
382: try {
383: $dayStart = new Horde_Date($year, $month, $day);
384: return $this->dayOrTime($dayStart, array($tokens[3]), $options);
385: } catch (Exception $e) {
386: return null;
387: }
388: }
389:
390: public function handle_rmn_sd_sy($tokens, $options)
391: {
392: $month = $tokens[0]->getTag('repeater_month_name')->index();
393: $day = $tokens[1]->getTag('scalar_day');
394: $year = $tokens[2]->getTag('scalar_year');
395:
396: $timeTokens = array_slice($tokens, 3);
397:
398: try {
399: $dayStart = new Horde_Date($year, $month, $day);
400: return $this->dayOrTime($dayStart, $timeTokens, $options);
401: } catch (Exception $e) {
402: return null;
403: }
404: }
405:
406: public function handle_sd_rmn_sy($tokens, $options)
407: {
408: $newTokens = array($tokens[1], $tokens[0], $tokens[2]);
409: $timeTokens = array_slice($tokens, 3);
410: return $this->handle_rmn_sd_sy(array_merge($newTokens, $timeTokens), $options);
411: }
412:
413: public function handle_sm_sd_sy($tokens, $options)
414: {
415: $month = $tokens[0]->getTag('scalar_month');
416: $day = $tokens[1]->getTag('scalar_day');
417: $year = $tokens[2]->getTag('scalar_year');
418:
419: $timeTokens = array_slice($tokens, 3);
420:
421: try {
422: $dayStart = new Horde_Date($year, $month, $day);
423: return $this->dayOrTime($dayStart, $timeTokens, $options);
424: } catch (Exception $e) {
425: return null;
426: }
427: }
428:
429: public function handle_sd_sm_sy($tokens, $options)
430: {
431: $newTokens = array($tokens[1], $tokens[0], $tokens[2]);
432: $timeTokens = array_slice($tokens, 3);
433: return $this->handle_sm_sd_sy(array_merge($newTokens, $timeTokens), $options);
434: }
435:
436: public function handle_sy_sm_sd($tokens, $options)
437: {
438: $newTokens = array($tokens[1], $tokens[2], $tokens[0]);
439: $timeTokens = array_slice($tokens, 3);
440: return $this->handle_sm_sd_sy(array_merge($newTokens, $timeTokens), $options);
441: }
442:
443: public function handle_sm_sy($tokens, $options)
444: {
445: $month = $tokens[0]->getTag('scalar_month');
446: $year = $tokens[1]->getTag('scalar_year');
447:
448: try {
449: return new Horde_Date_Span(new Horde_Date($year, $month, 1), new Horde_Date($year, $month + 1, 1));
450: } catch (Exception $e) {
451: return null;
452: }
453: }
454:
455:
456: 457: 458:
459:
460: public function handle_r($tokens, $options)
461: {
462: $ddTokens = $this->dealiasAndDisambiguateTimes($tokens, $options);
463: return $this->getAnchor($ddTokens, $options);
464: }
465:
466: public function handle_r_g_r($tokens, $options)
467: {
468: $newTokens = array($tokens[1], $tokens[0], $tokens[2]);
469: return $this->handle_r($newTokens, $options);
470: }
471:
472:
473: 474: 475:
476:
477: public function handle_srp($tokens, $span, $options)
478: {
479: $distance = $tokens[0]->getTag('scalar');
480: $repeater = $tokens[1]->getTag('repeater');
481: $pointer = $tokens[2]->getTag('pointer');
482:
483: return $repeater->offset($span, $distance, $pointer);
484: }
485:
486: public function handle_s_r_p($tokens, $options)
487: {
488: $span = new Horde_Date_Span($this->now, $this->now->add(1));
489: return $this->handle_srp($tokens, $span, $options);
490: }
491:
492: public function handle_p_s_r($tokens, $options)
493: {
494: $newTokens = array($tokens[1], $tokens[2], $tokens[0]);
495: return $this->handle_s_r_p($newTokens, $options);
496: }
497:
498: public function handle_s_r_p_a($tokens, $options)
499: {
500: $anchorSpan = $this->getAnchor(array_slice($tokens, 3), $options);
501: return $this->handle_srp($tokens, $anchorSpan, $options);
502: }
503:
504:
505: 506: 507:
508:
509: public function handle_orr($tokens, $outerSpan, $options)
510: {
511: $repeater = $tokens[1]->getTag('repeater');
512: $repeater->now = $outerSpan->begin->sub(1);
513: $ordinal = $tokens[0]->getTag('ordinal');
514: $span = null;
515:
516: for ($i = 0; $i < $ordinal; $i++) {
517: $span = $repeater->next('future');
518: if ($span->begin->after($outerSpan->end)) {
519: $span = null;
520: break;
521: }
522: }
523: return $span;
524: }
525:
526: public function handle_o_r_s_r($tokens, $options)
527: {
528: $outerSpan = $this->getAnchor(array($tokens[3]), $options);
529: return $this->handle_orr(array($tokens[0], $tokens[1]), $outerSpan, $options);
530: }
531:
532: public function handle_o_r_g_r($tokens, $options)
533: {
534: $outerSpan = $this->getAnchor(array($tokens[2], $tokens[3]), $options);
535: return $this->handle_orr(array($tokens[0], $tokens[1]), $outerSpan, $options);
536: }
537:
538:
539: 540: 541:
542:
543: public function debug($method, $args)
544: {
545: $args = func_get_args();
546: $method = array_shift($args);
547:
548: }
549:
550:
551: 552: 553:
554:
555: public function getAnchor($tokens, $options)
556: {
557: $grabber = 'this';
558: $pointer = 'future';
559:
560: $repeaters = $this->getRepeaters($tokens);
561: for ($i = 0, $size = count($repeaters); $i < $size; $i++) {
562: array_pop($tokens);
563: }
564:
565: if (count($tokens) && $tokens[0]->getTag('grabber')) {
566: $grabber = $tokens[0]->getTag('grabber');
567: array_pop($tokens);
568: }
569:
570: $head = array_shift($repeaters);
571: $head->now = $this->now;
572:
573: switch ($grabber) {
574: case 'last':
575: $outerSpan = $head->next('past');
576: break;
577:
578: case 'this':
579: if (count($repeaters)) {
580: $outerSpan = $head->this('none');
581: } else {
582: $outerSpan = $head->this($options['context']);
583: }
584: break;
585:
586: case 'next':
587: $outerSpan = $head->next('future');
588: break;
589:
590: default:
591: throw new Horde_Date_Parser_Exception('Invalid grabber ' . $grabber);
592: }
593:
594: return $this->findWithin($repeaters, $outerSpan, $pointer);
595: }
596:
597: public function getRepeaters($tokens)
598: {
599: $repeaters = array();
600: foreach ($tokens as $token) {
601: if ($t = $token->getTag('repeater')) {
602: $repeaters[] = $t;
603: }
604: }
605:
606:
607: usort($repeaters, create_function('$a, $b', 'return $b->width() > $a->width();'));
608: return $repeaters;
609: }
610:
611: 612: 613: 614: 615:
616: public function findWithin($tags, $span, $pointer)
617: {
618: if (empty($tags)) { return $span; }
619:
620: $head = array_shift($tags);
621: $rest = $tags;
622: $head->now = ($pointer == 'future') ? $span->begin : $span->end;
623: $h = $head->this('none');
624:
625: if ($span->includes($h->begin) || $span->includes($h->end)) {
626: return $this->findWithin($rest, $h, $pointer);
627: } else {
628: return null;
629: }
630: }
631:
632: 633: 634: 635: 636:
637: public function dealiasAndDisambiguateTimes($tokens, $options)
638: {
639: $dayPortionIndex = null;
640: foreach ($tokens as $i => $t) {
641: if ($t->getTag('repeater_day_portion')) {
642: $dayPortionIndex = $i;
643: break;
644: }
645: }
646:
647: $timeIndex = null;
648: foreach ($tokens as $i => $t) {
649: if ($t->getTag('repeater_time')) {
650: $timeIndex = $i;
651: break;
652: }
653: }
654:
655: if ($dayPortionIndex !== null && $timeIndex !== null) {
656: $t1 = $tokens[$dayPortionIndex];
657: $t1tag = $t1->getTag('repeater_day_portion');
658:
659: if ($t1tag->type == 'morning') {
660: $t1->untag('repeater_day_portion');
661: $t1->tag('repeater_day_portion', new Horde_Date_Repeater_DayPortion('am'));
662: } elseif (in_array($t1tag->type, array('afternoon', 'evening', 'night'))) {
663: $t1->untag('repeater_day_portion');
664: $t1->tag('repeater_day_portion', new Horde_Date_Repeater_DayPortion('pm'));
665: }
666: }
667:
668:
669: if (!isset($options['ambiguousTimeRange']) || $options['ambiguousTimeRange'] != 'none') {
670: $ttokens = array();
671: foreach ($tokens as $i => $t0) {
672: $ttokens[] = $t0;
673: $t1 = isset($tokens[$i + 1]) ? $tokens[$i + 1] : null;
674: if ($t0->getTag('repeater_time') && $t0->getTag('repeater_time')->ambiguous && (!$t1 || !$t1->getTag('repeater_day_portion'))) {
675: $distoken = new Horde_Date_Parser_Token('disambiguator');
676: $distoken->tag('repeater_day_portion', new Horde_Date_Repeater_DayPortion($options['ambiguousTimeRange']));
677: $ttokens[] = $distoken;
678: }
679: }
680:
681: $tokens = $ttokens;
682: }
683:
684: return $tokens;
685: }
686: }
687: