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/datetimes/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/datetimes/dateparse_utils.g4;hb=HEAD>`_ for more details.
100 from typing import Any, Callable, Dict, Optional
102 import antlr4 # type: ignore
103 import holidays # type: ignore
106 from pyutils import bootstrap, decorator_utils
107 from pyutils.datetimes.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
108 from pyutils.datetimes.dateparse_utilsListener import (
109 dateparse_utilsListener,
111 from pyutils.datetimes.dateparse_utilsParser import (
112 dateparse_utilsParser,
114 from pyutils.datetimes.datetime_utils import (
119 n_timeunits_from_base,
121 from pyutils.security import acl
123 logger = logging.getLogger(__name__)
126 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
127 @functools.wraps(enter_or_exit_f)
128 def debug_parse_wrapper(*args, **kwargs):
134 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
136 for c in ctx.getChildren():
137 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
138 retval = enter_or_exit_f(*args, **kwargs)
141 return debug_parse_wrapper
144 class ParseException(Exception):
145 """An exception thrown during parsing because of unrecognized input."""
147 def __init__(self, message: str) -> None:
150 message: parse error message description.
153 self.message = message
156 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
157 """An error listener that raises ParseExceptions."""
159 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
160 raise ParseException(msg)
163 self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs
167 def reportAttemptingFullContext(
168 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
172 def reportContextSensitivity(
173 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
178 @decorator_utils.decorate_matching_methods_with(
180 acl=acl.StringWildcardBasedACL(
185 denied_patterns=['enterEveryRule', 'exitEveryRule'],
186 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
187 default_answer=False,
190 class DateParser(dateparse_utilsListener):
191 """A class to parse dates expressed in human language (English).
195 d.parse('next wednesday')
196 dt = d.get_datetime()
198 Wednesday 2022/10/26 00:00:00.000000
200 Note that the interface is somewhat klunky here because this class is
201 conforming to interfaces auto-generated by ANTLR as it parses the grammar.
202 See also :meth:`pyutils.string_utils.to_date`.
206 PARSE_TYPE_SINGLE_DATE_EXPR = 1
207 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
208 PARSE_TYPE_SINGLE_TIME_EXPR = 3
209 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
211 def __init__(self, *, override_now_for_test_purposes=None) -> None:
212 """Construct a parser.
215 override_now_for_test_purposes: passing a value to
216 override_now_for_test_purposes can be used to force
217 this parser instance to use a custom date/time for its
218 idea of "now" so that the code can be more easily
219 unittested. Leave as None for real use cases.
221 self.month_name_to_number = {
236 # Used only for ides/nones. Month length on a non-leap year.
237 self.typical_days_per_month = {
252 # N.B. day number is also synched with datetime_utils.TimeUnit values
253 # which allows expressions like "3 wednesdays from now" to work.
254 self.day_name_to_number = {
264 # These TimeUnits are defined in datetime_utils and are used as params
265 # to datetime_utils.n_timeunits_from_base.
266 self.time_delta_unit_to_constant = {
267 'hou': TimeUnit.HOURS,
268 'min': TimeUnit.MINUTES,
269 'sec': TimeUnit.SECONDS,
271 self.delta_unit_to_constant = {
272 'day': TimeUnit.DAYS,
273 'wor': TimeUnit.WORKDAYS,
274 'wee': TimeUnit.WEEKS,
275 'mon': TimeUnit.MONTHS,
276 'yea': TimeUnit.YEARS,
278 self.override_now_for_test_purposes = override_now_for_test_purposes
280 # Note: _reset defines several class fields. It is used both here
281 # in the c'tor but also in between parse operations to restore the
282 # class' state and allow it to be reused.
286 def parse(self, date_string: str) -> Optional[datetime.datetime]:
288 Parse a ~free form date/time expression and return a
289 timezone agnostic datetime on success. Also sets
290 `self.datetime`, `self.date` and `self.time` which can each be
291 accessed other methods on the class: :meth:`get_datetime`,
292 :meth:`get_date` and :meth:`get_time`. Raises a
293 `ParseException` with a helpful(?) message on parse error or
296 This is the main entrypoint to this class for caller code.
298 To get an idea of what expressions can be parsed, check out
299 the unittest and the grammar.
302 date_string: the string to parse
305 A datetime.datetime representing the parsed date/time or
310 Parsed date expressions without any time part return
311 hours = minutes = seconds = microseconds = 0 (i.e. at
312 midnight that day). Parsed time expressions without any
313 date part default to date = today.
317 txt = '3 weeks before last tues at 9:15am'
320 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
322 # dt1 and dt2 will be identical other than the fact that
323 # the latter's tzinfo will be set to PST/PDT.
326 date_string = date_string.strip()
327 date_string = re.sub(r'\s+', ' ', date_string)
329 listener = RaisingErrorListener()
330 input_stream = antlr4.InputStream(date_string)
331 lexer = dateparse_utilsLexer(input_stream)
332 lexer.removeErrorListeners()
333 lexer.addErrorListener(listener)
334 stream = antlr4.CommonTokenStream(lexer)
335 parser = dateparse_utilsParser(stream)
336 parser.removeErrorListeners()
337 parser.addErrorListener(listener)
338 tree = parser.parse()
339 walker = antlr4.ParseTreeWalker()
340 walker.walk(self, tree)
343 def get_date(self) -> Optional[datetime.date]:
346 The date part of the last :meth:`parse` operation again
351 def get_time(self) -> Optional[datetime.time]:
354 The time part of the last :meth:`parse` operation again
359 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
360 """Get the datetime of the last :meth:`parse` operation again
364 tz: the timezone to set on output times. By default we
365 return timezone-naive datetime objects.
368 the same datetime that :meth:`parse` last did, optionally
369 overriding the timezone. Returns None of no calls to
370 :meth:`parse` have yet been made.
374 Parsed date expressions without any time part return
375 hours = minutes = seconds = microseconds = 0 (i.e. at
376 midnight that day). Parsed time expressions without any
377 date part default to date = today.
382 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
388 """Reset at init and between parses."""
389 if self.override_now_for_test_purposes is None:
390 self.now_datetime = datetime.datetime.now()
391 self.today = datetime.date.today()
393 self.now_datetime = self.override_now_for_test_purposes
394 self.today = datetime_to_date(self.override_now_for_test_purposes)
395 self.date: Optional[datetime.date] = None
396 self.time: Optional[datetime.time] = None
397 self.datetime: Optional[datetime.datetime] = None
398 self.context: Dict[str, Any] = {}
399 self.timedelta = datetime.timedelta(seconds=0)
400 self.saw_overt_year = False
403 def _normalize_special_day_name(name: str) -> str:
404 """String normalization / canonicalization for date expressions."""
406 name = name.replace("'", '')
407 name = name.replace('xmas', 'christmas')
408 name = name.replace('mlk', 'martin luther king')
409 name = name.replace(' ', '')
410 eve = 'eve' if name[-3:] == 'eve' else ''
411 name = name[:5] + eve
412 name = name.replace('washi', 'presi')
415 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
416 """Figure out what unit a date expression piece is talking about."""
418 return TimeUnit.MONTHS
419 txt = orig.lower()[:3]
420 if txt in self.day_name_to_number:
421 return TimeUnit(self.day_name_to_number[txt])
422 elif txt in self.delta_unit_to_constant:
423 return TimeUnit(self.delta_unit_to_constant[txt])
424 raise ParseException(f'Invalid date unit: {orig}')
426 def _figure_out_time_unit(self, orig: str) -> int:
427 """Figure out what unit a time expression piece is talking about."""
428 txt = orig.lower()[:3]
429 if txt in self.time_delta_unit_to_constant:
430 return self.time_delta_unit_to_constant[txt]
431 raise ParseException(f'Invalid time unit: {orig}')
433 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
434 """Parse what we think is a special date name and return its datetime
435 (or None if it can't be parsed).
438 year = self.context.get('year', today.year)
439 name = DateParser._normalize_special_day_name(self.context['special'])
441 # Yesterday, today, tomorrow -- ignore any next/last
442 if name in ('today', 'now'):
445 return today + datetime.timedelta(days=-1)
447 return today + datetime.timedelta(days=+1)
449 next_last = self.context.get('special_next_last', '')
450 if next_last == 'next':
452 self.saw_overt_year = True
453 elif next_last == 'last':
455 self.saw_overt_year = True
459 return easter(year=year)
460 elif name == 'hallo':
461 return datetime.date(year=year, month=10, day=31)
463 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
464 if 'Observed' not in holiday_name:
465 holiday_name = DateParser._normalize_special_day_name(holiday_name)
466 if name == holiday_name:
468 if name == 'chriseve':
469 return datetime.date(year=year, month=12, day=24)
470 elif name == 'newyeeve':
471 return datetime.date(year=year, month=12, day=31)
474 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
475 """Handle date expressions like "the ides of March" which require
476 both the "ides" and the month since the definition of the "ides"
477 changes based on the length of the month.
479 assert 'ide' in day or 'non' in day
480 assert month_number in self.typical_days_per_month
481 typical_days_per_month = self.typical_days_per_month[month_number]
484 if typical_days_per_month == 31:
485 if self.context['day'] == 'ide':
492 if self.context['day'] == 'ide':
497 def _parse_normal_date(self) -> datetime.date:
498 if 'dow' in self.context and 'month' not in self.context:
500 while d.weekday() != self.context['dow']:
501 d += datetime.timedelta(days=1)
504 if 'month' not in self.context:
505 raise ParseException('Missing month')
506 if 'day' not in self.context:
507 raise ParseException('Missing day')
508 if 'year' not in self.context:
509 self.context['year'] = self.today.year
510 self.saw_overt_year = False
512 self.saw_overt_year = True
514 # Handling "ides" and "nones" requires both the day and month.
515 if self.context['day'] == 'ide' or self.context['day'] == 'non':
516 self.context['day'] = self._resolve_ides_nones(
517 self.context['day'], self.context['month']
520 return datetime.date(
521 year=self.context['year'],
522 month=self.context['month'],
523 day=self.context['day'],
527 def _parse_tz(txt: str) -> Any:
533 tz1 = pytz.timezone(txt)
539 # Try constructing an offset in seconds
542 if txt_sign in ('-', '+'):
543 sign = +1 if txt_sign == '+' else -1
545 minute = int(txt[-2:])
546 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
547 tzoffset = datetime.timezone(datetime.timedelta(seconds=offset))
554 def _get_int(txt: str) -> int:
555 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
557 while not txt[-1].isdigit():
561 # -- overridden methods invoked by parse walk. Note: not part of the class'
564 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
567 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
570 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
571 """Populate self.datetime."""
572 if self.date is None:
573 self.date = self.today
574 year = self.date.year
575 month = self.date.month
578 if self.time is None:
579 self.time = datetime.time(0, 0, 0)
580 hour = self.time.hour
581 minute = self.time.minute
582 second = self.time.second
583 micros = self.time.microsecond
585 self.datetime = datetime.datetime(
593 tzinfo=self.time.tzinfo,
596 # Apply resudual adjustments to times here when we have a
598 self.datetime = self.datetime + self.timedelta
599 assert self.datetime is not None
600 self.time = datetime.time(
602 self.datetime.minute,
603 self.datetime.second,
604 self.datetime.microsecond,
605 self.datetime.tzinfo,
608 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
610 if ctx.singleDateExpr() is not None:
611 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
612 elif ctx.baseAndOffsetDateExpr() is not None:
613 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
615 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
617 if ctx.singleTimeExpr() is not None:
618 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
619 elif ctx.baseAndOffsetTimeExpr() is not None:
620 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
622 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
623 """When we leave the date expression, populate self.date."""
624 if 'special' in self.context:
625 self.date = self._parse_special_date(self.context['special'])
627 self.date = self._parse_normal_date()
628 assert self.date is not None
630 # For a single date, just return the date we pulled out.
631 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
634 # Otherwise treat self.date as a base date that we're modifying
636 if 'delta_int' not in self.context:
637 raise ParseException('Missing delta_int?!')
638 count = self.context['delta_int']
642 # Adjust count's sign based on the presence of 'before' or 'after'.
643 if 'delta_before_after' in self.context:
644 before_after = self.context['delta_before_after'].lower()
645 if before_after in ('before', 'until', 'til', 'to'):
648 # What are we counting units of?
649 if 'delta_unit' not in self.context:
650 raise ParseException('Missing delta_unit?!')
651 unit = self.context['delta_unit']
652 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
653 self.date = datetime_to_date(dt)
655 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
657 self.time = datetime.time(
658 self.context['hour'],
659 self.context['minute'],
660 self.context['seconds'],
661 self.context['micros'],
662 tzinfo=self.context.get('tz', None),
664 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
667 # If we get here there (should be) a relative adjustment to
669 if 'nth' in self.context:
670 count = self.context['nth']
671 elif 'time_delta_int' in self.context:
672 count = self.context['time_delta_int']
674 raise ParseException('Missing delta in relative time.')
678 # Adjust count's sign based on the presence of 'before' or 'after'.
679 if 'time_delta_before_after' in self.context:
680 before_after = self.context['time_delta_before_after'].lower()
681 if before_after in ('before', 'until', 'til', 'to'):
684 # What are we counting units of... assume minutes.
685 if 'time_delta_unit' not in self.context:
686 self.timedelta += datetime.timedelta(minutes=count)
688 unit = self.context['time_delta_unit']
689 if unit == TimeUnit.SECONDS:
690 self.timedelta += datetime.timedelta(seconds=count)
691 elif unit == TimeUnit.MINUTES:
692 self.timedelta = datetime.timedelta(minutes=count)
693 elif unit == TimeUnit.HOURS:
694 self.timedelta = datetime.timedelta(hours=count)
696 raise ParseException(f'Invalid Unit: "{unit}"')
698 def exitDeltaPlusMinusExpr(
699 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
704 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
706 n = DateParser._get_int(n)
707 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
708 except Exception as e:
709 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
711 self.context['delta_int'] = n
712 self.context['delta_unit'] = unit
714 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
716 unit = self._figure_out_date_unit(ctx.getText().lower())
717 except Exception as e:
718 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
720 self.context['delta_unit'] = unit
722 def exitDeltaNextLast(
723 self, ctx: dateparse_utilsParser.DeltaNextLastContext
726 txt = ctx.getText().lower()
727 except Exception as e:
728 raise ParseException(f'Bad next/last: {ctx.getText()}') from e
729 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
730 raise ParseException(
731 'Next/last expression expected to be relative to today.'
733 if txt[:4] == 'next':
734 self.context['delta_int'] = +1
735 self.context['day'] = self.now_datetime.day
736 self.context['month'] = self.now_datetime.month
737 self.context['year'] = self.now_datetime.year
738 self.saw_overt_year = True
739 elif txt[:4] == 'last':
740 self.context['delta_int'] = -1
741 self.context['day'] = self.now_datetime.day
742 self.context['month'] = self.now_datetime.month
743 self.context['year'] = self.now_datetime.year
744 self.saw_overt_year = True
746 raise ParseException(f'Bad next/last: {ctx.getText()}')
748 def exitCountUnitsBeforeAfterTimeExpr(
749 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
751 if 'nth' not in self.context:
752 raise ParseException(f'Bad count expression: {ctx.getText()}')
754 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
755 self.context['time_delta_unit'] = unit
756 except Exception as e:
757 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
758 if 'time_delta_before_after' not in self.context:
759 raise ParseException(f'Bad Before/After: {ctx.getText()}')
761 def exitDeltaTimeFraction(
762 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
765 txt = ctx.getText().lower()[:4]
767 self.context['time_delta_int'] = 15
768 self.context['time_delta_unit'] = TimeUnit.MINUTES
770 self.context['time_delta_int'] = 30
771 self.context['time_delta_unit'] = TimeUnit.MINUTES
773 raise ParseException(f'Bad time fraction {ctx.getText()}')
774 except Exception as e:
775 raise ParseException(f'Bad time fraction {ctx.getText()}') from e
777 def exitDeltaBeforeAfter(
778 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
781 txt = ctx.getText().lower()
782 except Exception as e:
783 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
785 self.context['delta_before_after'] = txt
787 def exitDeltaTimeBeforeAfter(
788 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
791 txt = ctx.getText().lower()
792 except Exception as e:
793 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
795 self.context['time_delta_before_after'] = txt
797 def exitNthWeekdayInMonthMaybeYearExpr(
798 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
800 """Do a bunch of work to convert expressions like...
802 'the 2nd Friday of June' -and-
803 'the last Wednesday in October'
805 ...into base + offset expressions instead.
808 if 'nth' not in self.context:
809 raise ParseException(f'Missing nth number: {ctx.getText()}')
810 n = self.context['nth']
811 if n < 1 or n > 5: # months never have more than 5 Foodays
813 raise ParseException(f'Invalid nth number: {ctx.getText()}')
814 del self.context['nth']
815 self.context['delta_int'] = n
817 year = self.context.get('year', self.today.year)
818 if 'month' not in self.context:
819 raise ParseException(f'Missing month expression: {ctx.getText()}')
820 month = self.context['month']
822 dow = self.context['dow']
823 del self.context['dow']
824 self.context['delta_unit'] = dow
826 # For the nth Fooday in Month, start at the last day of
827 # the previous month count ahead N Foodays. For the last
828 # Fooday in Month, start at the last of the month and
829 # count back one Fooday.
835 tmp_date = datetime.date(year=year, month=month, day=1)
836 tmp_date = tmp_date - datetime.timedelta(days=1)
838 # The delta adjustment code can handle the case where
839 # the last day of the month is the day we're looking
842 tmp_date = datetime.date(year=year, month=month, day=1)
843 tmp_date = tmp_date - datetime.timedelta(days=1)
845 self.context['year'] = tmp_date.year
846 self.context['month'] = tmp_date.month
847 self.context['day'] = tmp_date.day
848 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
849 except Exception as e:
850 raise ParseException(
851 f'Invalid nthWeekday expression: {ctx.getText()}'
854 def exitFirstLastWeekdayInMonthMaybeYearExpr(
856 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
858 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
860 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
862 i = DateParser._get_int(ctx.getText())
863 except Exception as e:
864 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
866 self.context['nth'] = i
868 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
876 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
877 except Exception as e:
878 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
880 self.context['nth'] = txt
882 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
884 dow = ctx.getText().lower()[:3]
885 dow = self.day_name_to_number.get(dow, None)
886 except Exception as e:
887 raise ParseException('Bad day of week') from e
889 self.context['dow'] = dow
891 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
893 day = ctx.getText().lower()
895 self.context['day'] = 'ide'
898 self.context['day'] = 'non'
901 self.context['day'] = 1
903 day = DateParser._get_int(day)
904 if day < 1 or day > 31:
905 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
906 except Exception as e:
907 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
908 self.context['day'] = day
910 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
912 month = ctx.getText()
913 while month[0] == '/' or month[0] == '-':
915 month = month[:3].lower()
916 month = self.month_name_to_number.get(month, None)
918 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
919 except Exception as e:
920 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
922 self.context['month'] = month
924 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
926 month = DateParser._get_int(ctx.getText())
927 if month < 1 or month > 12:
928 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
929 except Exception as e:
930 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
932 self.context['month'] = month
934 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
936 year = DateParser._get_int(ctx.getText())
938 raise ParseException(f'Bad year expression: {ctx.getText()}')
939 except Exception as e:
940 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
942 self.saw_overt_year = True
943 self.context['year'] = year
945 def exitSpecialDateMaybeYearExpr(
946 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
949 special = ctx.specialDate().getText().lower()
950 self.context['special'] = special
951 except Exception as e:
952 raise ParseException(
953 f'Bad specialDate expression: {ctx.specialDate().getText()}'
956 mod = ctx.thisNextLast()
958 if mod.THIS() is not None:
959 self.context['special_next_last'] = 'this'
960 elif mod.NEXT() is not None:
961 self.context['special_next_last'] = 'next'
962 elif mod.LAST() is not None:
963 self.context['special_next_last'] = 'last'
964 except Exception as e:
965 raise ParseException(
966 f'Bad specialDateNextLast expression: {ctx.getText()}'
969 def exitNFoosFromTodayAgoExpr(
970 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
972 d = self.now_datetime
974 count = DateParser._get_int(ctx.unsignedInt().getText())
975 unit = ctx.deltaUnit().getText().lower()
976 ago_from_now = ctx.AGO_FROM_NOW().getText()
977 except Exception as e:
978 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
980 if "ago" in ago_from_now or "back" in ago_from_now:
983 unit = self._figure_out_date_unit(unit)
984 d = n_timeunits_from_base(count, TimeUnit(unit), d)
985 self.context['year'] = d.year
986 self.context['month'] = d.month
987 self.context['day'] = d.day
989 def exitDeltaRelativeToTodayExpr(
990 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
992 # When someone says "next week" they mean a week from now.
993 # Likewise next month or last year. These expressions are now
996 # But when someone says "this Friday" they mean "this coming
997 # Friday". It would be weird to say "this Friday" if today
998 # was already Friday but I'm parsing it to mean: the next day
999 # that is a Friday. So when you say "next Friday" you mean
1000 # the Friday after this coming Friday, or 2 Fridays from now.
1002 # This set handles this weirdness.
1007 TimeUnit.WEDNESDAYS,
1014 d = self.now_datetime
1016 mod = ctx.thisNextLast()
1017 unit = ctx.deltaUnit().getText().lower()
1018 unit = self._figure_out_date_unit(unit)
1022 if unit in weekdays:
1027 if unit in weekdays:
1032 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
1033 except Exception as e:
1034 raise ParseException(
1035 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
1037 d = n_timeunits_from_base(count, TimeUnit(unit), d)
1038 self.context['year'] = d.year
1039 self.context['month'] = d.month
1040 self.context['day'] = d.day
1042 def exitSpecialTimeExpr(
1043 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
1046 txt = ctx.specialTime().getText().lower()
1047 except Exception as e:
1048 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
1050 if txt in ('noon', 'midday'):
1051 self.context['hour'] = 12
1052 self.context['minute'] = 0
1053 self.context['seconds'] = 0
1054 self.context['micros'] = 0
1055 elif txt == 'midnight':
1056 self.context['hour'] = 0
1057 self.context['minute'] = 0
1058 self.context['seconds'] = 0
1059 self.context['micros'] = 0
1061 raise ParseException(f'Bad special time expression: {txt}')
1064 tz = ctx.tzExpr().getText()
1065 self.context['tz'] = DateParser._parse_tz(tz)
1069 def exitTwelveHourTimeExpr(
1070 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
1073 hour = ctx.hour().getText()
1074 while not hour[-1].isdigit():
1076 hour = DateParser._get_int(hour)
1077 except Exception as e:
1078 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1079 if hour <= 0 or hour > 12:
1080 raise ParseException(f'Bad hour (out of range): {hour}')
1083 minute = DateParser._get_int(ctx.minute().getText())
1086 if minute < 0 or minute > 59:
1087 raise ParseException(f'Bad minute (out of range): {minute}')
1088 self.context['minute'] = minute
1091 seconds = DateParser._get_int(ctx.second().getText())
1094 if seconds < 0 or seconds > 59:
1095 raise ParseException(f'Bad second (out of range): {seconds}')
1096 self.context['seconds'] = seconds
1099 micros = DateParser._get_int(ctx.micros().getText())
1102 if micros < 0 or micros > 1000000:
1103 raise ParseException(f'Bad micros (out of range): {micros}')
1104 self.context['micros'] = micros
1107 ampm = ctx.ampm().getText()
1108 except Exception as e:
1109 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
1114 self.context['hour'] = hour
1117 tz = ctx.tzExpr().getText()
1118 self.context['tz'] = DateParser._parse_tz(tz)
1122 def exitTwentyFourHourTimeExpr(
1123 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1126 hour = ctx.hour().getText()
1127 while not hour[-1].isdigit():
1129 hour = DateParser._get_int(hour)
1130 except Exception as e:
1131 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1132 if hour < 0 or hour > 23:
1133 raise ParseException(f'Bad hour (out of range): {hour}')
1134 self.context['hour'] = hour
1137 minute = DateParser._get_int(ctx.minute().getText())
1140 if minute < 0 or minute > 59:
1141 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1142 self.context['minute'] = minute
1145 seconds = DateParser._get_int(ctx.second().getText())
1148 if seconds < 0 or seconds > 59:
1149 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1150 self.context['seconds'] = seconds
1153 micros = DateParser._get_int(ctx.micros().getText())
1156 if micros < 0 or micros >= 1000000:
1157 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1158 self.context['micros'] = micros
1161 tz = ctx.tzExpr().getText()
1162 self.context['tz'] = DateParser._parse_tz(tz)
1167 @bootstrap.initialize
1169 parser = DateParser()
1170 for line in sys.stdin:
1172 line = re.sub(r"#.*$", "", line)
1173 if re.match(r"^ *$", line) is not None:
1176 dt = parser.parse(line)
1177 except Exception as e:
1179 print("Unrecognized.")
1181 assert dt is not None
1182 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1186 if __name__ == "__main__":