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