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