3 # pylint: disable=W0201
4 # pylint: disable=R0904
6 # © Copyright 2021-2022, Scott Gasch
8 """Parse dates in a variety of formats."""
15 from typing import Any, Callable, Dict, Optional
17 import antlr4 # type: ignore
18 import dateutil.easter
20 import holidays # type: ignore
23 from pyutils import bootstrap, decorator_utils
24 from pyutils.datetimez.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
25 from pyutils.datetimez.dateparse_utilsListener import (
26 dateparse_utilsListener,
28 from pyutils.datetimez.dateparse_utilsParser import (
29 dateparse_utilsParser,
31 from pyutils.datetimez.datetime_utils import (
35 n_timeunits_from_base,
37 from pyutils.security import acl
39 logger = logging.getLogger(__name__)
42 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
43 @functools.wraps(enter_or_exit_f)
44 def debug_parse_wrapper(*args, **kwargs):
50 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
52 for c in ctx.getChildren():
53 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
54 retval = enter_or_exit_f(*args, **kwargs)
57 return debug_parse_wrapper
60 class ParseException(Exception):
61 """An exception thrown during parsing because of unrecognized input."""
63 def __init__(self, message: str) -> None:
65 self.message = message
68 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
69 """An error listener that raises ParseExceptions."""
71 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
72 raise ParseException(msg)
75 self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs
79 def reportAttemptingFullContext(
80 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
84 def reportContextSensitivity(
85 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
90 @decorator_utils.decorate_matching_methods_with(
92 acl=acl.StringWildcardBasedACL(
97 denied_patterns=['enterEveryRule', 'exitEveryRule'],
98 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
102 class DateParser(dateparse_utilsListener):
103 """A class to parse dates expressed in human language."""
105 PARSE_TYPE_SINGLE_DATE_EXPR = 1
106 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
107 PARSE_TYPE_SINGLE_TIME_EXPR = 3
108 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
110 def __init__(self, *, override_now_for_test_purposes=None) -> None:
111 """C'tor. Passing a value to override_now_for_test_purposes can be
112 used to force this instance to use a custom date/time for its
113 idea of "now" so that the code can be more easily unittested.
114 Leave as None for real use cases.
116 self.month_name_to_number = {
131 # Used only for ides/nones. Month length on a non-leap year.
132 self.typical_days_per_month = {
147 # N.B. day number is also synched with datetime_utils.TimeUnit values
148 # which allows expressions like "3 wednesdays from now" to work.
149 self.day_name_to_number = {
159 # These TimeUnits are defined in datetime_utils and are used as params
160 # to datetime_utils.n_timeunits_from_base.
161 self.time_delta_unit_to_constant = {
162 'hou': TimeUnit.HOURS,
163 'min': TimeUnit.MINUTES,
164 'sec': TimeUnit.SECONDS,
166 self.delta_unit_to_constant = {
167 'day': TimeUnit.DAYS,
168 'wor': TimeUnit.WORKDAYS,
169 'wee': TimeUnit.WEEKS,
170 'mon': TimeUnit.MONTHS,
171 'yea': TimeUnit.YEARS,
173 self.override_now_for_test_purposes = override_now_for_test_purposes
175 # Note: _reset defines several class fields. It is used both here
176 # in the c'tor but also in between parse operations to restore the
177 # class' state and allow it to be reused.
181 def parse(self, date_string: str) -> Optional[datetime.datetime]:
182 """Parse a date/time expression and return a timezone agnostic
183 datetime on success. Also sets self.datetime, self.date and
184 self.time which can each be accessed other methods on the
185 class: get_datetime(), get_date() and get_time(). Raises a
186 ParseException with a helpful(?) message on parse error or
189 To get an idea of what expressions can be parsed, check out
190 the unittest and the grammar.
194 txt = '3 weeks before last tues at 9:15am'
197 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
199 # dt1 and dt2 will be identical other than the fact that
200 # the latter's tzinfo will be set to PST/PDT.
202 This is the main entrypoint to this class for caller code.
204 date_string = date_string.strip()
205 date_string = re.sub(r'\s+', ' ', date_string)
207 listener = RaisingErrorListener()
208 input_stream = antlr4.InputStream(date_string)
209 lexer = dateparse_utilsLexer(input_stream)
210 lexer.removeErrorListeners()
211 lexer.addErrorListener(listener)
212 stream = antlr4.CommonTokenStream(lexer)
213 parser = dateparse_utilsParser(stream)
214 parser.removeErrorListeners()
215 parser.addErrorListener(listener)
216 tree = parser.parse()
217 walker = antlr4.ParseTreeWalker()
218 walker.walk(self, tree)
221 def get_date(self) -> Optional[datetime.date]:
222 """Return the date part or None."""
225 def get_time(self) -> Optional[datetime.time]:
226 """Return the time part or None."""
229 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
230 """Return as a datetime. Parsed date expressions without any time
231 part return hours = minutes = seconds = microseconds = 0 (i.e. at
232 midnight that day). Parsed time expressions without any date part
233 default to date = today.
235 The optional tz param allows the caller to request the datetime be
236 timezone aware and sets the tzinfo to the indicated zone. Defaults
237 to timezone naive (i.e. tzinfo = None).
242 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
248 """Reset at init and between parses."""
249 if self.override_now_for_test_purposes is None:
250 self.now_datetime = datetime.datetime.now()
251 self.today = datetime.date.today()
253 self.now_datetime = self.override_now_for_test_purposes
254 self.today = datetime_to_date(self.override_now_for_test_purposes)
255 self.date: Optional[datetime.date] = None
256 self.time: Optional[datetime.time] = None
257 self.datetime: Optional[datetime.datetime] = None
258 self.context: Dict[str, Any] = {}
259 self.timedelta = datetime.timedelta(seconds=0)
260 self.saw_overt_year = False
263 def _normalize_special_day_name(name: str) -> str:
264 """String normalization / canonicalization for date expressions."""
266 name = name.replace("'", '')
267 name = name.replace('xmas', 'christmas')
268 name = name.replace('mlk', 'martin luther king')
269 name = name.replace(' ', '')
270 eve = 'eve' if name[-3:] == 'eve' else ''
271 name = name[:5] + eve
272 name = name.replace('washi', 'presi')
275 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
276 """Figure out what unit a date expression piece is talking about."""
278 return TimeUnit.MONTHS
279 txt = orig.lower()[:3]
280 if txt in self.day_name_to_number:
281 return TimeUnit(self.day_name_to_number[txt])
282 elif txt in self.delta_unit_to_constant:
283 return TimeUnit(self.delta_unit_to_constant[txt])
284 raise ParseException(f'Invalid date unit: {orig}')
286 def _figure_out_time_unit(self, orig: str) -> int:
287 """Figure out what unit a time expression piece is talking about."""
288 txt = orig.lower()[:3]
289 if txt in self.time_delta_unit_to_constant:
290 return self.time_delta_unit_to_constant[txt]
291 raise ParseException(f'Invalid time unit: {orig}')
293 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
294 """Parse what we think is a special date name and return its datetime
295 (or None if it can't be parsed).
298 year = self.context.get('year', today.year)
299 name = DateParser._normalize_special_day_name(self.context['special'])
301 # Yesterday, today, tomorrow -- ignore any next/last
302 if name in ('today', 'now'):
305 return today + datetime.timedelta(days=-1)
307 return today + datetime.timedelta(days=+1)
309 next_last = self.context.get('special_next_last', '')
310 if next_last == 'next':
312 self.saw_overt_year = True
313 elif next_last == 'last':
315 self.saw_overt_year = True
319 return dateutil.easter.easter(year=year)
320 elif name == 'hallo':
321 return datetime.date(year=year, month=10, day=31)
323 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
324 if 'Observed' not in holiday_name:
325 holiday_name = DateParser._normalize_special_day_name(holiday_name)
326 if name == holiday_name:
328 if name == 'chriseve':
329 return datetime.date(year=year, month=12, day=24)
330 elif name == 'newyeeve':
331 return datetime.date(year=year, month=12, day=31)
334 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
335 """Handle date expressions like "the ides of March" which require
336 both the "ides" and the month since the definition of the "ides"
337 changes based on the length of the month.
339 assert 'ide' in day or 'non' in day
340 assert month_number in self.typical_days_per_month
341 typical_days_per_month = self.typical_days_per_month[month_number]
344 if typical_days_per_month == 31:
345 if self.context['day'] == 'ide':
352 if self.context['day'] == 'ide':
357 def _parse_normal_date(self) -> datetime.date:
358 if 'dow' in self.context and 'month' not in self.context:
360 while d.weekday() != self.context['dow']:
361 d += datetime.timedelta(days=1)
364 if 'month' not in self.context:
365 raise ParseException('Missing month')
366 if 'day' not in self.context:
367 raise ParseException('Missing day')
368 if 'year' not in self.context:
369 self.context['year'] = self.today.year
370 self.saw_overt_year = False
372 self.saw_overt_year = True
374 # Handling "ides" and "nones" requires both the day and month.
375 if self.context['day'] == 'ide' or self.context['day'] == 'non':
376 self.context['day'] = self._resolve_ides_nones(
377 self.context['day'], self.context['month']
380 return datetime.date(
381 year=self.context['year'],
382 month=self.context['month'],
383 day=self.context['day'],
387 def _parse_tz(txt: str) -> Any:
393 tz1 = pytz.timezone(txt)
401 tz2 = dateutil.tz.gettz(txt)
407 # Try constructing an offset in seconds
410 if txt_sign in ('-', '+'):
411 sign = +1 if txt_sign == '+' else -1
413 minute = int(txt[-2:])
414 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
415 tzoffset = dateutil.tz.tzoffset(txt, offset)
422 def _get_int(txt: str) -> int:
423 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
425 while not txt[-1].isdigit():
429 # -- overridden methods invoked by parse walk. Note: not part of the class'
432 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
435 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
438 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
439 """Populate self.datetime."""
440 if self.date is None:
441 self.date = self.today
442 year = self.date.year
443 month = self.date.month
446 if self.time is None:
447 self.time = datetime.time(0, 0, 0)
448 hour = self.time.hour
449 minute = self.time.minute
450 second = self.time.second
451 micros = self.time.microsecond
453 self.datetime = datetime.datetime(
461 tzinfo=self.time.tzinfo,
464 # Apply resudual adjustments to times here when we have a
466 self.datetime = self.datetime + self.timedelta
467 assert self.datetime is not None
468 self.time = datetime.time(
470 self.datetime.minute,
471 self.datetime.second,
472 self.datetime.microsecond,
473 self.datetime.tzinfo,
476 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
478 if ctx.singleDateExpr() is not None:
479 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
480 elif ctx.baseAndOffsetDateExpr() is not None:
481 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
483 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
485 if ctx.singleTimeExpr() is not None:
486 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
487 elif ctx.baseAndOffsetTimeExpr() is not None:
488 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
490 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
491 """When we leave the date expression, populate self.date."""
492 if 'special' in self.context:
493 self.date = self._parse_special_date(self.context['special'])
495 self.date = self._parse_normal_date()
496 assert self.date is not None
498 # For a single date, just return the date we pulled out.
499 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
502 # Otherwise treat self.date as a base date that we're modifying
504 if 'delta_int' not in self.context:
505 raise ParseException('Missing delta_int?!')
506 count = self.context['delta_int']
510 # Adjust count's sign based on the presence of 'before' or 'after'.
511 if 'delta_before_after' in self.context:
512 before_after = self.context['delta_before_after'].lower()
513 if before_after in ('before', 'until', 'til', 'to'):
516 # What are we counting units of?
517 if 'delta_unit' not in self.context:
518 raise ParseException('Missing delta_unit?!')
519 unit = self.context['delta_unit']
520 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
521 self.date = datetime_to_date(dt)
523 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
525 self.time = datetime.time(
526 self.context['hour'],
527 self.context['minute'],
528 self.context['seconds'],
529 self.context['micros'],
530 tzinfo=self.context.get('tz', None),
532 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
535 # If we get here there (should be) a relative adjustment to
537 if 'nth' in self.context:
538 count = self.context['nth']
539 elif 'time_delta_int' in self.context:
540 count = self.context['time_delta_int']
542 raise ParseException('Missing delta in relative time.')
546 # Adjust count's sign based on the presence of 'before' or 'after'.
547 if 'time_delta_before_after' in self.context:
548 before_after = self.context['time_delta_before_after'].lower()
549 if before_after in ('before', 'until', 'til', 'to'):
552 # What are we counting units of... assume minutes.
553 if 'time_delta_unit' not in self.context:
554 self.timedelta += datetime.timedelta(minutes=count)
556 unit = self.context['time_delta_unit']
557 if unit == TimeUnit.SECONDS:
558 self.timedelta += datetime.timedelta(seconds=count)
559 elif unit == TimeUnit.MINUTES:
560 self.timedelta = datetime.timedelta(minutes=count)
561 elif unit == TimeUnit.HOURS:
562 self.timedelta = datetime.timedelta(hours=count)
564 raise ParseException(f'Invalid Unit: "{unit}"')
566 def exitDeltaPlusMinusExpr(
567 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
572 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
574 n = DateParser._get_int(n)
575 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
576 except Exception as e:
577 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
579 self.context['delta_int'] = n
580 self.context['delta_unit'] = unit
582 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
584 unit = self._figure_out_date_unit(ctx.getText().lower())
585 except Exception as e:
586 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
588 self.context['delta_unit'] = unit
590 def exitDeltaNextLast(
591 self, ctx: dateparse_utilsParser.DeltaNextLastContext
594 txt = ctx.getText().lower()
595 except Exception as e:
596 raise ParseException(f'Bad next/last: {ctx.getText()}') from e
597 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
598 raise ParseException(
599 'Next/last expression expected to be relative to today.'
601 if txt[:4] == 'next':
602 self.context['delta_int'] = +1
603 self.context['day'] = self.now_datetime.day
604 self.context['month'] = self.now_datetime.month
605 self.context['year'] = self.now_datetime.year
606 self.saw_overt_year = True
607 elif txt[:4] == 'last':
608 self.context['delta_int'] = -1
609 self.context['day'] = self.now_datetime.day
610 self.context['month'] = self.now_datetime.month
611 self.context['year'] = self.now_datetime.year
612 self.saw_overt_year = True
614 raise ParseException(f'Bad next/last: {ctx.getText()}')
616 def exitCountUnitsBeforeAfterTimeExpr(
617 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
619 if 'nth' not in self.context:
620 raise ParseException(f'Bad count expression: {ctx.getText()}')
622 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
623 self.context['time_delta_unit'] = unit
624 except Exception as e:
625 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
626 if 'time_delta_before_after' not in self.context:
627 raise ParseException(f'Bad Before/After: {ctx.getText()}')
629 def exitDeltaTimeFraction(
630 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
633 txt = ctx.getText().lower()[:4]
635 self.context['time_delta_int'] = 15
636 self.context['time_delta_unit'] = TimeUnit.MINUTES
638 self.context['time_delta_int'] = 30
639 self.context['time_delta_unit'] = TimeUnit.MINUTES
641 raise ParseException(f'Bad time fraction {ctx.getText()}')
642 except Exception as e:
643 raise ParseException(f'Bad time fraction {ctx.getText()}') from e
645 def exitDeltaBeforeAfter(
646 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
649 txt = ctx.getText().lower()
650 except Exception as e:
651 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
653 self.context['delta_before_after'] = txt
655 def exitDeltaTimeBeforeAfter(
656 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
659 txt = ctx.getText().lower()
660 except Exception as e:
661 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
663 self.context['time_delta_before_after'] = txt
665 def exitNthWeekdayInMonthMaybeYearExpr(
666 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
668 """Do a bunch of work to convert expressions like...
670 'the 2nd Friday of June' -and-
671 'the last Wednesday in October'
673 ...into base + offset expressions instead.
676 if 'nth' not in self.context:
677 raise ParseException(f'Missing nth number: {ctx.getText()}')
678 n = self.context['nth']
679 if n < 1 or n > 5: # months never have more than 5 Foodays
681 raise ParseException(f'Invalid nth number: {ctx.getText()}')
682 del self.context['nth']
683 self.context['delta_int'] = n
685 year = self.context.get('year', self.today.year)
686 if 'month' not in self.context:
687 raise ParseException(f'Missing month expression: {ctx.getText()}')
688 month = self.context['month']
690 dow = self.context['dow']
691 del self.context['dow']
692 self.context['delta_unit'] = dow
694 # For the nth Fooday in Month, start at the last day of
695 # the previous month count ahead N Foodays. For the last
696 # Fooday in Month, start at the last of the month and
697 # count back one Fooday.
703 tmp_date = datetime.date(year=year, month=month, day=1)
704 tmp_date = tmp_date - datetime.timedelta(days=1)
706 # The delta adjustment code can handle the case where
707 # the last day of the month is the day we're looking
710 tmp_date = datetime.date(year=year, month=month, day=1)
711 tmp_date = tmp_date - datetime.timedelta(days=1)
713 self.context['year'] = tmp_date.year
714 self.context['month'] = tmp_date.month
715 self.context['day'] = tmp_date.day
716 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
717 except Exception as e:
718 raise ParseException(
719 f'Invalid nthWeekday expression: {ctx.getText()}'
722 def exitFirstLastWeekdayInMonthMaybeYearExpr(
724 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
726 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
728 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
730 i = DateParser._get_int(ctx.getText())
731 except Exception as e:
732 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
734 self.context['nth'] = i
736 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
744 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
745 except Exception as e:
746 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
748 self.context['nth'] = txt
750 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
752 dow = ctx.getText().lower()[:3]
753 dow = self.day_name_to_number.get(dow, None)
754 except Exception as e:
755 raise ParseException('Bad day of week') from e
757 self.context['dow'] = dow
759 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
761 day = ctx.getText().lower()
763 self.context['day'] = 'ide'
766 self.context['day'] = 'non'
769 self.context['day'] = 1
771 day = DateParser._get_int(day)
772 if day < 1 or day > 31:
773 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
774 except Exception as e:
775 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
776 self.context['day'] = day
778 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
780 month = ctx.getText()
781 while month[0] == '/' or month[0] == '-':
783 month = month[:3].lower()
784 month = self.month_name_to_number.get(month, None)
786 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
787 except Exception as e:
788 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
790 self.context['month'] = month
792 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
794 month = DateParser._get_int(ctx.getText())
795 if month < 1 or month > 12:
796 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
797 except Exception as e:
798 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
800 self.context['month'] = month
802 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
804 year = DateParser._get_int(ctx.getText())
806 raise ParseException(f'Bad year expression: {ctx.getText()}')
807 except Exception as e:
808 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
810 self.saw_overt_year = True
811 self.context['year'] = year
813 def exitSpecialDateMaybeYearExpr(
814 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
817 special = ctx.specialDate().getText().lower()
818 self.context['special'] = special
819 except Exception as e:
820 raise ParseException(
821 f'Bad specialDate expression: {ctx.specialDate().getText()}'
824 mod = ctx.thisNextLast()
826 if mod.THIS() is not None:
827 self.context['special_next_last'] = 'this'
828 elif mod.NEXT() is not None:
829 self.context['special_next_last'] = 'next'
830 elif mod.LAST() is not None:
831 self.context['special_next_last'] = 'last'
832 except Exception as e:
833 raise ParseException(
834 f'Bad specialDateNextLast expression: {ctx.getText()}'
837 def exitNFoosFromTodayAgoExpr(
838 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
840 d = self.now_datetime
842 count = DateParser._get_int(ctx.unsignedInt().getText())
843 unit = ctx.deltaUnit().getText().lower()
844 ago_from_now = ctx.AGO_FROM_NOW().getText()
845 except Exception as e:
846 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
848 if "ago" in ago_from_now or "back" in ago_from_now:
851 unit = self._figure_out_date_unit(unit)
852 d = n_timeunits_from_base(count, TimeUnit(unit), d)
853 self.context['year'] = d.year
854 self.context['month'] = d.month
855 self.context['day'] = d.day
857 def exitDeltaRelativeToTodayExpr(
858 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
860 # When someone says "next week" they mean a week from now.
861 # Likewise next month or last year. These expressions are now
864 # But when someone says "this Friday" they mean "this coming
865 # Friday". It would be weird to say "this Friday" if today
866 # was already Friday but I'm parsing it to mean: the next day
867 # that is a Friday. So when you say "next Friday" you mean
868 # the Friday after this coming Friday, or 2 Fridays from now.
870 # This set handles this weirdness.
882 d = self.now_datetime
884 mod = ctx.thisNextLast()
885 unit = ctx.deltaUnit().getText().lower()
886 unit = self._figure_out_date_unit(unit)
900 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
901 except Exception as e:
902 raise ParseException(
903 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
905 d = n_timeunits_from_base(count, TimeUnit(unit), d)
906 self.context['year'] = d.year
907 self.context['month'] = d.month
908 self.context['day'] = d.day
910 def exitSpecialTimeExpr(
911 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
914 txt = ctx.specialTime().getText().lower()
915 except Exception as e:
916 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
918 if txt in ('noon', 'midday'):
919 self.context['hour'] = 12
920 self.context['minute'] = 0
921 self.context['seconds'] = 0
922 self.context['micros'] = 0
923 elif txt == 'midnight':
924 self.context['hour'] = 0
925 self.context['minute'] = 0
926 self.context['seconds'] = 0
927 self.context['micros'] = 0
929 raise ParseException(f'Bad special time expression: {txt}')
932 tz = ctx.tzExpr().getText()
933 self.context['tz'] = DateParser._parse_tz(tz)
937 def exitTwelveHourTimeExpr(
938 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
941 hour = ctx.hour().getText()
942 while not hour[-1].isdigit():
944 hour = DateParser._get_int(hour)
945 except Exception as e:
946 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
947 if hour <= 0 or hour > 12:
948 raise ParseException(f'Bad hour (out of range): {hour}')
951 minute = DateParser._get_int(ctx.minute().getText())
954 if minute < 0 or minute > 59:
955 raise ParseException(f'Bad minute (out of range): {minute}')
956 self.context['minute'] = minute
959 seconds = DateParser._get_int(ctx.second().getText())
962 if seconds < 0 or seconds > 59:
963 raise ParseException(f'Bad second (out of range): {seconds}')
964 self.context['seconds'] = seconds
967 micros = DateParser._get_int(ctx.micros().getText())
970 if micros < 0 or micros > 1000000:
971 raise ParseException(f'Bad micros (out of range): {micros}')
972 self.context['micros'] = micros
975 ampm = ctx.ampm().getText()
976 except Exception as e:
977 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
982 self.context['hour'] = hour
985 tz = ctx.tzExpr().getText()
986 self.context['tz'] = DateParser._parse_tz(tz)
990 def exitTwentyFourHourTimeExpr(
991 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
994 hour = ctx.hour().getText()
995 while not hour[-1].isdigit():
997 hour = DateParser._get_int(hour)
998 except Exception as e:
999 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1000 if hour < 0 or hour > 23:
1001 raise ParseException(f'Bad hour (out of range): {hour}')
1002 self.context['hour'] = hour
1005 minute = DateParser._get_int(ctx.minute().getText())
1008 if minute < 0 or minute > 59:
1009 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1010 self.context['minute'] = minute
1013 seconds = DateParser._get_int(ctx.second().getText())
1016 if seconds < 0 or seconds > 59:
1017 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1018 self.context['seconds'] = seconds
1021 micros = DateParser._get_int(ctx.micros().getText())
1024 if micros < 0 or micros >= 1000000:
1025 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1026 self.context['micros'] = micros
1029 tz = ctx.tzExpr().getText()
1030 self.context['tz'] = DateParser._parse_tz(tz)
1035 @bootstrap.initialize
1037 parser = DateParser()
1038 for line in sys.stdin:
1040 line = re.sub(r"#.*$", "", line)
1041 if re.match(r"^ *$", line) is not None:
1044 dt = parser.parse(line)
1045 except Exception as e:
1047 print("Unrecognized.")
1049 assert dt is not None
1050 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1054 if __name__ == "__main__":