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