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 such as
85 :meth:`pyutils.argparse_utils.valid_datetime`,
86 :meth:`pyutils.argparse_utils.valid_date`,
87 :meth:`pyutils.string_utils.to_datetime`
89 :meth:`pyutils.string_utils.to_date`. This means any of these are
90 also able to accept and recognize this larger set of date expressions.
92 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.
100 from typing import Any, Callable, Dict, Optional
102 import antlr4 # type: ignore
103 import dateutil.easter
105 import holidays # type: ignore
108 from pyutils import bootstrap, decorator_utils
109 from pyutils.datetimez.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
110 from pyutils.datetimez.dateparse_utilsListener import (
111 dateparse_utilsListener,
113 from pyutils.datetimez.dateparse_utilsParser import (
114 dateparse_utilsParser,
116 from pyutils.datetimez.datetime_utils import (
120 n_timeunits_from_base,
122 from pyutils.security import acl
124 logger = logging.getLogger(__name__)
127 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
128 @functools.wraps(enter_or_exit_f)
129 def debug_parse_wrapper(*args, **kwargs):
135 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
137 for c in ctx.getChildren():
138 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
139 retval = enter_or_exit_f(*args, **kwargs)
142 return debug_parse_wrapper
145 class ParseException(Exception):
146 """An exception thrown during parsing because of unrecognized input."""
148 def __init__(self, message: str) -> None:
151 message: parse error message description.
154 self.message = message
157 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
158 """An error listener that raises ParseExceptions."""
160 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
161 raise ParseException(msg)
164 self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs
168 def reportAttemptingFullContext(
169 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
173 def reportContextSensitivity(
174 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
179 @decorator_utils.decorate_matching_methods_with(
181 acl=acl.StringWildcardBasedACL(
186 denied_patterns=['enterEveryRule', 'exitEveryRule'],
187 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
188 default_answer=False,
191 class DateParser(dateparse_utilsListener):
192 """A class to parse dates expressed in human language (English).
196 d.parse('next wednesday')
197 dt = d.get_datetime()
199 Wednesday 2022/10/26 00:00:00.000000
201 Note that the interface is somewhat klunky here because this class is
202 conforming to interfaces auto-generated by ANTLR as it parses the grammar.
203 See also :meth:`pyutils.string_utils.to_date`.
207 PARSE_TYPE_SINGLE_DATE_EXPR = 1
208 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
209 PARSE_TYPE_SINGLE_TIME_EXPR = 3
210 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
212 def __init__(self, *, override_now_for_test_purposes=None) -> None:
213 """Construct a parser.
216 override_now_for_test_purposes: passing a value to
217 override_now_for_test_purposes can be used to force
218 this parser instance to use a custom date/time for its
219 idea of "now" so that the code can be more easily
220 unittested. Leave as None for real use cases.
222 self.month_name_to_number = {
237 # Used only for ides/nones. Month length on a non-leap year.
238 self.typical_days_per_month = {
253 # N.B. day number is also synched with datetime_utils.TimeUnit values
254 # which allows expressions like "3 wednesdays from now" to work.
255 self.day_name_to_number = {
265 # These TimeUnits are defined in datetime_utils and are used as params
266 # to datetime_utils.n_timeunits_from_base.
267 self.time_delta_unit_to_constant = {
268 'hou': TimeUnit.HOURS,
269 'min': TimeUnit.MINUTES,
270 'sec': TimeUnit.SECONDS,
272 self.delta_unit_to_constant = {
273 'day': TimeUnit.DAYS,
274 'wor': TimeUnit.WORKDAYS,
275 'wee': TimeUnit.WEEKS,
276 'mon': TimeUnit.MONTHS,
277 'yea': TimeUnit.YEARS,
279 self.override_now_for_test_purposes = override_now_for_test_purposes
281 # Note: _reset defines several class fields. It is used both here
282 # in the c'tor but also in between parse operations to restore the
283 # class' state and allow it to be reused.
287 def parse(self, date_string: str) -> Optional[datetime.datetime]:
289 Parse a ~free form date/time expression and return a
290 timezone agnostic datetime on success. Also sets
291 `self.datetime`, `self.date` and `self.time` which can each be
292 accessed other methods on the class: :meth:`get_datetime`,
293 :meth:`get_date` and :meth:`get_time`. Raises a
294 `ParseException` with a helpful(?) message on parse error or
297 This is the main entrypoint to this class for caller code.
299 To get an idea of what expressions can be parsed, check out
300 the unittest and the grammar.
303 date_string: the string to parse
306 A datetime.datetime representing the parsed date/time or
311 Parsed date expressions without any time part return
312 hours = minutes = seconds = microseconds = 0 (i.e. at
313 midnight that day). Parsed time expressions without any
314 date part default to date = today.
318 txt = '3 weeks before last tues at 9:15am'
321 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
323 # dt1 and dt2 will be identical other than the fact that
324 # the latter's tzinfo will be set to PST/PDT.
327 date_string = date_string.strip()
328 date_string = re.sub(r'\s+', ' ', date_string)
330 listener = RaisingErrorListener()
331 input_stream = antlr4.InputStream(date_string)
332 lexer = dateparse_utilsLexer(input_stream)
333 lexer.removeErrorListeners()
334 lexer.addErrorListener(listener)
335 stream = antlr4.CommonTokenStream(lexer)
336 parser = dateparse_utilsParser(stream)
337 parser.removeErrorListeners()
338 parser.addErrorListener(listener)
339 tree = parser.parse()
340 walker = antlr4.ParseTreeWalker()
341 walker.walk(self, tree)
344 def get_date(self) -> Optional[datetime.date]:
347 The date part of the last :meth:`parse` operation again
352 def get_time(self) -> Optional[datetime.time]:
355 The time part of the last :meth:`parse` operation again
360 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
361 """Get the datetime of the last :meth:`parse` operation again
365 tz: the timezone to set on output times. By default we
366 return timezone-naive datetime objects.
369 the same datetime that :meth:`parse` last did, optionally
370 overriding the timezone. Returns None of no calls to
371 :meth:`parse` have yet been made.
375 Parsed date expressions without any time part return
376 hours = minutes = seconds = microseconds = 0 (i.e. at
377 midnight that day). Parsed time expressions without any
378 date part default to date = today.
383 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
389 """Reset at init and between parses."""
390 if self.override_now_for_test_purposes is None:
391 self.now_datetime = datetime.datetime.now()
392 self.today = datetime.date.today()
394 self.now_datetime = self.override_now_for_test_purposes
395 self.today = datetime_to_date(self.override_now_for_test_purposes)
396 self.date: Optional[datetime.date] = None
397 self.time: Optional[datetime.time] = None
398 self.datetime: Optional[datetime.datetime] = None
399 self.context: Dict[str, Any] = {}
400 self.timedelta = datetime.timedelta(seconds=0)
401 self.saw_overt_year = False
404 def _normalize_special_day_name(name: str) -> str:
405 """String normalization / canonicalization for date expressions."""
407 name = name.replace("'", '')
408 name = name.replace('xmas', 'christmas')
409 name = name.replace('mlk', 'martin luther king')
410 name = name.replace(' ', '')
411 eve = 'eve' if name[-3:] == 'eve' else ''
412 name = name[:5] + eve
413 name = name.replace('washi', 'presi')
416 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
417 """Figure out what unit a date expression piece is talking about."""
419 return TimeUnit.MONTHS
420 txt = orig.lower()[:3]
421 if txt in self.day_name_to_number:
422 return TimeUnit(self.day_name_to_number[txt])
423 elif txt in self.delta_unit_to_constant:
424 return TimeUnit(self.delta_unit_to_constant[txt])
425 raise ParseException(f'Invalid date unit: {orig}')
427 def _figure_out_time_unit(self, orig: str) -> int:
428 """Figure out what unit a time expression piece is talking about."""
429 txt = orig.lower()[:3]
430 if txt in self.time_delta_unit_to_constant:
431 return self.time_delta_unit_to_constant[txt]
432 raise ParseException(f'Invalid time unit: {orig}')
434 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
435 """Parse what we think is a special date name and return its datetime
436 (or None if it can't be parsed).
439 year = self.context.get('year', today.year)
440 name = DateParser._normalize_special_day_name(self.context['special'])
442 # Yesterday, today, tomorrow -- ignore any next/last
443 if name in ('today', 'now'):
446 return today + datetime.timedelta(days=-1)
448 return today + datetime.timedelta(days=+1)
450 next_last = self.context.get('special_next_last', '')
451 if next_last == 'next':
453 self.saw_overt_year = True
454 elif next_last == 'last':
456 self.saw_overt_year = True
460 return dateutil.easter.easter(year=year)
461 elif name == 'hallo':
462 return datetime.date(year=year, month=10, day=31)
464 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
465 if 'Observed' not in holiday_name:
466 holiday_name = DateParser._normalize_special_day_name(holiday_name)
467 if name == holiday_name:
469 if name == 'chriseve':
470 return datetime.date(year=year, month=12, day=24)
471 elif name == 'newyeeve':
472 return datetime.date(year=year, month=12, day=31)
475 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
476 """Handle date expressions like "the ides of March" which require
477 both the "ides" and the month since the definition of the "ides"
478 changes based on the length of the month.
480 assert 'ide' in day or 'non' in day
481 assert month_number in self.typical_days_per_month
482 typical_days_per_month = self.typical_days_per_month[month_number]
485 if typical_days_per_month == 31:
486 if self.context['day'] == 'ide':
493 if self.context['day'] == 'ide':
498 def _parse_normal_date(self) -> datetime.date:
499 if 'dow' in self.context and 'month' not in self.context:
501 while d.weekday() != self.context['dow']:
502 d += datetime.timedelta(days=1)
505 if 'month' not in self.context:
506 raise ParseException('Missing month')
507 if 'day' not in self.context:
508 raise ParseException('Missing day')
509 if 'year' not in self.context:
510 self.context['year'] = self.today.year
511 self.saw_overt_year = False
513 self.saw_overt_year = True
515 # Handling "ides" and "nones" requires both the day and month.
516 if self.context['day'] == 'ide' or self.context['day'] == 'non':
517 self.context['day'] = self._resolve_ides_nones(
518 self.context['day'], self.context['month']
521 return datetime.date(
522 year=self.context['year'],
523 month=self.context['month'],
524 day=self.context['day'],
528 def _parse_tz(txt: str) -> Any:
534 tz1 = pytz.timezone(txt)
542 tz2 = dateutil.tz.gettz(txt)
548 # Try constructing an offset in seconds
551 if txt_sign in ('-', '+'):
552 sign = +1 if txt_sign == '+' else -1
554 minute = int(txt[-2:])
555 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
556 tzoffset = dateutil.tz.tzoffset(txt, offset)
563 def _get_int(txt: str) -> int:
564 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
566 while not txt[-1].isdigit():
570 # -- overridden methods invoked by parse walk. Note: not part of the class'
573 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
576 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
579 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
580 """Populate self.datetime."""
581 if self.date is None:
582 self.date = self.today
583 year = self.date.year
584 month = self.date.month
587 if self.time is None:
588 self.time = datetime.time(0, 0, 0)
589 hour = self.time.hour
590 minute = self.time.minute
591 second = self.time.second
592 micros = self.time.microsecond
594 self.datetime = datetime.datetime(
602 tzinfo=self.time.tzinfo,
605 # Apply resudual adjustments to times here when we have a
607 self.datetime = self.datetime + self.timedelta
608 assert self.datetime is not None
609 self.time = datetime.time(
611 self.datetime.minute,
612 self.datetime.second,
613 self.datetime.microsecond,
614 self.datetime.tzinfo,
617 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
619 if ctx.singleDateExpr() is not None:
620 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
621 elif ctx.baseAndOffsetDateExpr() is not None:
622 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
624 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
626 if ctx.singleTimeExpr() is not None:
627 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
628 elif ctx.baseAndOffsetTimeExpr() is not None:
629 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
631 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
632 """When we leave the date expression, populate self.date."""
633 if 'special' in self.context:
634 self.date = self._parse_special_date(self.context['special'])
636 self.date = self._parse_normal_date()
637 assert self.date is not None
639 # For a single date, just return the date we pulled out.
640 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
643 # Otherwise treat self.date as a base date that we're modifying
645 if 'delta_int' not in self.context:
646 raise ParseException('Missing delta_int?!')
647 count = self.context['delta_int']
651 # Adjust count's sign based on the presence of 'before' or 'after'.
652 if 'delta_before_after' in self.context:
653 before_after = self.context['delta_before_after'].lower()
654 if before_after in ('before', 'until', 'til', 'to'):
657 # What are we counting units of?
658 if 'delta_unit' not in self.context:
659 raise ParseException('Missing delta_unit?!')
660 unit = self.context['delta_unit']
661 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
662 self.date = datetime_to_date(dt)
664 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
666 self.time = datetime.time(
667 self.context['hour'],
668 self.context['minute'],
669 self.context['seconds'],
670 self.context['micros'],
671 tzinfo=self.context.get('tz', None),
673 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
676 # If we get here there (should be) a relative adjustment to
678 if 'nth' in self.context:
679 count = self.context['nth']
680 elif 'time_delta_int' in self.context:
681 count = self.context['time_delta_int']
683 raise ParseException('Missing delta in relative time.')
687 # Adjust count's sign based on the presence of 'before' or 'after'.
688 if 'time_delta_before_after' in self.context:
689 before_after = self.context['time_delta_before_after'].lower()
690 if before_after in ('before', 'until', 'til', 'to'):
693 # What are we counting units of... assume minutes.
694 if 'time_delta_unit' not in self.context:
695 self.timedelta += datetime.timedelta(minutes=count)
697 unit = self.context['time_delta_unit']
698 if unit == TimeUnit.SECONDS:
699 self.timedelta += datetime.timedelta(seconds=count)
700 elif unit == TimeUnit.MINUTES:
701 self.timedelta = datetime.timedelta(minutes=count)
702 elif unit == TimeUnit.HOURS:
703 self.timedelta = datetime.timedelta(hours=count)
705 raise ParseException(f'Invalid Unit: "{unit}"')
707 def exitDeltaPlusMinusExpr(
708 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
713 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
715 n = DateParser._get_int(n)
716 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
717 except Exception as e:
718 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
720 self.context['delta_int'] = n
721 self.context['delta_unit'] = unit
723 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
725 unit = self._figure_out_date_unit(ctx.getText().lower())
726 except Exception as e:
727 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
729 self.context['delta_unit'] = unit
731 def exitDeltaNextLast(
732 self, ctx: dateparse_utilsParser.DeltaNextLastContext
735 txt = ctx.getText().lower()
736 except Exception as e:
737 raise ParseException(f'Bad next/last: {ctx.getText()}') from e
738 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
739 raise ParseException(
740 'Next/last expression expected to be relative to today.'
742 if txt[:4] == 'next':
743 self.context['delta_int'] = +1
744 self.context['day'] = self.now_datetime.day
745 self.context['month'] = self.now_datetime.month
746 self.context['year'] = self.now_datetime.year
747 self.saw_overt_year = True
748 elif txt[:4] == 'last':
749 self.context['delta_int'] = -1
750 self.context['day'] = self.now_datetime.day
751 self.context['month'] = self.now_datetime.month
752 self.context['year'] = self.now_datetime.year
753 self.saw_overt_year = True
755 raise ParseException(f'Bad next/last: {ctx.getText()}')
757 def exitCountUnitsBeforeAfterTimeExpr(
758 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
760 if 'nth' not in self.context:
761 raise ParseException(f'Bad count expression: {ctx.getText()}')
763 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
764 self.context['time_delta_unit'] = unit
765 except Exception as e:
766 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
767 if 'time_delta_before_after' not in self.context:
768 raise ParseException(f'Bad Before/After: {ctx.getText()}')
770 def exitDeltaTimeFraction(
771 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
774 txt = ctx.getText().lower()[:4]
776 self.context['time_delta_int'] = 15
777 self.context['time_delta_unit'] = TimeUnit.MINUTES
779 self.context['time_delta_int'] = 30
780 self.context['time_delta_unit'] = TimeUnit.MINUTES
782 raise ParseException(f'Bad time fraction {ctx.getText()}')
783 except Exception as e:
784 raise ParseException(f'Bad time fraction {ctx.getText()}') from e
786 def exitDeltaBeforeAfter(
787 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
790 txt = ctx.getText().lower()
791 except Exception as e:
792 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
794 self.context['delta_before_after'] = txt
796 def exitDeltaTimeBeforeAfter(
797 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
800 txt = ctx.getText().lower()
801 except Exception as e:
802 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
804 self.context['time_delta_before_after'] = txt
806 def exitNthWeekdayInMonthMaybeYearExpr(
807 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
809 """Do a bunch of work to convert expressions like...
811 'the 2nd Friday of June' -and-
812 'the last Wednesday in October'
814 ...into base + offset expressions instead.
817 if 'nth' not in self.context:
818 raise ParseException(f'Missing nth number: {ctx.getText()}')
819 n = self.context['nth']
820 if n < 1 or n > 5: # months never have more than 5 Foodays
822 raise ParseException(f'Invalid nth number: {ctx.getText()}')
823 del self.context['nth']
824 self.context['delta_int'] = n
826 year = self.context.get('year', self.today.year)
827 if 'month' not in self.context:
828 raise ParseException(f'Missing month expression: {ctx.getText()}')
829 month = self.context['month']
831 dow = self.context['dow']
832 del self.context['dow']
833 self.context['delta_unit'] = dow
835 # For the nth Fooday in Month, start at the last day of
836 # the previous month count ahead N Foodays. For the last
837 # Fooday in Month, start at the last of the month and
838 # count back one Fooday.
844 tmp_date = datetime.date(year=year, month=month, day=1)
845 tmp_date = tmp_date - datetime.timedelta(days=1)
847 # The delta adjustment code can handle the case where
848 # the last day of the month is the day we're looking
851 tmp_date = datetime.date(year=year, month=month, day=1)
852 tmp_date = tmp_date - datetime.timedelta(days=1)
854 self.context['year'] = tmp_date.year
855 self.context['month'] = tmp_date.month
856 self.context['day'] = tmp_date.day
857 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
858 except Exception as e:
859 raise ParseException(
860 f'Invalid nthWeekday expression: {ctx.getText()}'
863 def exitFirstLastWeekdayInMonthMaybeYearExpr(
865 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
867 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
869 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
871 i = DateParser._get_int(ctx.getText())
872 except Exception as e:
873 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
875 self.context['nth'] = i
877 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
885 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
886 except Exception as e:
887 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
889 self.context['nth'] = txt
891 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
893 dow = ctx.getText().lower()[:3]
894 dow = self.day_name_to_number.get(dow, None)
895 except Exception as e:
896 raise ParseException('Bad day of week') from e
898 self.context['dow'] = dow
900 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
902 day = ctx.getText().lower()
904 self.context['day'] = 'ide'
907 self.context['day'] = 'non'
910 self.context['day'] = 1
912 day = DateParser._get_int(day)
913 if day < 1 or day > 31:
914 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
915 except Exception as e:
916 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
917 self.context['day'] = day
919 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
921 month = ctx.getText()
922 while month[0] == '/' or month[0] == '-':
924 month = month[:3].lower()
925 month = self.month_name_to_number.get(month, None)
927 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
928 except Exception as e:
929 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
931 self.context['month'] = month
933 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
935 month = DateParser._get_int(ctx.getText())
936 if month < 1 or month > 12:
937 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
938 except Exception as e:
939 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
941 self.context['month'] = month
943 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
945 year = DateParser._get_int(ctx.getText())
947 raise ParseException(f'Bad year expression: {ctx.getText()}')
948 except Exception as e:
949 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
951 self.saw_overt_year = True
952 self.context['year'] = year
954 def exitSpecialDateMaybeYearExpr(
955 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
958 special = ctx.specialDate().getText().lower()
959 self.context['special'] = special
960 except Exception as e:
961 raise ParseException(
962 f'Bad specialDate expression: {ctx.specialDate().getText()}'
965 mod = ctx.thisNextLast()
967 if mod.THIS() is not None:
968 self.context['special_next_last'] = 'this'
969 elif mod.NEXT() is not None:
970 self.context['special_next_last'] = 'next'
971 elif mod.LAST() is not None:
972 self.context['special_next_last'] = 'last'
973 except Exception as e:
974 raise ParseException(
975 f'Bad specialDateNextLast expression: {ctx.getText()}'
978 def exitNFoosFromTodayAgoExpr(
979 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
981 d = self.now_datetime
983 count = DateParser._get_int(ctx.unsignedInt().getText())
984 unit = ctx.deltaUnit().getText().lower()
985 ago_from_now = ctx.AGO_FROM_NOW().getText()
986 except Exception as e:
987 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
989 if "ago" in ago_from_now or "back" in ago_from_now:
992 unit = self._figure_out_date_unit(unit)
993 d = n_timeunits_from_base(count, TimeUnit(unit), d)
994 self.context['year'] = d.year
995 self.context['month'] = d.month
996 self.context['day'] = d.day
998 def exitDeltaRelativeToTodayExpr(
999 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
1001 # When someone says "next week" they mean a week from now.
1002 # Likewise next month or last year. These expressions are now
1005 # But when someone says "this Friday" they mean "this coming
1006 # Friday". It would be weird to say "this Friday" if today
1007 # was already Friday but I'm parsing it to mean: the next day
1008 # that is a Friday. So when you say "next Friday" you mean
1009 # the Friday after this coming Friday, or 2 Fridays from now.
1011 # This set handles this weirdness.
1016 TimeUnit.WEDNESDAYS,
1023 d = self.now_datetime
1025 mod = ctx.thisNextLast()
1026 unit = ctx.deltaUnit().getText().lower()
1027 unit = self._figure_out_date_unit(unit)
1031 if unit in weekdays:
1036 if unit in weekdays:
1041 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
1042 except Exception as e:
1043 raise ParseException(
1044 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
1046 d = n_timeunits_from_base(count, TimeUnit(unit), d)
1047 self.context['year'] = d.year
1048 self.context['month'] = d.month
1049 self.context['day'] = d.day
1051 def exitSpecialTimeExpr(
1052 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
1055 txt = ctx.specialTime().getText().lower()
1056 except Exception as e:
1057 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
1059 if txt in ('noon', 'midday'):
1060 self.context['hour'] = 12
1061 self.context['minute'] = 0
1062 self.context['seconds'] = 0
1063 self.context['micros'] = 0
1064 elif txt == 'midnight':
1065 self.context['hour'] = 0
1066 self.context['minute'] = 0
1067 self.context['seconds'] = 0
1068 self.context['micros'] = 0
1070 raise ParseException(f'Bad special time expression: {txt}')
1073 tz = ctx.tzExpr().getText()
1074 self.context['tz'] = DateParser._parse_tz(tz)
1078 def exitTwelveHourTimeExpr(
1079 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
1082 hour = ctx.hour().getText()
1083 while not hour[-1].isdigit():
1085 hour = DateParser._get_int(hour)
1086 except Exception as e:
1087 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1088 if hour <= 0 or hour > 12:
1089 raise ParseException(f'Bad hour (out of range): {hour}')
1092 minute = DateParser._get_int(ctx.minute().getText())
1095 if minute < 0 or minute > 59:
1096 raise ParseException(f'Bad minute (out of range): {minute}')
1097 self.context['minute'] = minute
1100 seconds = DateParser._get_int(ctx.second().getText())
1103 if seconds < 0 or seconds > 59:
1104 raise ParseException(f'Bad second (out of range): {seconds}')
1105 self.context['seconds'] = seconds
1108 micros = DateParser._get_int(ctx.micros().getText())
1111 if micros < 0 or micros > 1000000:
1112 raise ParseException(f'Bad micros (out of range): {micros}')
1113 self.context['micros'] = micros
1116 ampm = ctx.ampm().getText()
1117 except Exception as e:
1118 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
1123 self.context['hour'] = hour
1126 tz = ctx.tzExpr().getText()
1127 self.context['tz'] = DateParser._parse_tz(tz)
1131 def exitTwentyFourHourTimeExpr(
1132 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1135 hour = ctx.hour().getText()
1136 while not hour[-1].isdigit():
1138 hour = DateParser._get_int(hour)
1139 except Exception as e:
1140 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1141 if hour < 0 or hour > 23:
1142 raise ParseException(f'Bad hour (out of range): {hour}')
1143 self.context['hour'] = hour
1146 minute = DateParser._get_int(ctx.minute().getText())
1149 if minute < 0 or minute > 59:
1150 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1151 self.context['minute'] = minute
1154 seconds = DateParser._get_int(ctx.second().getText())
1157 if seconds < 0 or seconds > 59:
1158 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1159 self.context['seconds'] = seconds
1162 micros = DateParser._get_int(ctx.micros().getText())
1165 if micros < 0 or micros >= 1000000:
1166 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1167 self.context['micros'] = micros
1170 tz = ctx.tzExpr().getText()
1171 self.context['tz'] = DateParser._parse_tz(tz)
1176 @bootstrap.initialize
1178 parser = DateParser()
1179 for line in sys.stdin:
1181 line = re.sub(r"#.*$", "", line)
1182 if re.match(r"^ *$", line) is not None:
1185 dt = parser.parse(line)
1186 except Exception as e:
1188 print("Unrecognized.")
1190 assert dt is not None
1191 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1195 if __name__ == "__main__":