3 # pylint: disable=W0201
4 # pylint: disable=R0904
6 # © Copyright 2021-2022, Scott Gasch
9 Parse dates / datetimes in a variety of formats. Some examples:
31 | 5 years from yesterday
32 | 6 weeks from tomorrow
35 | 9:30am on last Wednesday
38 | the 1st wednesday in may
39 | the last sun of june
45 | 5 work days after independence day
46 | 50 working days from last wed
47 | 25 working days before xmas
50 | 3 weeks before xmas, 1999
51 | 3 days before new years eve, 2000
56 | 4 sundays before veterans' day
64 | 2 days before last xmas at 3:14:15.92a
65 | 3 weeks after xmas, 1995 at midday
66 | 4 months before easter, 1992 at midnight
67 | 5 months before halloween, 1995 at noon
68 | 4 days before last wednesday
69 | 44 months after today
70 | 44 years before today
75 | 4 seconds to midnight
76 | 4 seconds to midnight, tomorrow
77 | 2021/apr/15T21:30:44.55
78 | 2021/apr/15 at 21:30:44.55
79 | 2021/4/15 at 21:30:44.55
80 | 2021/04/15 at 21:30:44.55Z
81 | 2021/04/15 at 21:30:44.55EST
82 | 13 days after last memorial day at 12 seconds before 4pm
84 This code is used by other code in the pyutils library; for example,
85 when using :file:`argparse_utils.py` to pass an argument of type
86 datetime it allows the user to use free form English expressions.
88 See the `unittest <https://wannabe.guru.org/gitweb/?p=pyutils.git;a=blob_plain;f=tests/datetimez/dateparse_utils_test.py;h=93c7b96e4c19af217fbafcf1ed5dbde13ec599c5;hb=HEAD>`_ for more examples and the `grammar <https://wannabe.guru.org/gitweb/?p=pyutils.git;a=blob_plain;f=src/pyutils/datetimez/dateparse_utils.g4;hb=HEAD>`_ for more details.
96 from typing import Any, Callable, Dict, Optional
98 import antlr4 # type: ignore
99 import dateutil.easter
101 import holidays # type: ignore
104 from pyutils import bootstrap, decorator_utils
105 from pyutils.datetimez.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
106 from pyutils.datetimez.dateparse_utilsListener import (
107 dateparse_utilsListener,
109 from pyutils.datetimez.dateparse_utilsParser import (
110 dateparse_utilsParser,
112 from pyutils.datetimez.datetime_utils import (
116 n_timeunits_from_base,
118 from pyutils.security import acl
120 logger = logging.getLogger(__name__)
123 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
124 @functools.wraps(enter_or_exit_f)
125 def debug_parse_wrapper(*args, **kwargs):
131 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
133 for c in ctx.getChildren():
134 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
135 retval = enter_or_exit_f(*args, **kwargs)
138 return debug_parse_wrapper
141 class ParseException(Exception):
142 """An exception thrown during parsing because of unrecognized input."""
144 def __init__(self, message: str) -> None:
147 message: parse error message description.
150 self.message = message
153 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
154 """An error listener that raises ParseExceptions."""
156 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
157 raise ParseException(msg)
160 self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs
164 def reportAttemptingFullContext(
165 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
169 def reportContextSensitivity(
170 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
175 @decorator_utils.decorate_matching_methods_with(
177 acl=acl.StringWildcardBasedACL(
182 denied_patterns=['enterEveryRule', 'exitEveryRule'],
183 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
184 default_answer=False,
187 class DateParser(dateparse_utilsListener):
188 """A class to parse dates expressed in human language (English).
192 d.parse('next wednesday')
193 dt = d.get_datetime()
195 Wednesday 2022/10/26 00:00:00.000000
197 Note that the interface is somewhat klunky here because this class is
198 conforming to interfaces auto-generated by ANTLR as it parses the grammar.
199 See also :meth:`pyutils.string_utils.to_date`.
203 PARSE_TYPE_SINGLE_DATE_EXPR = 1
204 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
205 PARSE_TYPE_SINGLE_TIME_EXPR = 3
206 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
208 def __init__(self, *, override_now_for_test_purposes=None) -> None:
209 """Construct a parser.
212 override_now_for_test_purposes: passing a value to
213 override_now_for_test_purposes can be used to force
214 this parser instance to use a custom date/time for its
215 idea of "now" so that the code can be more easily
216 unittested. Leave as None for real use cases.
218 self.month_name_to_number = {
233 # Used only for ides/nones. Month length on a non-leap year.
234 self.typical_days_per_month = {
249 # N.B. day number is also synched with datetime_utils.TimeUnit values
250 # which allows expressions like "3 wednesdays from now" to work.
251 self.day_name_to_number = {
261 # These TimeUnits are defined in datetime_utils and are used as params
262 # to datetime_utils.n_timeunits_from_base.
263 self.time_delta_unit_to_constant = {
264 'hou': TimeUnit.HOURS,
265 'min': TimeUnit.MINUTES,
266 'sec': TimeUnit.SECONDS,
268 self.delta_unit_to_constant = {
269 'day': TimeUnit.DAYS,
270 'wor': TimeUnit.WORKDAYS,
271 'wee': TimeUnit.WEEKS,
272 'mon': TimeUnit.MONTHS,
273 'yea': TimeUnit.YEARS,
275 self.override_now_for_test_purposes = override_now_for_test_purposes
277 # Note: _reset defines several class fields. It is used both here
278 # in the c'tor but also in between parse operations to restore the
279 # class' state and allow it to be reused.
283 def parse(self, date_string: str) -> Optional[datetime.datetime]:
285 Parse a ~free form date/time expression and return a
286 timezone agnostic datetime on success. Also sets
287 `self.datetime`, `self.date` and `self.time` which can each be
288 accessed other methods on the class: :meth:`get_datetime`,
289 :meth:`get_date` and :meth:`get_time`. Raises a
290 `ParseException` with a helpful(?) message on parse error or
293 This is the main entrypoint to this class for caller code.
295 To get an idea of what expressions can be parsed, check out
296 the unittest and the grammar.
299 date_string: the string to parse
302 A datetime.datetime representing the parsed date/time or
307 Parsed date expressions without any time part return
308 hours = minutes = seconds = microseconds = 0 (i.e. at
309 midnight that day). Parsed time expressions without any
310 date part default to date = today.
314 txt = '3 weeks before last tues at 9:15am'
317 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
319 # dt1 and dt2 will be identical other than the fact that
320 # the latter's tzinfo will be set to PST/PDT.
323 date_string = date_string.strip()
324 date_string = re.sub(r'\s+', ' ', date_string)
326 listener = RaisingErrorListener()
327 input_stream = antlr4.InputStream(date_string)
328 lexer = dateparse_utilsLexer(input_stream)
329 lexer.removeErrorListeners()
330 lexer.addErrorListener(listener)
331 stream = antlr4.CommonTokenStream(lexer)
332 parser = dateparse_utilsParser(stream)
333 parser.removeErrorListeners()
334 parser.addErrorListener(listener)
335 tree = parser.parse()
336 walker = antlr4.ParseTreeWalker()
337 walker.walk(self, tree)
340 def get_date(self) -> Optional[datetime.date]:
343 The date part of the last :meth:`parse` operation again
348 def get_time(self) -> Optional[datetime.time]:
351 The time part of the last :meth:`parse` operation again
356 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
357 """Get the datetime of the last :meth:`parse` operation again
361 tz: the timezone to set on output times. By default we
362 return timezone-naive datetime objects.
365 the same datetime that :meth:`parse` last did, optionally
366 overriding the timezone. Returns None of no calls to
367 :meth:`parse` have yet been made.
371 Parsed date expressions without any time part return
372 hours = minutes = seconds = microseconds = 0 (i.e. at
373 midnight that day). Parsed time expressions without any
374 date part default to date = today.
379 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
385 """Reset at init and between parses."""
386 if self.override_now_for_test_purposes is None:
387 self.now_datetime = datetime.datetime.now()
388 self.today = datetime.date.today()
390 self.now_datetime = self.override_now_for_test_purposes
391 self.today = datetime_to_date(self.override_now_for_test_purposes)
392 self.date: Optional[datetime.date] = None
393 self.time: Optional[datetime.time] = None
394 self.datetime: Optional[datetime.datetime] = None
395 self.context: Dict[str, Any] = {}
396 self.timedelta = datetime.timedelta(seconds=0)
397 self.saw_overt_year = False
400 def _normalize_special_day_name(name: str) -> str:
401 """String normalization / canonicalization for date expressions."""
403 name = name.replace("'", '')
404 name = name.replace('xmas', 'christmas')
405 name = name.replace('mlk', 'martin luther king')
406 name = name.replace(' ', '')
407 eve = 'eve' if name[-3:] == 'eve' else ''
408 name = name[:5] + eve
409 name = name.replace('washi', 'presi')
412 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
413 """Figure out what unit a date expression piece is talking about."""
415 return TimeUnit.MONTHS
416 txt = orig.lower()[:3]
417 if txt in self.day_name_to_number:
418 return TimeUnit(self.day_name_to_number[txt])
419 elif txt in self.delta_unit_to_constant:
420 return TimeUnit(self.delta_unit_to_constant[txt])
421 raise ParseException(f'Invalid date unit: {orig}')
423 def _figure_out_time_unit(self, orig: str) -> int:
424 """Figure out what unit a time expression piece is talking about."""
425 txt = orig.lower()[:3]
426 if txt in self.time_delta_unit_to_constant:
427 return self.time_delta_unit_to_constant[txt]
428 raise ParseException(f'Invalid time unit: {orig}')
430 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
431 """Parse what we think is a special date name and return its datetime
432 (or None if it can't be parsed).
435 year = self.context.get('year', today.year)
436 name = DateParser._normalize_special_day_name(self.context['special'])
438 # Yesterday, today, tomorrow -- ignore any next/last
439 if name in ('today', 'now'):
442 return today + datetime.timedelta(days=-1)
444 return today + datetime.timedelta(days=+1)
446 next_last = self.context.get('special_next_last', '')
447 if next_last == 'next':
449 self.saw_overt_year = True
450 elif next_last == 'last':
452 self.saw_overt_year = True
456 return dateutil.easter.easter(year=year)
457 elif name == 'hallo':
458 return datetime.date(year=year, month=10, day=31)
460 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
461 if 'Observed' not in holiday_name:
462 holiday_name = DateParser._normalize_special_day_name(holiday_name)
463 if name == holiday_name:
465 if name == 'chriseve':
466 return datetime.date(year=year, month=12, day=24)
467 elif name == 'newyeeve':
468 return datetime.date(year=year, month=12, day=31)
471 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
472 """Handle date expressions like "the ides of March" which require
473 both the "ides" and the month since the definition of the "ides"
474 changes based on the length of the month.
476 assert 'ide' in day or 'non' in day
477 assert month_number in self.typical_days_per_month
478 typical_days_per_month = self.typical_days_per_month[month_number]
481 if typical_days_per_month == 31:
482 if self.context['day'] == 'ide':
489 if self.context['day'] == 'ide':
494 def _parse_normal_date(self) -> datetime.date:
495 if 'dow' in self.context and 'month' not in self.context:
497 while d.weekday() != self.context['dow']:
498 d += datetime.timedelta(days=1)
501 if 'month' not in self.context:
502 raise ParseException('Missing month')
503 if 'day' not in self.context:
504 raise ParseException('Missing day')
505 if 'year' not in self.context:
506 self.context['year'] = self.today.year
507 self.saw_overt_year = False
509 self.saw_overt_year = True
511 # Handling "ides" and "nones" requires both the day and month.
512 if self.context['day'] == 'ide' or self.context['day'] == 'non':
513 self.context['day'] = self._resolve_ides_nones(
514 self.context['day'], self.context['month']
517 return datetime.date(
518 year=self.context['year'],
519 month=self.context['month'],
520 day=self.context['day'],
524 def _parse_tz(txt: str) -> Any:
530 tz1 = pytz.timezone(txt)
538 tz2 = dateutil.tz.gettz(txt)
544 # Try constructing an offset in seconds
547 if txt_sign in ('-', '+'):
548 sign = +1 if txt_sign == '+' else -1
550 minute = int(txt[-2:])
551 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
552 tzoffset = dateutil.tz.tzoffset(txt, offset)
559 def _get_int(txt: str) -> int:
560 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
562 while not txt[-1].isdigit():
566 # -- overridden methods invoked by parse walk. Note: not part of the class'
569 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
572 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
575 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
576 """Populate self.datetime."""
577 if self.date is None:
578 self.date = self.today
579 year = self.date.year
580 month = self.date.month
583 if self.time is None:
584 self.time = datetime.time(0, 0, 0)
585 hour = self.time.hour
586 minute = self.time.minute
587 second = self.time.second
588 micros = self.time.microsecond
590 self.datetime = datetime.datetime(
598 tzinfo=self.time.tzinfo,
601 # Apply resudual adjustments to times here when we have a
603 self.datetime = self.datetime + self.timedelta
604 assert self.datetime is not None
605 self.time = datetime.time(
607 self.datetime.minute,
608 self.datetime.second,
609 self.datetime.microsecond,
610 self.datetime.tzinfo,
613 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
615 if ctx.singleDateExpr() is not None:
616 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
617 elif ctx.baseAndOffsetDateExpr() is not None:
618 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
620 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
622 if ctx.singleTimeExpr() is not None:
623 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
624 elif ctx.baseAndOffsetTimeExpr() is not None:
625 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
627 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
628 """When we leave the date expression, populate self.date."""
629 if 'special' in self.context:
630 self.date = self._parse_special_date(self.context['special'])
632 self.date = self._parse_normal_date()
633 assert self.date is not None
635 # For a single date, just return the date we pulled out.
636 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
639 # Otherwise treat self.date as a base date that we're modifying
641 if 'delta_int' not in self.context:
642 raise ParseException('Missing delta_int?!')
643 count = self.context['delta_int']
647 # Adjust count's sign based on the presence of 'before' or 'after'.
648 if 'delta_before_after' in self.context:
649 before_after = self.context['delta_before_after'].lower()
650 if before_after in ('before', 'until', 'til', 'to'):
653 # What are we counting units of?
654 if 'delta_unit' not in self.context:
655 raise ParseException('Missing delta_unit?!')
656 unit = self.context['delta_unit']
657 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
658 self.date = datetime_to_date(dt)
660 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
662 self.time = datetime.time(
663 self.context['hour'],
664 self.context['minute'],
665 self.context['seconds'],
666 self.context['micros'],
667 tzinfo=self.context.get('tz', None),
669 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
672 # If we get here there (should be) a relative adjustment to
674 if 'nth' in self.context:
675 count = self.context['nth']
676 elif 'time_delta_int' in self.context:
677 count = self.context['time_delta_int']
679 raise ParseException('Missing delta in relative time.')
683 # Adjust count's sign based on the presence of 'before' or 'after'.
684 if 'time_delta_before_after' in self.context:
685 before_after = self.context['time_delta_before_after'].lower()
686 if before_after in ('before', 'until', 'til', 'to'):
689 # What are we counting units of... assume minutes.
690 if 'time_delta_unit' not in self.context:
691 self.timedelta += datetime.timedelta(minutes=count)
693 unit = self.context['time_delta_unit']
694 if unit == TimeUnit.SECONDS:
695 self.timedelta += datetime.timedelta(seconds=count)
696 elif unit == TimeUnit.MINUTES:
697 self.timedelta = datetime.timedelta(minutes=count)
698 elif unit == TimeUnit.HOURS:
699 self.timedelta = datetime.timedelta(hours=count)
701 raise ParseException(f'Invalid Unit: "{unit}"')
703 def exitDeltaPlusMinusExpr(
704 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
709 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
711 n = DateParser._get_int(n)
712 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
713 except Exception as e:
714 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
716 self.context['delta_int'] = n
717 self.context['delta_unit'] = unit
719 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
721 unit = self._figure_out_date_unit(ctx.getText().lower())
722 except Exception as e:
723 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
725 self.context['delta_unit'] = unit
727 def exitDeltaNextLast(
728 self, ctx: dateparse_utilsParser.DeltaNextLastContext
731 txt = ctx.getText().lower()
732 except Exception as e:
733 raise ParseException(f'Bad next/last: {ctx.getText()}') from e
734 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
735 raise ParseException(
736 'Next/last expression expected to be relative to today.'
738 if txt[:4] == 'next':
739 self.context['delta_int'] = +1
740 self.context['day'] = self.now_datetime.day
741 self.context['month'] = self.now_datetime.month
742 self.context['year'] = self.now_datetime.year
743 self.saw_overt_year = True
744 elif txt[:4] == 'last':
745 self.context['delta_int'] = -1
746 self.context['day'] = self.now_datetime.day
747 self.context['month'] = self.now_datetime.month
748 self.context['year'] = self.now_datetime.year
749 self.saw_overt_year = True
751 raise ParseException(f'Bad next/last: {ctx.getText()}')
753 def exitCountUnitsBeforeAfterTimeExpr(
754 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
756 if 'nth' not in self.context:
757 raise ParseException(f'Bad count expression: {ctx.getText()}')
759 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
760 self.context['time_delta_unit'] = unit
761 except Exception as e:
762 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
763 if 'time_delta_before_after' not in self.context:
764 raise ParseException(f'Bad Before/After: {ctx.getText()}')
766 def exitDeltaTimeFraction(
767 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
770 txt = ctx.getText().lower()[:4]
772 self.context['time_delta_int'] = 15
773 self.context['time_delta_unit'] = TimeUnit.MINUTES
775 self.context['time_delta_int'] = 30
776 self.context['time_delta_unit'] = TimeUnit.MINUTES
778 raise ParseException(f'Bad time fraction {ctx.getText()}')
779 except Exception as e:
780 raise ParseException(f'Bad time fraction {ctx.getText()}') from e
782 def exitDeltaBeforeAfter(
783 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
786 txt = ctx.getText().lower()
787 except Exception as e:
788 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
790 self.context['delta_before_after'] = txt
792 def exitDeltaTimeBeforeAfter(
793 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
796 txt = ctx.getText().lower()
797 except Exception as e:
798 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
800 self.context['time_delta_before_after'] = txt
802 def exitNthWeekdayInMonthMaybeYearExpr(
803 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
805 """Do a bunch of work to convert expressions like...
807 'the 2nd Friday of June' -and-
808 'the last Wednesday in October'
810 ...into base + offset expressions instead.
813 if 'nth' not in self.context:
814 raise ParseException(f'Missing nth number: {ctx.getText()}')
815 n = self.context['nth']
816 if n < 1 or n > 5: # months never have more than 5 Foodays
818 raise ParseException(f'Invalid nth number: {ctx.getText()}')
819 del self.context['nth']
820 self.context['delta_int'] = n
822 year = self.context.get('year', self.today.year)
823 if 'month' not in self.context:
824 raise ParseException(f'Missing month expression: {ctx.getText()}')
825 month = self.context['month']
827 dow = self.context['dow']
828 del self.context['dow']
829 self.context['delta_unit'] = dow
831 # For the nth Fooday in Month, start at the last day of
832 # the previous month count ahead N Foodays. For the last
833 # Fooday in Month, start at the last of the month and
834 # count back one Fooday.
840 tmp_date = datetime.date(year=year, month=month, day=1)
841 tmp_date = tmp_date - datetime.timedelta(days=1)
843 # The delta adjustment code can handle the case where
844 # the last day of the month is the day we're looking
847 tmp_date = datetime.date(year=year, month=month, day=1)
848 tmp_date = tmp_date - datetime.timedelta(days=1)
850 self.context['year'] = tmp_date.year
851 self.context['month'] = tmp_date.month
852 self.context['day'] = tmp_date.day
853 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
854 except Exception as e:
855 raise ParseException(
856 f'Invalid nthWeekday expression: {ctx.getText()}'
859 def exitFirstLastWeekdayInMonthMaybeYearExpr(
861 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
863 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
865 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
867 i = DateParser._get_int(ctx.getText())
868 except Exception as e:
869 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
871 self.context['nth'] = i
873 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
881 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
882 except Exception as e:
883 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
885 self.context['nth'] = txt
887 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
889 dow = ctx.getText().lower()[:3]
890 dow = self.day_name_to_number.get(dow, None)
891 except Exception as e:
892 raise ParseException('Bad day of week') from e
894 self.context['dow'] = dow
896 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
898 day = ctx.getText().lower()
900 self.context['day'] = 'ide'
903 self.context['day'] = 'non'
906 self.context['day'] = 1
908 day = DateParser._get_int(day)
909 if day < 1 or day > 31:
910 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
911 except Exception as e:
912 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
913 self.context['day'] = day
915 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
917 month = ctx.getText()
918 while month[0] == '/' or month[0] == '-':
920 month = month[:3].lower()
921 month = self.month_name_to_number.get(month, None)
923 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
924 except Exception as e:
925 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
927 self.context['month'] = month
929 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
931 month = DateParser._get_int(ctx.getText())
932 if month < 1 or month > 12:
933 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
934 except Exception as e:
935 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
937 self.context['month'] = month
939 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
941 year = DateParser._get_int(ctx.getText())
943 raise ParseException(f'Bad year expression: {ctx.getText()}')
944 except Exception as e:
945 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
947 self.saw_overt_year = True
948 self.context['year'] = year
950 def exitSpecialDateMaybeYearExpr(
951 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
954 special = ctx.specialDate().getText().lower()
955 self.context['special'] = special
956 except Exception as e:
957 raise ParseException(
958 f'Bad specialDate expression: {ctx.specialDate().getText()}'
961 mod = ctx.thisNextLast()
963 if mod.THIS() is not None:
964 self.context['special_next_last'] = 'this'
965 elif mod.NEXT() is not None:
966 self.context['special_next_last'] = 'next'
967 elif mod.LAST() is not None:
968 self.context['special_next_last'] = 'last'
969 except Exception as e:
970 raise ParseException(
971 f'Bad specialDateNextLast expression: {ctx.getText()}'
974 def exitNFoosFromTodayAgoExpr(
975 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
977 d = self.now_datetime
979 count = DateParser._get_int(ctx.unsignedInt().getText())
980 unit = ctx.deltaUnit().getText().lower()
981 ago_from_now = ctx.AGO_FROM_NOW().getText()
982 except Exception as e:
983 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
985 if "ago" in ago_from_now or "back" in ago_from_now:
988 unit = self._figure_out_date_unit(unit)
989 d = n_timeunits_from_base(count, TimeUnit(unit), d)
990 self.context['year'] = d.year
991 self.context['month'] = d.month
992 self.context['day'] = d.day
994 def exitDeltaRelativeToTodayExpr(
995 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
997 # When someone says "next week" they mean a week from now.
998 # Likewise next month or last year. These expressions are now
1001 # But when someone says "this Friday" they mean "this coming
1002 # Friday". It would be weird to say "this Friday" if today
1003 # was already Friday but I'm parsing it to mean: the next day
1004 # that is a Friday. So when you say "next Friday" you mean
1005 # the Friday after this coming Friday, or 2 Fridays from now.
1007 # This set handles this weirdness.
1012 TimeUnit.WEDNESDAYS,
1019 d = self.now_datetime
1021 mod = ctx.thisNextLast()
1022 unit = ctx.deltaUnit().getText().lower()
1023 unit = self._figure_out_date_unit(unit)
1027 if unit in weekdays:
1032 if unit in weekdays:
1037 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
1038 except Exception as e:
1039 raise ParseException(
1040 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
1042 d = n_timeunits_from_base(count, TimeUnit(unit), d)
1043 self.context['year'] = d.year
1044 self.context['month'] = d.month
1045 self.context['day'] = d.day
1047 def exitSpecialTimeExpr(
1048 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
1051 txt = ctx.specialTime().getText().lower()
1052 except Exception as e:
1053 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
1055 if txt in ('noon', 'midday'):
1056 self.context['hour'] = 12
1057 self.context['minute'] = 0
1058 self.context['seconds'] = 0
1059 self.context['micros'] = 0
1060 elif txt == 'midnight':
1061 self.context['hour'] = 0
1062 self.context['minute'] = 0
1063 self.context['seconds'] = 0
1064 self.context['micros'] = 0
1066 raise ParseException(f'Bad special time expression: {txt}')
1069 tz = ctx.tzExpr().getText()
1070 self.context['tz'] = DateParser._parse_tz(tz)
1074 def exitTwelveHourTimeExpr(
1075 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
1078 hour = ctx.hour().getText()
1079 while not hour[-1].isdigit():
1081 hour = DateParser._get_int(hour)
1082 except Exception as e:
1083 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1084 if hour <= 0 or hour > 12:
1085 raise ParseException(f'Bad hour (out of range): {hour}')
1088 minute = DateParser._get_int(ctx.minute().getText())
1091 if minute < 0 or minute > 59:
1092 raise ParseException(f'Bad minute (out of range): {minute}')
1093 self.context['minute'] = minute
1096 seconds = DateParser._get_int(ctx.second().getText())
1099 if seconds < 0 or seconds > 59:
1100 raise ParseException(f'Bad second (out of range): {seconds}')
1101 self.context['seconds'] = seconds
1104 micros = DateParser._get_int(ctx.micros().getText())
1107 if micros < 0 or micros > 1000000:
1108 raise ParseException(f'Bad micros (out of range): {micros}')
1109 self.context['micros'] = micros
1112 ampm = ctx.ampm().getText()
1113 except Exception as e:
1114 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
1119 self.context['hour'] = hour
1122 tz = ctx.tzExpr().getText()
1123 self.context['tz'] = DateParser._parse_tz(tz)
1127 def exitTwentyFourHourTimeExpr(
1128 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1131 hour = ctx.hour().getText()
1132 while not hour[-1].isdigit():
1134 hour = DateParser._get_int(hour)
1135 except Exception as e:
1136 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1137 if hour < 0 or hour > 23:
1138 raise ParseException(f'Bad hour (out of range): {hour}')
1139 self.context['hour'] = hour
1142 minute = DateParser._get_int(ctx.minute().getText())
1145 if minute < 0 or minute > 59:
1146 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1147 self.context['minute'] = minute
1150 seconds = DateParser._get_int(ctx.second().getText())
1153 if seconds < 0 or seconds > 59:
1154 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1155 self.context['seconds'] = seconds
1158 micros = DateParser._get_int(ctx.micros().getText())
1161 if micros < 0 or micros >= 1000000:
1162 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1163 self.context['micros'] = micros
1166 tz = ctx.tzExpr().getText()
1167 self.context['tz'] = DateParser._parse_tz(tz)
1172 @bootstrap.initialize
1174 parser = DateParser()
1175 for line in sys.stdin:
1177 line = re.sub(r"#.*$", "", line)
1178 if re.match(r"^ *$", line) is not None:
1181 dt = parser.parse(line)
1182 except Exception as e:
1184 print("Unrecognized.")
1186 assert dt is not None
1187 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1191 if __name__ == "__main__":