cd8bc3543513d147b030c5303424964aaa49b5b9
[python_utils.git] / dateparse / dateparse_utils.py
1 #!/usr/bin/env python3
2
3 import datetime
4 import functools
5 import holidays  # type: ignore
6 import logging
7 import re
8 import sys
9 from typing import Any, Callable, Dict, Optional
10
11 import antlr4  # type: ignore
12 import dateutil.easter
13 import dateutil.tz
14 import pytz
15
16 import acl
17 import bootstrap
18 from datetime_utils import (
19     TimeUnit, n_timeunits_from_base, datetime_to_date, date_to_datetime
20 )
21 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer  # type: ignore
22 from dateparse.dateparse_utilsListener import dateparse_utilsListener  # type: ignore
23 from dateparse.dateparse_utilsParser import dateparse_utilsParser  # type: ignore
24 import decorator_utils
25
26
27 logger = logging.getLogger(__name__)
28
29
30 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
31     @functools.wraps(enter_or_exit_f)
32     def debug_parse_wrapper(*args, **kwargs):
33         slf = args[0]
34         ctx = args[1]
35         depth = ctx.depth()
36         logger.debug(
37             '  ' * (depth-1) +
38             f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
39         )
40         for c in ctx.getChildren():
41             logger.debug(
42                 '  ' * (depth-1) +
43                 f'{c} {type(c)}'
44             )
45         retval = enter_or_exit_f(*args, **kwargs)
46         return retval
47     return debug_parse_wrapper
48
49
50 class ParseException(Exception):
51     """An exception thrown during parsing because of unrecognized input."""
52     def __init__(self, message: str) -> None:
53         logger.error(message)
54         self.message = message
55
56
57 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
58     """An error listener that raises ParseExceptions."""
59     def syntaxError(
60             self, recognizer, offendingSymbol, line, column, msg, e
61     ):
62         logger.error(msg)
63         raise ParseException(msg)
64
65     def reportAmbiguity(
66             self, recognizer, dfa, startIndex, stopIndex, exact,
67             ambigAlts, configs
68     ):
69         pass
70
71     def reportAttemptingFullContext(
72             self, recognizer, dfa, startIndex, stopIndex, conflictingAlts,
73             configs
74     ):
75         pass
76
77     def reportContextSensitivity(
78             self, recognizer, dfa, startIndex, stopIndex, prediction, configs
79     ):
80         pass
81
82
83 @decorator_utils.decorate_matching_methods_with(
84     debug_parse,
85     acl=acl.StringWildcardBasedACL(
86         allowed_patterns=[
87             'enter*',
88             'exit*',
89         ],
90         denied_patterns=[
91             'enterEveryRule',
92             'exitEveryRule'
93         ],
94         order_to_check_allow_deny=acl.Order.DENY_ALLOW,
95         default_answer=False
96     )
97 )
98 class DateParser(dateparse_utilsListener):
99     PARSE_TYPE_SINGLE_DATE_EXPR = 1
100     PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
101     PARSE_TYPE_SINGLE_TIME_EXPR = 3
102     PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
103
104     def __init__(
105             self,
106             *,
107             override_now_for_test_purposes = None
108     ) -> None:
109         """C'tor.  Passing a value to override_now_for_test_purposes can be
110         used to force this instance to use a custom date/time for its
111         idea of "now" so that the code can be more easily unittested.
112         Leave as None for real use cases.
113         """
114         self.month_name_to_number = {
115             'jan': 1,
116             'feb': 2,
117             'mar': 3,
118             'apr': 4,
119             'may': 5,
120             'jun': 6,
121             'jul': 7,
122             'aug': 8,
123             'sep': 9,
124             'oct': 10,
125             'nov': 11,
126             'dec': 12,
127         }
128
129         # Used only for ides/nones.  Month length on a non-leap year.
130         self.typical_days_per_month = {
131             1: 31,
132             2: 28,
133             3: 31,
134             4: 30,
135             5: 31,
136             6: 30,
137             7: 31,
138             8: 31,
139             9: 30,
140             10: 31,
141             11: 30,
142             12: 31
143         }
144
145         # N.B. day number is also synched with datetime_utils.TimeUnit values
146         # which allows expressions like "3 wednesdays from now" to work.
147         self.day_name_to_number = {
148             'mon': 0,
149             'tue': 1,
150             'wed': 2,
151             'thu': 3,
152             'fri': 4,
153             'sat': 5,
154             'sun': 6,
155         }
156
157         # These TimeUnits are defined in datetime_utils and are used as params
158         # to datetime_utils.n_timeunits_from_base.
159         self.time_delta_unit_to_constant = {
160             'hou': TimeUnit.HOURS,
161             'min': TimeUnit.MINUTES,
162             'sec': TimeUnit.SECONDS,
163         }
164         self.delta_unit_to_constant = {
165             'day': TimeUnit.DAYS,
166             'wor': TimeUnit.WORKDAYS,
167             'wee': TimeUnit.WEEKS,
168             'mon': TimeUnit.MONTHS,
169             'yea': TimeUnit.YEARS,
170         }
171         self.override_now_for_test_purposes = override_now_for_test_purposes
172         self._reset()
173
174     def parse(self, date_string: str) -> Optional[datetime.datetime]:
175         """Parse a date/time expression and return a timezone agnostic
176         datetime on success.  Also sets self.datetime, self.date and
177         self.time which can each be accessed other methods on the
178         class: get_datetime(), get_date() and get_time().  Raises a
179         ParseException with a helpful(?) message on parse error or
180         confusion.
181
182         To get an idea of what expressions can be parsed, check out
183         the unittest and the grammar.
184
185         Usage:
186
187         txt = '3 weeks before last tues at 9:15am'
188         dp = DateParser()
189         dt1 = dp.parse(txt)
190         dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
191
192         # dt1 and dt2 will be identical other than the fact that
193         # the latter's tzinfo will be set to PST/PDT.
194
195         This is the main entrypoint to this class for caller code.
196         """
197         self._reset()
198         listener = RaisingErrorListener()
199         input_stream = antlr4.InputStream(date_string)
200         lexer = dateparse_utilsLexer(input_stream)
201         lexer.removeErrorListeners()
202         lexer.addErrorListener(listener)
203         stream = antlr4.CommonTokenStream(lexer)
204         parser = dateparse_utilsParser(stream)
205         parser.removeErrorListeners()
206         parser.addErrorListener(listener)
207         tree = parser.parse()
208         walker = antlr4.ParseTreeWalker()
209         walker.walk(self, tree)
210         return self.datetime
211
212     def get_date(self) -> Optional[datetime.date]:
213         """Return the date part or None."""
214         return self.date
215
216     def get_time(self) -> Optional[datetime.time]:
217         """Return the time part or None."""
218         return self.time
219
220     def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
221         """Return as a datetime.  Parsed date expressions without any time
222         part return hours = minutes = seconds = microseconds = 0 (i.e. at
223         midnight that day).  Parsed time expressions without any date part
224         default to date = today.
225
226         The optional tz param allows the caller to request the datetime be
227         timezone aware and sets the tzinfo to the indicated zone.  Defaults
228         to timezone naive (i.e. tzinfo = None).
229         """
230         dt = self.datetime
231         if tz is not None:
232             dt = dt.replace(tzinfo=None).astimezone(tz=tz)
233         return dt
234
235     # -- helpers --
236
237     def _reset(self):
238         """Reset at init and between parses."""
239         if self.override_now_for_test_purposes is None:
240             self.now_datetime = datetime.datetime.now()
241             self.today = datetime.date.today()
242         else:
243             self.now_datetime = self.override_now_for_test_purposes
244             self.today = datetime_to_date(
245                 self.override_now_for_test_purposes
246             )
247         self.date: Optional[datetime.date] = None
248         self.time: Optional[datetime.time] = None
249         self.datetime: Optional[datetime.datetime] = None
250         self.context: Dict[str, Any] = {}
251         self.timedelta = datetime.timedelta(seconds=0)
252
253     @staticmethod
254     def _normalize_special_day_name(name: str) -> str:
255         """String normalization / canonicalization for date expressions."""
256         name = name.lower()
257         name = name.replace("'", '')
258         name = name.replace('xmas', 'christmas')
259         name = name.replace('mlk', 'martin luther king')
260         name = name.replace(' ', '')
261         eve = 'eve' if name[-3:] == 'eve' else ''
262         name = name[:5] + eve
263         name = name.replace('washi', 'presi')
264         return name
265
266     def _figure_out_date_unit(self, orig: str) -> TimeUnit:
267         """Figure out what unit a date expression piece is talking about."""
268         if 'month' in orig:
269             return TimeUnit.MONTHS
270         txt = orig.lower()[:3]
271         if txt in self.day_name_to_number:
272             return(self.day_name_to_number[txt])
273         elif txt in self.delta_unit_to_constant:
274             return(self.delta_unit_to_constant[txt])
275         raise ParseException(f'Invalid date unit: {orig}')
276
277     def _figure_out_time_unit(self, orig: str) -> int:
278         """Figure out what unit a time expression piece is talking about."""
279         txt = orig.lower()[:3]
280         if txt in self.time_delta_unit_to_constant:
281             return(self.time_delta_unit_to_constant[txt])
282         raise ParseException(f'Invalid time unit: {orig}')
283
284     def _parse_special_date(self, name: str) -> Optional[datetime.date]:
285         """Parse what we think is a special date name and return its datetime
286         (or None if it can't be parsed).
287         """
288         today = self.today
289         year = self.context.get('year', today.year)
290         name = DateParser._normalize_special_day_name(self.context['special'])
291
292         # Yesterday, today, tomorrow -- ignore any next/last
293         if name == 'today' or name == 'now':
294             return today
295         if name == 'yeste':
296             return today + datetime.timedelta(days=-1)
297         if name == 'tomor':
298             return today + datetime.timedelta(days=+1)
299
300         next_last = self.context.get('special_next_last', '')
301         if next_last == 'next':
302             year += 1
303         elif next_last == 'last':
304             year -= 1
305
306         # Holiday names
307         if name == 'easte':
308             return dateutil.easter.easter(year=year)
309         elif name == 'hallo':
310             return datetime.date(year=year, month=10, day=31)
311
312         for holiday_date, holiday_name in sorted(
313             holidays.US(years=year).items()
314         ):
315             if 'Observed' not in holiday_name:
316                 holiday_name = DateParser._normalize_special_day_name(
317                     holiday_name
318                 )
319                 if name == holiday_name:
320                     return holiday_date
321         if name == 'chriseve':
322             return datetime.date(year=year, month=12, day=24)
323         elif name == 'newyeeve':
324             return datetime.date(year=year, month=12, day=31)
325         return None
326
327     def _resolve_ides_nones(self, day: str, month_number: int) -> int:
328         """Handle date expressions like "the ides of March" which require
329         both the "ides" and the month since the definition of the "ides"
330         changes based on the length of the month.
331         """
332         assert 'ide' in day or 'non' in day
333         assert month_number in self.typical_days_per_month
334         typical_days_per_month = self.typical_days_per_month[month_number]
335
336         # "full" month
337         if typical_days_per_month == 31:
338             if self.context['day'] == 'ide':
339                 return 15
340             else:
341                 return 7
342
343         # "hollow" month
344         else:
345             if self.context['day'] == 'ide':
346                 return 13
347             else:
348                 return 5
349
350     def _parse_normal_date(self) -> datetime.date:
351         if 'dow' in self.context:
352             d = self.today
353             while d.weekday() != self.context['dow']:
354                 d += datetime.timedelta(days=1)
355             return d
356
357         if 'month' not in self.context:
358             raise ParseException('Missing month')
359         if 'day' not in self.context:
360             raise ParseException('Missing day')
361         if 'year' not in self.context:
362             self.context['year'] = self.today.year
363
364         # Handling "ides" and "nones" requires both the day and month.
365         if (
366                 self.context['day'] == 'ide' or
367                 self.context['day'] == 'non'
368         ):
369             self.context['day'] = self._resolve_ides_nones(
370                 self.context['day'], self.context['month']
371             )
372
373         return datetime.date(
374             year=self.context['year'],
375             month=self.context['month'],
376             day=self.context['day'],
377         )
378
379     def _parse_tz(self, txt: str) -> Any:
380         if txt == 'Z':
381             txt = 'UTC'
382
383         # Try pytz
384         try:
385             tz = pytz.timezone(txt)
386             if tz is not None:
387                 return tz
388         except Exception:
389             pass
390
391         # Try dateutil
392         try:
393             tz = dateutil.tz.gettz(txt)
394             if tz is not None:
395                 return tz
396         except Exception:
397             pass
398
399         # Try constructing an offset in seconds
400         try:
401             sign = txt[0]
402             if sign == '-' or sign == '+':
403                 sign = +1 if sign == '+' else -1
404                 hour = int(txt[1:3])
405                 minute = int(txt[-2:])
406                 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
407                 tzoffset = dateutil.tz.tzoffset(txt, offset)
408                 return tzoffset
409         except Exception:
410             pass
411         return None
412
413     def _get_int(self, txt: str) -> int:
414         while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
415             txt = txt[1:]
416         while not txt[-1].isdigit():
417             txt = txt[:-1]
418         return int(txt)
419
420     # -- overridden methods invoked by parse walk --
421
422     def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
423         pass
424
425     def visitTerminal(self, node: antlr4.TerminalNode) -> None:
426         pass
427
428     def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
429         """Populate self.datetime."""
430         if self.date is None:
431             self.date = self.today
432         year = self.date.year
433         month = self.date.month
434         day = self.date.day
435
436         if self.time is None:
437             self.time = datetime.time(0, 0, 0)
438         hour = self.time.hour
439         minute = self.time.minute
440         second = self.time.second
441         micros = self.time.microsecond
442
443         self.datetime = datetime.datetime(
444             year, month, day, hour, minute, second, micros,
445             tzinfo=self.time.tzinfo
446         )
447
448         # Apply resudual adjustments to times here when we have a
449         # datetime.
450         self.datetime = self.datetime + self.timedelta
451         self.time = datetime.time(
452             self.datetime.hour,
453             self.datetime.minute,
454             self.datetime.second,
455             self.datetime.microsecond,
456             self.datetime.tzinfo
457         )
458
459     def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
460         self.date = None
461         if ctx.singleDateExpr() is not None:
462             self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
463         elif ctx.baseAndOffsetDateExpr() is not None:
464             self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
465
466     def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
467         self.time = None
468         if ctx.singleTimeExpr() is not None:
469             self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
470         elif ctx.baseAndOffsetTimeExpr() is not None:
471             self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
472
473     def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
474         """When we leave the date expression, populate self.date."""
475         if 'special' in self.context:
476             self.date = self._parse_special_date(self.context['special'])
477         else:
478             self.date = self._parse_normal_date()
479         assert self.date is not None
480
481         # For a single date, just return the date we pulled out.
482         if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
483             return
484
485         # Otherwise treat self.date as a base date that we're modifying
486         # with an offset.
487         if 'delta_int' not in self.context:
488             raise ParseException('Missing delta_int?!')
489         count = self.context['delta_int']
490         if count == 0:
491             return
492
493         # Adjust count's sign based on the presence of 'before' or 'after'.
494         if 'delta_before_after' in self.context:
495             before_after = self.context['delta_before_after'].lower()
496             if (
497                     before_after == 'before' or
498                     before_after == 'until' or
499                     before_after == 'til' or
500                     before_after == 'to'
501             ):
502                 count = -count
503
504         # What are we counting units of?
505         if 'delta_unit' not in self.context:
506             raise ParseException('Missing delta_unit?!')
507         unit = self.context['delta_unit']
508         dt = n_timeunits_from_base(
509             count,
510             unit,
511             date_to_datetime(self.date)
512         )
513         self.date = datetime_to_date(dt)
514
515     def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
516         # Simple time?
517         self.time = datetime.time(
518             self.context['hour'],
519             self.context['minute'],
520             self.context['seconds'],
521             self.context['micros'],
522             tzinfo=self.context.get('tz', None),
523         )
524         if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
525             return
526
527         # If we get here there (should be) a relative adjustment to
528         # the time.
529         if 'nth' in self.context:
530             count = self.context['nth']
531         elif 'time_delta_int' in self.context:
532             count = self.context['time_delta_int']
533         else:
534             raise ParseException('Missing delta in relative time.')
535         if count == 0:
536             return
537
538         # Adjust count's sign based on the presence of 'before' or 'after'.
539         if 'time_delta_before_after' in self.context:
540             before_after = self.context['time_delta_before_after'].lower()
541             if (
542                     before_after == 'before' or
543                     before_after == 'until' or
544                     before_after == 'til' or
545                     before_after == 'to'
546             ):
547                 count = -count
548
549         # What are we counting units of... assume minutes.
550         if 'time_delta_unit' not in self.context:
551             self.timedelta += datetime.timedelta(minutes=count)
552         else:
553             unit = self.context['time_delta_unit']
554             if unit == TimeUnit.SECONDS:
555                 self.timedelta += datetime.timedelta(seconds=count)
556             elif unit == TimeUnit.MINUTES:
557                 self.timedelta = datetime.timedelta(minutes=count)
558             elif unit == TimeUnit.HOURS:
559                 self.timedelta = datetime.timedelta(hours=count)
560             else:
561                 raise ParseException()
562
563     def exitDeltaPlusMinusExpr(
564         self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
565     ) -> None:
566         try:
567             n = ctx.nth()
568             if n is None:
569                 raise ParseException(
570                     f'Bad N in Delta +/- Expr: {ctx.getText()}'
571                 )
572             n = n.getText()
573             n = self._get_int(n)
574             unit = self._figure_out_date_unit(
575                 ctx.deltaUnit().getText().lower()
576             )
577         except Exception:
578             raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
579         else:
580             self.context['delta_int'] = n
581             self.context['delta_unit'] = unit
582
583     def exitNextLastUnit(
584         self, ctx: dateparse_utilsParser.DeltaUnitContext
585     ) -> None:
586         try:
587             unit = self._figure_out_date_unit(ctx.getText().lower())
588         except Exception:
589             raise ParseException(f'Bad delta unit: {ctx.getText()}')
590         else:
591             self.context['delta_unit'] = unit
592
593     def exitDeltaNextLast(
594             self, ctx: dateparse_utilsParser.DeltaNextLastContext
595     ) -> None:
596         try:
597             txt = ctx.getText().lower()
598         except Exception:
599             raise ParseException(f'Bad next/last: {ctx.getText()}')
600         if (
601                 'month' in self.context or
602                 'day' in self.context or
603                 'year' in self.context
604         ):
605             raise ParseException(
606                 'Next/last expression expected to be relative to today.'
607             )
608         if txt[:4] == 'next':
609             self.context['delta_int'] = +1
610             self.context['day'] = self.now_datetime.day
611             self.context['month'] = self.now_datetime.month
612             self.context['year'] = self.now_datetime.year
613         elif txt[:4] == 'last':
614             self.context['delta_int'] = -1
615             self.context['day'] = self.now_datetime.day
616             self.context['month'] = self.now_datetime.month
617             self.context['year'] = self.now_datetime.year
618         else:
619             raise ParseException(f'Bad next/last: {ctx.getText()}')
620
621     def exitCountUnitsBeforeAfterTimeExpr(
622         self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
623     ) -> None:
624         if 'nth' not in self.context:
625             raise ParseException(
626                 f'Bad count expression: {ctx.getText()}'
627             )
628         try:
629             unit = self._figure_out_time_unit(
630                 ctx.deltaTimeUnit().getText().lower()
631             )
632             self.context['time_delta_unit'] = unit
633         except Exception:
634             raise ParseException(f'Bad delta unit: {ctx.getText()}')
635         if 'time_delta_before_after' not in self.context:
636             raise ParseException(
637                 f'Bad Before/After: {ctx.getText()}'
638             )
639
640     def exitDeltaTimeFraction(
641             self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
642     ) -> None:
643         try:
644             txt = ctx.getText().lower()[:4]
645             if txt == 'quar':
646                 self.context['time_delta_int'] = 15
647                 self.context[
648                     'time_delta_unit'
649                 ] = TimeUnit.MINUTES
650             elif txt == 'half':
651                 self.context['time_delta_int'] = 30
652                 self.context[
653                     'time_delta_unit'
654                 ] = TimeUnit.MINUTES
655             else:
656                 raise ParseException(f'Bad time fraction {ctx.getText()}')
657         except Exception:
658             raise ParseException(f'Bad time fraction {ctx.getText()}')
659
660     def exitDeltaBeforeAfter(
661         self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
662     ) -> None:
663         try:
664             txt = ctx.getText().lower()
665         except Exception:
666             raise ParseException(f'Bad delta before|after: {ctx.getText()}')
667         else:
668             self.context['delta_before_after'] = txt
669
670     def exitDeltaTimeBeforeAfter(
671         self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
672     ) -> None:
673         try:
674             txt = ctx.getText().lower()
675         except Exception:
676             raise ParseException(f'Bad delta before|after: {ctx.getText()}')
677         else:
678             self.context['time_delta_before_after'] = txt
679
680     def exitNthWeekdayInMonthMaybeYearExpr(
681         self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
682     ) -> None:
683         """Do a bunch of work to convert expressions like...
684
685         'the 2nd Friday of June' -and-
686         'the last Wednesday in October'
687
688         ...into base + offset expressions instead.
689         """
690         try:
691             if 'nth' not in self.context:
692                 raise ParseException(f'Missing nth number: {ctx.getText()}')
693             n = self.context['nth']
694             if n < 1 or n > 5:  # months never have more than 5 Foodays
695                 if n != -1:
696                     raise ParseException(f'Invalid nth number: {ctx.getText()}')
697             del self.context['nth']
698             self.context['delta_int'] = n
699
700             year = self.context.get('year', self.today.year)
701             if 'month' not in self.context:
702                 raise ParseException(
703                     f'Missing month expression: {ctx.getText()}'
704                 )
705             month = self.context['month']
706
707             dow = self.context['dow']
708             del self.context['dow']
709             self.context['delta_unit'] = dow
710
711             # For the nth Fooday in Month, start at the 1st of the
712             # month and count ahead N Foodays.  For the last Fooday in
713             # Month, start at the last of the month and count back one
714             # Fooday.
715             if n == -1:
716                 month += 1
717                 if month == 13:
718                     month = 1
719                     year += 1
720                 tmp_date = datetime.date(year=year, month=month, day=1)
721                 tmp_date = tmp_date - datetime.timedelta(days=1)
722
723                 self.context['year'] = tmp_date.year
724                 self.context['month'] = tmp_date.month
725                 self.context['day'] = tmp_date.day
726
727                 # The delta adjustment code can handle the case where
728                 # the last day of the month is the day we're looking
729                 # for already.
730             else:
731                 self.context['year'] = year
732                 self.context['month'] = month
733                 self.context['day'] = 1
734             self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
735         except Exception:
736             raise ParseException(
737                 f'Invalid nthWeekday expression: {ctx.getText()}'
738             )
739
740     def exitFirstLastWeekdayInMonthMaybeYearExpr(
741         self,
742         ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
743     ) -> None:
744         self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
745
746     def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
747         try:
748             i = self._get_int(ctx.getText())
749         except Exception:
750             raise ParseException(f'Bad nth expression: {ctx.getText()}')
751         else:
752             self.context['nth'] = i
753
754     def exitFirstOrLast(
755         self, ctx: dateparse_utilsParser.FirstOrLastContext
756     ) -> None:
757         try:
758             txt = ctx.getText()
759             if txt == 'first':
760                 txt = 1
761             elif txt == 'last':
762                 txt = -1
763             else:
764                 raise ParseException(
765                     f'Bad first|last expression: {ctx.getText()}'
766                 )
767         except Exception:
768             raise ParseException(f'Bad first|last expression: {ctx.getText()}')
769         else:
770             self.context['nth'] = txt
771
772     def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
773         try:
774             dow = ctx.getText().lower()[:3]
775             dow = self.day_name_to_number.get(dow, None)
776         except Exception:
777             raise ParseException('Bad day of week')
778         else:
779             self.context['dow'] = dow
780
781     def exitDayOfMonth(
782         self, ctx: dateparse_utilsParser.DayOfMonthContext
783     ) -> None:
784         try:
785             day = ctx.getText().lower()
786             if day[:3] == 'ide':
787                 self.context['day'] = 'ide'
788                 return
789             if day[:3] == 'non':
790                 self.context['day'] = 'non'
791                 return
792             if day[:3] == 'kal':
793                 self.context['day'] = 1
794                 return
795             day = self._get_int(day)
796             if day < 1 or day > 31:
797                 raise ParseException(
798                     f'Bad dayOfMonth expression: {ctx.getText()}'
799                 )
800         except Exception:
801             raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
802         self.context['day'] = day
803
804     def exitMonthName(
805         self, ctx: dateparse_utilsParser.MonthNameContext
806     ) -> None:
807         try:
808             month = ctx.getText()
809             while month[0] == '/' or month[0] == '-':
810                 month = month[1:]
811             month = month[:3].lower()
812             month = self.month_name_to_number.get(month, None)
813             if month is None:
814                 raise ParseException(
815                     f'Bad monthName expression: {ctx.getText()}'
816                 )
817         except Exception:
818             raise ParseException(f'Bad monthName expression: {ctx.getText()}')
819         else:
820             self.context['month'] = month
821
822     def exitMonthNumber(
823         self, ctx: dateparse_utilsParser.MonthNumberContext
824     ) -> None:
825         try:
826             month = self._get_int(ctx.getText())
827             if month < 1 or month > 12:
828                 raise ParseException(
829                     f'Bad monthNumber expression: {ctx.getText()}'
830                 )
831         except Exception:
832             raise ParseException(
833                 f'Bad monthNumber expression: {ctx.getText()}'
834             )
835         else:
836             self.context['month'] = month
837
838     def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
839         try:
840             year = self._get_int(ctx.getText())
841             if year < 1:
842                 raise ParseException(f'Bad year expression: {ctx.getText()}')
843         except Exception:
844             raise ParseException(f'Bad year expression: {ctx.getText()}')
845         else:
846             self.context['year'] = year
847
848     def exitSpecialDateMaybeYearExpr(
849         self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
850     ) -> None:
851         try:
852             special = ctx.specialDate().getText().lower()
853             self.context['special'] = special
854         except Exception:
855             raise ParseException(
856                 f'Bad specialDate expression: {ctx.specialDate().getText()}'
857             )
858         try:
859             mod = ctx.thisNextLast()
860             if mod is not None:
861                 if mod.THIS() is not None:
862                     self.context['special_next_last'] = 'this'
863                 elif mod.NEXT() is not None:
864                     self.context['special_next_last'] = 'next'
865                 elif mod.LAST() is not None:
866                     self.context['special_next_last'] = 'last'
867         except Exception:
868             raise ParseException(
869                 f'Bad specialDateNextLast expression: {ctx.getText()}'
870             )
871
872     def exitNFoosFromTodayAgoExpr(
873         self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
874     ) -> None:
875         d = self.now_datetime
876         try:
877             count = self._get_int(ctx.unsignedInt().getText())
878             unit = ctx.deltaUnit().getText().lower()
879             ago_from_now = ctx.AGO_FROM_NOW().getText()
880         except Exception:
881             raise ParseException(
882                 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
883             )
884
885         if "ago" in ago_from_now or "back" in ago_from_now:
886             count = -count
887
888         unit = self._figure_out_date_unit(unit)
889         d = n_timeunits_from_base(
890             count,
891             unit,
892             d)
893         self.context['year'] = d.year
894         self.context['month'] = d.month
895         self.context['day'] = d.day
896
897     def exitDeltaRelativeToTodayExpr(
898         self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
899     ) -> None:
900         d = self.now_datetime
901         try:
902             mod = ctx.thisNextLast()
903             if mod.LAST():
904                 count = -1
905             elif mod.THIS():
906                 count = +1
907             elif mod.NEXT():
908                 count = +2
909             else:
910                 raise ParseException(
911                     f'Bad This/Next/Last modifier: {mod}'
912                 )
913             unit = ctx.deltaUnit().getText().lower()
914         except Exception:
915             raise ParseException(
916                 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
917             )
918         unit = self._figure_out_date_unit(unit)
919         d = n_timeunits_from_base(
920             count,
921             unit,
922             d)
923         self.context['year'] = d.year
924         self.context['month'] = d.month
925         self.context['day'] = d.day
926
927     def exitSpecialTimeExpr(
928         self, ctx: dateparse_utilsParser.SpecialTimeExprContext
929     ) -> None:
930         try:
931             txt = ctx.specialTime().getText().lower()
932         except Exception:
933             raise ParseException(
934                 f'Bad special time expression: {ctx.getText()}'
935             )
936         else:
937             if txt == 'noon' or txt == 'midday':
938                 self.context['hour'] = 12
939                 self.context['minute'] = 0
940                 self.context['seconds'] = 0
941                 self.context['micros'] = 0
942             elif txt == 'midnight':
943                 self.context['hour'] = 0
944                 self.context['minute'] = 0
945                 self.context['seconds'] = 0
946                 self.context['micros'] = 0
947             else:
948                 raise ParseException(f'Bad special time expression: {txt}')
949
950         try:
951             tz = ctx.tzExpr().getText()
952             self.context['tz'] = self._parse_tz(tz)
953         except Exception:
954             pass
955
956     def exitTwelveHourTimeExpr(
957         self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
958     ) -> None:
959         try:
960             hour = ctx.hour().getText()
961             while not hour[-1].isdigit():
962                 hour = hour[:-1]
963             hour = self._get_int(hour)
964         except Exception:
965             raise ParseException(f'Bad hour: {ctx.hour().getText()}')
966         if hour <= 0 or hour > 12:
967             raise ParseException(f'Bad hour (out of range): {hour}')
968
969         try:
970             minute = self._get_int(ctx.minute().getText())
971         except Exception:
972             minute = 0
973         if minute < 0 or minute > 59:
974             raise ParseException(f'Bad minute (out of range): {minute}')
975         self.context['minute'] = minute
976
977         try:
978             seconds = self._get_int(ctx.second().getText())
979         except Exception:
980             seconds = 0
981         if seconds < 0 or seconds > 59:
982             raise ParseException(f'Bad second (out of range): {seconds}')
983         self.context['seconds'] = seconds
984
985         try:
986             micros = self._get_int(ctx.micros().getText())
987         except Exception:
988             micros = 0
989         if micros < 0 or micros > 1000000:
990             raise ParseException(f'Bad micros (out of range): {micros}')
991         self.context['micros'] = micros
992
993         try:
994             ampm = ctx.ampm().getText()
995         except Exception:
996             raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
997         if hour == 12:
998             hour = 0
999         if ampm[0] == 'p':
1000             hour += 12
1001         self.context['hour'] = hour
1002
1003         try:
1004             tz = ctx.tzExpr().getText()
1005             self.context['tz'] = self._parse_tz(tz)
1006         except Exception:
1007             pass
1008
1009     def exitTwentyFourHourTimeExpr(
1010         self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1011     ) -> None:
1012         try:
1013             hour = ctx.hour().getText()
1014             while not hour[-1].isdigit():
1015                 hour = hour[:-1]
1016             hour = self._get_int(hour)
1017         except Exception:
1018             raise ParseException(f'Bad hour: {ctx.hour().getText()}')
1019         if hour < 0 or hour > 23:
1020             raise ParseException(f'Bad hour (out of range): {hour}')
1021         self.context['hour'] = hour
1022
1023         try:
1024             minute = self._get_int(ctx.minute().getText())
1025         except Exception:
1026             minute = 0
1027         if minute < 0 or minute > 59:
1028             raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1029         self.context['minute'] = minute
1030
1031         try:
1032             seconds = self._get_int(ctx.second().getText())
1033         except Exception:
1034             seconds = 0
1035         if seconds < 0 or seconds > 59:
1036             raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1037         self.context['seconds'] = seconds
1038
1039         try:
1040             micros = self._get_int(ctx.micros().getText())
1041         except Exception:
1042             micros = 0
1043         if micros < 0 or micros >= 1000000:
1044             raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1045         self.context['micros'] = micros
1046
1047         try:
1048             tz = ctx.tzExpr().getText()
1049             self.context['tz'] = self._parse_tz(tz)
1050         except Exception:
1051             pass
1052
1053
1054 @bootstrap.initialize
1055 def main() -> None:
1056     parser = DateParser()
1057     for line in sys.stdin:
1058         line = line.strip()
1059         line = re.sub(r"#.*$", "", line)
1060         if re.match(r"^ *$", line) is not None:
1061             continue
1062         try:
1063             dt = parser.parse(line)
1064         except Exception as e:
1065             print("Unrecognized.")
1066         else:
1067             print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1068     sys.exit(0)
1069
1070
1071 if __name__ == "__main__":
1072     main()