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