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