3 # pylint: disable=too-many-public-methods
4 # pylint: disable=too-many-instance-attributes
6 # © Copyright 2021-2023, 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
360 self, *, tz: Optional[datetime.tzinfo] = None
361 ) -> Optional[datetime.datetime]:
362 """Get the datetime of the last :meth:`parse` operation again
366 tz: the timezone to set on output times. By default we
367 return timezone-naive datetime objects.
370 the same datetime that :meth:`parse` last did, optionally
371 overriding the timezone. Returns None of no calls to
372 :meth:`parse` have yet been made.
376 Parsed date expressions without any time part return
377 hours = minutes = seconds = microseconds = 0 (i.e. at
378 midnight that day). Parsed time expressions without any
379 date part default to date = today.
384 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
390 """Reset at init and between parses."""
391 if self.override_now_for_test_purposes is None:
392 self.now_datetime = datetime.datetime.now()
393 self.today = datetime.date.today()
395 self.now_datetime = self.override_now_for_test_purposes
396 self.today = datetime_to_date(self.override_now_for_test_purposes)
397 self.date: Optional[datetime.date] = None
398 self.time: Optional[datetime.time] = None
399 self.datetime: Optional[datetime.datetime] = None
400 self.context: Dict[str, Any] = {}
401 self.timedelta = datetime.timedelta(seconds=0)
402 self.saw_overt_year = False
405 def _normalize_special_day_name(name: str) -> str:
406 """String normalization / canonicalization for date expressions."""
408 name = name.replace("'", '')
409 name = name.replace('xmas', 'christmas')
410 name = name.replace('mlk', 'martin luther king')
411 name = name.replace(' ', '')
412 eve = 'eve' if name[-3:] == 'eve' else ''
413 name = name[:5] + eve
414 name = name.replace('washi', 'presi')
417 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
418 """Figure out what unit a date expression piece is talking about."""
420 return TimeUnit.MONTHS
421 txt = orig.lower()[:3]
422 if txt in self.day_name_to_number:
423 return TimeUnit(self.day_name_to_number[txt])
424 elif txt in self.delta_unit_to_constant:
425 return TimeUnit(self.delta_unit_to_constant[txt])
426 raise ParseException(f'Invalid date unit: {orig}')
428 def _figure_out_time_unit(self, orig: str) -> int:
429 """Figure out what unit a time expression piece is talking about."""
430 txt = orig.lower()[:3]
431 if txt in self.time_delta_unit_to_constant:
432 return self.time_delta_unit_to_constant[txt]
433 raise ParseException(f'Invalid time unit: {orig}')
435 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
436 """Parse what we think is a special date name and return its datetime
437 (or None if it can't be parsed).
440 year = self.context.get('year', today.year)
441 name = DateParser._normalize_special_day_name(self.context['special'])
443 # Yesterday, today, tomorrow -- ignore any next/last
444 if name in {'today', 'now'}:
447 return today + datetime.timedelta(days=-1)
449 return today + datetime.timedelta(days=+1)
451 next_last = self.context.get('special_next_last', '')
452 if next_last == 'next':
454 self.saw_overt_year = True
455 elif next_last == 'last':
457 self.saw_overt_year = True
461 return easter(year=year)
462 elif name == 'hallo':
463 return datetime.date(year=year, month=10, day=31)
465 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
466 if 'Observed' not in holiday_name:
467 holiday_name = DateParser._normalize_special_day_name(holiday_name)
468 if name == holiday_name:
470 if name == 'chriseve':
471 return datetime.date(year=year, month=12, day=24)
472 elif name == 'newyeeve':
473 return datetime.date(year=year, month=12, day=31)
476 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
477 """Handle date expressions like "the ides of March" which require
478 both the "ides" and the month since the definition of the "ides"
479 changes based on the length of the month.
481 assert 'ide' in day or 'non' in day
482 assert month_number in self.typical_days_per_month
483 typical_days_per_month = self.typical_days_per_month[month_number]
486 if typical_days_per_month == 31:
487 if self.context['day'] == 'ide':
494 if self.context['day'] == 'ide':
499 def _parse_normal_date(self) -> datetime.date:
500 if 'dow' in self.context and 'month' not in self.context:
502 while d.weekday() != self.context['dow']:
503 d += datetime.timedelta(days=1)
506 if 'month' not in self.context:
507 raise ParseException('Missing month')
508 if 'day' not in self.context:
509 raise ParseException('Missing day')
510 if 'year' not in self.context:
511 self.context['year'] = self.today.year
512 self.saw_overt_year = False
514 self.saw_overt_year = True
516 # Handling "ides" and "nones" requires both the day and month.
517 if self.context['day'] == 'ide' or self.context['day'] == 'non':
518 self.context['day'] = self._resolve_ides_nones(
519 self.context['day'], self.context['month']
522 return datetime.date(
523 year=self.context['year'],
524 month=self.context['month'],
525 day=self.context['day'],
529 def _parse_tz(txt: str) -> Any:
535 tz1 = pytz.timezone(txt)
541 # Try constructing an offset in seconds
544 if txt_sign in {'-', '+'}:
545 sign = +1 if txt_sign == '+' else -1
547 minute = int(txt[-2:])
548 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
549 tzoffset = datetime.timezone(datetime.timedelta(seconds=offset))
556 def _get_int(txt: str) -> int:
557 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
559 while not txt[-1].isdigit():
563 # -- overridden methods invoked by parse walk. Note: not part of the class'
566 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
569 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
572 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
573 """Populate self.datetime."""
574 if self.date is None:
575 self.date = self.today
576 year = self.date.year
577 month = self.date.month
580 if self.time is None:
581 self.time = datetime.time(0, 0, 0)
582 hour = self.time.hour
583 minute = self.time.minute
584 second = self.time.second
585 micros = self.time.microsecond
587 self.datetime = datetime.datetime(
595 tzinfo=self.time.tzinfo,
598 # Apply resudual adjustments to times here when we have a
600 self.datetime = self.datetime + self.timedelta
601 assert self.datetime is not None
602 self.time = datetime.time(
604 self.datetime.minute,
605 self.datetime.second,
606 self.datetime.microsecond,
607 self.datetime.tzinfo,
610 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
612 if ctx.singleDateExpr() is not None:
613 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
614 elif ctx.baseAndOffsetDateExpr() is not None:
615 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
617 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
619 if ctx.singleTimeExpr() is not None:
620 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
621 elif ctx.baseAndOffsetTimeExpr() is not None:
622 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
624 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
625 """When we leave the date expression, populate self.date."""
626 if 'special' in self.context:
627 self.date = self._parse_special_date(self.context['special'])
629 self.date = self._parse_normal_date()
630 assert self.date is not None
632 # For a single date, just return the date we pulled out.
633 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
636 # Otherwise treat self.date as a base date that we're modifying
638 if 'delta_int' not in self.context:
639 raise ParseException('Missing delta_int?!')
640 count = self.context['delta_int']
644 # Adjust count's sign based on the presence of 'before' or 'after'.
645 if 'delta_before_after' in self.context:
646 before_after = self.context['delta_before_after'].lower()
647 if before_after in {'before', 'until', 'til', 'to'}:
650 # What are we counting units of?
651 if 'delta_unit' not in self.context:
652 raise ParseException('Missing delta_unit?!')
653 unit = self.context['delta_unit']
654 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
655 self.date = datetime_to_date(dt)
657 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
659 self.time = datetime.time(
660 self.context['hour'],
661 self.context['minute'],
662 self.context['seconds'],
663 self.context['micros'],
664 tzinfo=self.context.get('tz', None),
666 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
669 # If we get here there (should be) a relative adjustment to
671 if 'nth' in self.context:
672 count = self.context['nth']
673 elif 'time_delta_int' in self.context:
674 count = self.context['time_delta_int']
676 raise ParseException('Missing delta in relative time.')
680 # Adjust count's sign based on the presence of 'before' or 'after'.
681 if 'time_delta_before_after' in self.context:
682 before_after = self.context['time_delta_before_after'].lower()
683 if before_after in {'before', 'until', 'til', 'to'}:
686 # What are we counting units of... assume minutes.
687 if 'time_delta_unit' not in self.context:
688 self.timedelta += datetime.timedelta(minutes=count)
690 unit = self.context['time_delta_unit']
691 if unit == TimeUnit.SECONDS:
692 self.timedelta += datetime.timedelta(seconds=count)
693 elif unit == TimeUnit.MINUTES:
694 self.timedelta = datetime.timedelta(minutes=count)
695 elif unit == TimeUnit.HOURS:
696 self.timedelta = datetime.timedelta(hours=count)
698 raise ParseException(f'Invalid Unit: "{unit}"')
700 def exitDeltaPlusMinusExpr(
701 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
706 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
708 n = DateParser._get_int(n)
709 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
710 except Exception as e:
711 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
713 self.context['delta_int'] = n
714 self.context['delta_unit'] = unit
716 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
718 unit = self._figure_out_date_unit(ctx.getText().lower())
719 except Exception as e:
720 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
722 self.context['delta_unit'] = unit
724 def exitDeltaNextLast(
725 self, ctx: dateparse_utilsParser.DeltaNextLastContext
728 txt = ctx.getText().lower()
729 except Exception as e:
730 raise ParseException(f'Bad next/last: {ctx.getText()}') from e
731 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
732 raise ParseException(
733 'Next/last expression expected to be relative to today.'
735 if txt[:4] == 'next':
736 self.context['delta_int'] = +1
737 self.context['day'] = self.now_datetime.day
738 self.context['month'] = self.now_datetime.month
739 self.context['year'] = self.now_datetime.year
740 self.saw_overt_year = True
741 elif txt[:4] == 'last':
742 self.context['delta_int'] = -1
743 self.context['day'] = self.now_datetime.day
744 self.context['month'] = self.now_datetime.month
745 self.context['year'] = self.now_datetime.year
746 self.saw_overt_year = True
748 raise ParseException(f'Bad next/last: {ctx.getText()}')
750 def exitCountUnitsBeforeAfterTimeExpr(
751 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
753 if 'nth' not in self.context:
754 raise ParseException(f'Bad count expression: {ctx.getText()}')
756 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
757 self.context['time_delta_unit'] = unit
758 except Exception as e:
759 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
760 if 'time_delta_before_after' not in self.context:
761 raise ParseException(f'Bad Before/After: {ctx.getText()}')
763 def exitDeltaTimeFraction(
764 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
767 txt = ctx.getText().lower()[:4]
769 self.context['time_delta_int'] = 15
770 self.context['time_delta_unit'] = TimeUnit.MINUTES
772 self.context['time_delta_int'] = 30
773 self.context['time_delta_unit'] = TimeUnit.MINUTES
775 raise ParseException(f'Bad time fraction {ctx.getText()}')
776 except Exception as e:
777 raise ParseException(f'Bad time fraction {ctx.getText()}') from e
779 def exitDeltaBeforeAfter(
780 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
783 txt = ctx.getText().lower()
784 except Exception as e:
785 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
787 self.context['delta_before_after'] = txt
789 def exitDeltaTimeBeforeAfter(
790 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
793 txt = ctx.getText().lower()
794 except Exception as e:
795 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
797 self.context['time_delta_before_after'] = txt
799 def exitNthWeekdayInMonthMaybeYearExpr(
800 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
802 """Do a bunch of work to convert expressions like...
804 'the 2nd Friday of June' -and-
805 'the last Wednesday in October'
807 ...into base + offset expressions instead.
810 if 'nth' not in self.context:
811 raise ParseException(f'Missing nth number: {ctx.getText()}')
812 n = self.context['nth']
813 if n < 1 or n > 5: # months never have more than 5 Foodays
815 raise ParseException(f'Invalid nth number: {ctx.getText()}')
816 del self.context['nth']
817 self.context['delta_int'] = n
819 year = self.context.get('year', self.today.year)
820 if 'month' not in self.context:
821 raise ParseException(f'Missing month expression: {ctx.getText()}')
822 month = self.context['month']
824 dow = self.context['dow']
825 del self.context['dow']
826 self.context['delta_unit'] = dow
828 # For the nth Fooday in Month, start at the last day of
829 # the previous month count ahead N Foodays. For the last
830 # Fooday in Month, start at the last of the month and
831 # count back one Fooday.
837 tmp_date = datetime.date(year=year, month=month, day=1)
838 tmp_date = tmp_date - datetime.timedelta(days=1)
840 # The delta adjustment code can handle the case where
841 # the last day of the month is the day we're looking
844 tmp_date = datetime.date(year=year, month=month, day=1)
845 tmp_date = tmp_date - datetime.timedelta(days=1)
847 self.context['year'] = tmp_date.year
848 self.context['month'] = tmp_date.month
849 self.context['day'] = tmp_date.day
850 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
851 except Exception as e:
852 raise ParseException(
853 f'Invalid nthWeekday expression: {ctx.getText()}'
856 def exitFirstLastWeekdayInMonthMaybeYearExpr(
858 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
860 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
862 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
864 i = DateParser._get_int(ctx.getText())
865 except Exception as e:
866 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
868 self.context['nth'] = i
870 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
878 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
879 except Exception as e:
880 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
882 self.context['nth'] = txt
884 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
886 dow = ctx.getText().lower()[:3]
887 dow = self.day_name_to_number.get(dow, None)
888 except Exception as e:
889 raise ParseException('Bad day of week') from e
891 self.context['dow'] = dow
893 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
895 day = ctx.getText().lower()
897 self.context['day'] = 'ide'
900 self.context['day'] = 'non'
903 self.context['day'] = 1
905 day = DateParser._get_int(day)
906 if day < 1 or day > 31:
907 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
908 except Exception as e:
909 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
910 self.context['day'] = day
912 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
914 month = ctx.getText()
915 while month[0] == '/' or month[0] == '-':
917 month = month[:3].lower()
918 month = self.month_name_to_number.get(month, None)
920 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
921 except Exception as e:
922 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
924 self.context['month'] = month
926 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
928 month = DateParser._get_int(ctx.getText())
929 if month < 1 or month > 12:
930 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
931 except Exception as e:
932 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
934 self.context['month'] = month
936 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
938 year = DateParser._get_int(ctx.getText())
940 raise ParseException(f'Bad year expression: {ctx.getText()}')
941 except Exception as e:
942 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
944 self.saw_overt_year = True
945 self.context['year'] = year
947 def exitSpecialDateMaybeYearExpr(
948 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
951 special = ctx.specialDate().getText().lower()
952 self.context['special'] = special
953 except Exception as e:
954 raise ParseException(
955 f'Bad specialDate expression: {ctx.specialDate().getText()}'
958 mod = ctx.thisNextLast()
960 if mod.THIS() is not None:
961 self.context['special_next_last'] = 'this'
962 elif mod.NEXT() is not None:
963 self.context['special_next_last'] = 'next'
964 elif mod.LAST() is not None:
965 self.context['special_next_last'] = 'last'
966 except Exception as e:
967 raise ParseException(
968 f'Bad specialDateNextLast expression: {ctx.getText()}'
971 def exitNFoosFromTodayAgoExpr(
972 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
974 d = self.now_datetime
976 count = DateParser._get_int(ctx.unsignedInt().getText())
977 unit = ctx.deltaUnit().getText().lower()
978 ago_from_now = ctx.AGO_FROM_NOW().getText()
979 except Exception as e:
980 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
982 if "ago" in ago_from_now or "back" in ago_from_now:
985 unit = self._figure_out_date_unit(unit)
986 d = n_timeunits_from_base(count, TimeUnit(unit), d)
987 self.context['year'] = d.year
988 self.context['month'] = d.month
989 self.context['day'] = d.day
991 def exitDeltaRelativeToTodayExpr(
992 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
994 # When someone says "next week" they mean a week from now.
995 # Likewise next month or last year. These expressions are now
998 # But when someone says "this Friday" they mean "this coming
999 # Friday". It would be weird to say "this Friday" if today
1000 # was already Friday but I'm parsing it to mean: the next day
1001 # that is a Friday. So when you say "next Friday" you mean
1002 # the Friday after this coming Friday, or 2 Fridays from now.
1004 # This set handles this weirdness.
1009 TimeUnit.WEDNESDAYS,
1016 d = self.now_datetime
1018 mod = ctx.thisNextLast()
1019 unit = ctx.deltaUnit().getText().lower()
1020 unit = self._figure_out_date_unit(unit)
1024 if unit in weekdays:
1029 if unit in weekdays:
1034 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
1035 except Exception as e:
1036 raise ParseException(
1037 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
1039 d = n_timeunits_from_base(count, TimeUnit(unit), d)
1040 self.context['year'] = d.year
1041 self.context['month'] = d.month
1042 self.context['day'] = d.day
1044 def exitSpecialTimeExpr(
1045 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
1048 txt = ctx.specialTime().getText().lower()
1049 except Exception as e:
1050 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
1052 if txt in {'noon', 'midday'}:
1053 self.context['hour'] = 12
1054 self.context['minute'] = 0
1055 self.context['seconds'] = 0
1056 self.context['micros'] = 0
1057 elif txt == 'midnight':
1058 self.context['hour'] = 0
1059 self.context['minute'] = 0
1060 self.context['seconds'] = 0
1061 self.context['micros'] = 0
1063 raise ParseException(f'Bad special time expression: {txt}')
1066 tz = ctx.tzExpr().getText()
1067 self.context['tz'] = DateParser._parse_tz(tz)
1071 def exitTwelveHourTimeExpr(
1072 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
1075 hour = ctx.hour().getText()
1076 while not hour[-1].isdigit():
1078 hour = DateParser._get_int(hour)
1079 except Exception as e:
1080 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1081 if hour <= 0 or hour > 12:
1082 raise ParseException(f'Bad hour (out of range): {hour}')
1085 minute = DateParser._get_int(ctx.minute().getText())
1088 if minute < 0 or minute > 59:
1089 raise ParseException(f'Bad minute (out of range): {minute}')
1090 self.context['minute'] = minute
1093 seconds = DateParser._get_int(ctx.second().getText())
1096 if seconds < 0 or seconds > 59:
1097 raise ParseException(f'Bad second (out of range): {seconds}')
1098 self.context['seconds'] = seconds
1101 micros = DateParser._get_int(ctx.micros().getText())
1104 if micros < 0 or micros > 1000000:
1105 raise ParseException(f'Bad micros (out of range): {micros}')
1106 self.context['micros'] = micros
1109 ampm = ctx.ampm().getText()
1110 except Exception as e:
1111 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
1116 self.context['hour'] = hour
1119 tz = ctx.tzExpr().getText()
1120 self.context['tz'] = DateParser._parse_tz(tz)
1124 def exitTwentyFourHourTimeExpr(
1125 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1128 hour = ctx.hour().getText()
1129 while not hour[-1].isdigit():
1131 hour = DateParser._get_int(hour)
1132 except Exception as e:
1133 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1134 if hour < 0 or hour > 23:
1135 raise ParseException(f'Bad hour (out of range): {hour}')
1136 self.context['hour'] = hour
1139 minute = DateParser._get_int(ctx.minute().getText())
1142 if minute < 0 or minute > 59:
1143 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1144 self.context['minute'] = minute
1147 seconds = DateParser._get_int(ctx.second().getText())
1150 if seconds < 0 or seconds > 59:
1151 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1152 self.context['seconds'] = seconds
1155 micros = DateParser._get_int(ctx.micros().getText())
1158 if micros < 0 or micros >= 1000000:
1159 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1160 self.context['micros'] = micros
1163 tz = ctx.tzExpr().getText()
1164 self.context['tz'] = DateParser._parse_tz(tz)
1169 @bootstrap.initialize
1171 parser = DateParser()
1172 for line in sys.stdin:
1174 line = re.sub(r"#.*$", "", line)
1175 if re.match(r"^ *$", line) is not None:
1178 dt = parser.parse(line)
1180 logger.exception("Could not parse supposed date expression: %s", line)
1181 print("Unrecognized.")
1183 assert dt is not None
1184 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1188 if __name__ == "__main__":