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