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