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