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