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
25 import decorator_utils
26 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
27 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
28 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
29 from datetime_utils import (
33 n_timeunits_from_base,
36 logger = logging.getLogger(__name__)
39 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
40 @functools.wraps(enter_or_exit_f)
41 def debug_parse_wrapper(*args, **kwargs):
47 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
49 for c in ctx.getChildren():
50 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
51 retval = enter_or_exit_f(*args, **kwargs)
54 return debug_parse_wrapper
57 class ParseException(Exception):
58 """An exception thrown during parsing because of unrecognized input."""
60 def __init__(self, message: str) -> None:
62 self.message = message
65 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
66 """An error listener that raises ParseExceptions."""
68 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
69 raise ParseException(msg)
71 def reportAmbiguity(self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs):
74 def reportAttemptingFullContext(
75 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
79 def reportContextSensitivity(self, recognizer, dfa, startIndex, stopIndex, prediction, configs):
83 @decorator_utils.decorate_matching_methods_with(
85 acl=acl.StringWildcardBasedACL(
90 denied_patterns=['enterEveryRule', 'exitEveryRule'],
91 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
95 class DateParser(dateparse_utilsListener):
96 """A class to parse dates expressed in human language."""
98 PARSE_TYPE_SINGLE_DATE_EXPR = 1
99 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
100 PARSE_TYPE_SINGLE_TIME_EXPR = 3
101 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
103 def __init__(self, *, override_now_for_test_purposes=None) -> None:
104 """C'tor. Passing a value to override_now_for_test_purposes can be
105 used to force this instance to use a custom date/time for its
106 idea of "now" so that the code can be more easily unittested.
107 Leave as None for real use cases.
109 self.month_name_to_number = {
124 # Used only for ides/nones. Month length on a non-leap year.
125 self.typical_days_per_month = {
140 # N.B. day number is also synched with datetime_utils.TimeUnit values
141 # which allows expressions like "3 wednesdays from now" to work.
142 self.day_name_to_number = {
152 # These TimeUnits are defined in datetime_utils and are used as params
153 # to datetime_utils.n_timeunits_from_base.
154 self.time_delta_unit_to_constant = {
155 'hou': TimeUnit.HOURS,
156 'min': TimeUnit.MINUTES,
157 'sec': TimeUnit.SECONDS,
159 self.delta_unit_to_constant = {
160 'day': TimeUnit.DAYS,
161 'wor': TimeUnit.WORKDAYS,
162 'wee': TimeUnit.WEEKS,
163 'mon': TimeUnit.MONTHS,
164 'yea': TimeUnit.YEARS,
166 self.override_now_for_test_purposes = override_now_for_test_purposes
168 # Note: _reset defines several class fields. It is used both here
169 # in the c'tor but also in between parse operations to restore the
170 # class' state and allow it to be reused.
174 def parse(self, date_string: str) -> Optional[datetime.datetime]:
175 """Parse a date/time expression and return a timezone agnostic
176 datetime on success. Also sets self.datetime, self.date and
177 self.time which can each be accessed other methods on the
178 class: get_datetime(), get_date() and get_time(). Raises a
179 ParseException with a helpful(?) message on parse error or
182 To get an idea of what expressions can be parsed, check out
183 the unittest and the grammar.
187 txt = '3 weeks before last tues at 9:15am'
190 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
192 # dt1 and dt2 will be identical other than the fact that
193 # the latter's tzinfo will be set to PST/PDT.
195 This is the main entrypoint to this class for caller code.
197 date_string = date_string.strip()
198 date_string = re.sub(r'\s+', ' ', date_string)
200 listener = RaisingErrorListener()
201 input_stream = antlr4.InputStream(date_string)
202 lexer = dateparse_utilsLexer(input_stream)
203 lexer.removeErrorListeners()
204 lexer.addErrorListener(listener)
205 stream = antlr4.CommonTokenStream(lexer)
206 parser = dateparse_utilsParser(stream)
207 parser.removeErrorListeners()
208 parser.addErrorListener(listener)
209 tree = parser.parse()
210 walker = antlr4.ParseTreeWalker()
211 walker.walk(self, tree)
214 def get_date(self) -> Optional[datetime.date]:
215 """Return the date part or None."""
218 def get_time(self) -> Optional[datetime.time]:
219 """Return the time part or None."""
222 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
223 """Return as a datetime. Parsed date expressions without any time
224 part return hours = minutes = seconds = microseconds = 0 (i.e. at
225 midnight that day). Parsed time expressions without any date part
226 default to date = today.
228 The optional tz param allows the caller to request the datetime be
229 timezone aware and sets the tzinfo to the indicated zone. Defaults
230 to timezone naive (i.e. tzinfo = None).
235 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
241 """Reset at init and between parses."""
242 if self.override_now_for_test_purposes is None:
243 self.now_datetime = datetime.datetime.now()
244 self.today = datetime.date.today()
246 self.now_datetime = self.override_now_for_test_purposes
247 self.today = datetime_to_date(self.override_now_for_test_purposes)
248 self.date: Optional[datetime.date] = None
249 self.time: Optional[datetime.time] = None
250 self.datetime: Optional[datetime.datetime] = None
251 self.context: Dict[str, Any] = {}
252 self.timedelta = datetime.timedelta(seconds=0)
253 self.saw_overt_year = False
256 def _normalize_special_day_name(name: str) -> str:
257 """String normalization / canonicalization for date expressions."""
259 name = name.replace("'", '')
260 name = name.replace('xmas', 'christmas')
261 name = name.replace('mlk', 'martin luther king')
262 name = name.replace(' ', '')
263 eve = 'eve' if name[-3:] == 'eve' else ''
264 name = name[:5] + eve
265 name = name.replace('washi', 'presi')
268 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
269 """Figure out what unit a date expression piece is talking about."""
271 return TimeUnit.MONTHS
272 txt = orig.lower()[:3]
273 if txt in self.day_name_to_number:
274 return TimeUnit(self.day_name_to_number[txt])
275 elif txt in self.delta_unit_to_constant:
276 return TimeUnit(self.delta_unit_to_constant[txt])
277 raise ParseException(f'Invalid date unit: {orig}')
279 def _figure_out_time_unit(self, orig: str) -> int:
280 """Figure out what unit a time expression piece is talking about."""
281 txt = orig.lower()[:3]
282 if txt in self.time_delta_unit_to_constant:
283 return self.time_delta_unit_to_constant[txt]
284 raise ParseException(f'Invalid time unit: {orig}')
286 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
287 """Parse what we think is a special date name and return its datetime
288 (or None if it can't be parsed).
291 year = self.context.get('year', today.year)
292 name = DateParser._normalize_special_day_name(self.context['special'])
294 # Yesterday, today, tomorrow -- ignore any next/last
295 if name in ('today', 'now'):
298 return today + datetime.timedelta(days=-1)
300 return today + datetime.timedelta(days=+1)
302 next_last = self.context.get('special_next_last', '')
303 if next_last == 'next':
305 self.saw_overt_year = True
306 elif next_last == 'last':
308 self.saw_overt_year = True
312 return dateutil.easter.easter(year=year)
313 elif name == 'hallo':
314 return datetime.date(year=year, month=10, day=31)
316 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
317 if 'Observed' not in holiday_name:
318 holiday_name = DateParser._normalize_special_day_name(holiday_name)
319 if name == holiday_name:
321 if name == 'chriseve':
322 return datetime.date(year=year, month=12, day=24)
323 elif name == 'newyeeve':
324 return datetime.date(year=year, month=12, day=31)
327 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
328 """Handle date expressions like "the ides of March" which require
329 both the "ides" and the month since the definition of the "ides"
330 changes based on the length of the month.
332 assert 'ide' in day or 'non' in day
333 assert month_number in self.typical_days_per_month
334 typical_days_per_month = self.typical_days_per_month[month_number]
337 if typical_days_per_month == 31:
338 if self.context['day'] == 'ide':
345 if self.context['day'] == 'ide':
350 def _parse_normal_date(self) -> datetime.date:
351 if 'dow' in self.context:
353 while d.weekday() != self.context['dow']:
354 d += datetime.timedelta(days=1)
357 if 'month' not in self.context:
358 raise ParseException('Missing month')
359 if 'day' not in self.context:
360 raise ParseException('Missing day')
361 if 'year' not in self.context:
362 self.context['year'] = self.today.year
363 self.saw_overt_year = False
365 self.saw_overt_year = True
367 # Handling "ides" and "nones" requires both the day and month.
368 if self.context['day'] == 'ide' or self.context['day'] == 'non':
369 self.context['day'] = self._resolve_ides_nones(
370 self.context['day'], self.context['month']
373 return datetime.date(
374 year=self.context['year'],
375 month=self.context['month'],
376 day=self.context['day'],
380 def _parse_tz(txt: str) -> Any:
386 tz1 = pytz.timezone(txt)
394 tz2 = dateutil.tz.gettz(txt)
400 # Try constructing an offset in seconds
403 if txt_sign in ('-', '+'):
404 sign = +1 if txt_sign == '+' else -1
406 minute = int(txt[-2:])
407 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
408 tzoffset = dateutil.tz.tzoffset(txt, offset)
415 def _get_int(txt: str) -> int:
416 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
418 while not txt[-1].isdigit():
422 # -- overridden methods invoked by parse walk. Note: not part of the class'
425 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
428 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
431 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
432 """Populate self.datetime."""
433 if self.date is None:
434 self.date = self.today
435 year = self.date.year
436 month = self.date.month
439 if self.time is None:
440 self.time = datetime.time(0, 0, 0)
441 hour = self.time.hour
442 minute = self.time.minute
443 second = self.time.second
444 micros = self.time.microsecond
446 self.datetime = datetime.datetime(
454 tzinfo=self.time.tzinfo,
457 # Apply resudual adjustments to times here when we have a
459 self.datetime = self.datetime + self.timedelta
460 assert self.datetime is not None
461 self.time = datetime.time(
463 self.datetime.minute,
464 self.datetime.second,
465 self.datetime.microsecond,
466 self.datetime.tzinfo,
469 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
471 if ctx.singleDateExpr() is not None:
472 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
473 elif ctx.baseAndOffsetDateExpr() is not None:
474 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
476 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
478 if ctx.singleTimeExpr() is not None:
479 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
480 elif ctx.baseAndOffsetTimeExpr() is not None:
481 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
483 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
484 """When we leave the date expression, populate self.date."""
485 if 'special' in self.context:
486 self.date = self._parse_special_date(self.context['special'])
488 self.date = self._parse_normal_date()
489 assert self.date is not None
491 # For a single date, just return the date we pulled out.
492 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
495 # Otherwise treat self.date as a base date that we're modifying
497 if 'delta_int' not in self.context:
498 raise ParseException('Missing delta_int?!')
499 count = self.context['delta_int']
503 # Adjust count's sign based on the presence of 'before' or 'after'.
504 if 'delta_before_after' in self.context:
505 before_after = self.context['delta_before_after'].lower()
506 if before_after in ('before', 'until', 'til', 'to'):
509 # What are we counting units of?
510 if 'delta_unit' not in self.context:
511 raise ParseException('Missing delta_unit?!')
512 unit = self.context['delta_unit']
513 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
514 self.date = datetime_to_date(dt)
516 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
518 self.time = datetime.time(
519 self.context['hour'],
520 self.context['minute'],
521 self.context['seconds'],
522 self.context['micros'],
523 tzinfo=self.context.get('tz', None),
525 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
528 # If we get here there (should be) a relative adjustment to
530 if 'nth' in self.context:
531 count = self.context['nth']
532 elif 'time_delta_int' in self.context:
533 count = self.context['time_delta_int']
535 raise ParseException('Missing delta in relative time.')
539 # Adjust count's sign based on the presence of 'before' or 'after'.
540 if 'time_delta_before_after' in self.context:
541 before_after = self.context['time_delta_before_after'].lower()
542 if before_after in ('before', 'until', 'til', 'to'):
545 # What are we counting units of... assume minutes.
546 if 'time_delta_unit' not in self.context:
547 self.timedelta += datetime.timedelta(minutes=count)
549 unit = self.context['time_delta_unit']
550 if unit == TimeUnit.SECONDS:
551 self.timedelta += datetime.timedelta(seconds=count)
552 elif unit == TimeUnit.MINUTES:
553 self.timedelta = datetime.timedelta(minutes=count)
554 elif unit == TimeUnit.HOURS:
555 self.timedelta = datetime.timedelta(hours=count)
557 raise ParseException(f'Invalid Unit: "{unit}"')
559 def exitDeltaPlusMinusExpr(self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext) -> None:
563 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
565 n = DateParser._get_int(n)
566 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
567 except Exception as e:
568 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
570 self.context['delta_int'] = n
571 self.context['delta_unit'] = unit
573 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
575 unit = self._figure_out_date_unit(ctx.getText().lower())
576 except Exception as e:
577 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
579 self.context['delta_unit'] = unit
581 def exitDeltaNextLast(self, ctx: dateparse_utilsParser.DeltaNextLastContext) -> None:
583 txt = ctx.getText().lower()
584 except Exception as e:
585 raise ParseException(f'Bad next/last: {ctx.getText()}') from e
586 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
587 raise ParseException('Next/last expression expected to be relative to today.')
588 if txt[:4] == 'next':
589 self.context['delta_int'] = +1
590 self.context['day'] = self.now_datetime.day
591 self.context['month'] = self.now_datetime.month
592 self.context['year'] = self.now_datetime.year
593 self.saw_overt_year = True
594 elif txt[:4] == 'last':
595 self.context['delta_int'] = -1
596 self.context['day'] = self.now_datetime.day
597 self.context['month'] = self.now_datetime.month
598 self.context['year'] = self.now_datetime.year
599 self.saw_overt_year = True
601 raise ParseException(f'Bad next/last: {ctx.getText()}')
603 def exitCountUnitsBeforeAfterTimeExpr(
604 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
606 if 'nth' not in self.context:
607 raise ParseException(f'Bad count expression: {ctx.getText()}')
609 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
610 self.context['time_delta_unit'] = unit
611 except Exception as e:
612 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
613 if 'time_delta_before_after' not in self.context:
614 raise ParseException(f'Bad Before/After: {ctx.getText()}')
616 def exitDeltaTimeFraction(self, ctx: dateparse_utilsParser.DeltaTimeFractionContext) -> None:
618 txt = ctx.getText().lower()[:4]
620 self.context['time_delta_int'] = 15
621 self.context['time_delta_unit'] = TimeUnit.MINUTES
623 self.context['time_delta_int'] = 30
624 self.context['time_delta_unit'] = TimeUnit.MINUTES
626 raise ParseException(f'Bad time fraction {ctx.getText()}')
627 except Exception as e:
628 raise ParseException(f'Bad time fraction {ctx.getText()}') from e
630 def exitDeltaBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
632 txt = ctx.getText().lower()
633 except Exception as e:
634 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
636 self.context['delta_before_after'] = txt
638 def exitDeltaTimeBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
640 txt = ctx.getText().lower()
641 except Exception as e:
642 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
644 self.context['time_delta_before_after'] = txt
646 def exitNthWeekdayInMonthMaybeYearExpr(
647 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
649 """Do a bunch of work to convert expressions like...
651 'the 2nd Friday of June' -and-
652 'the last Wednesday in October'
654 ...into base + offset expressions instead.
657 if 'nth' not in self.context:
658 raise ParseException(f'Missing nth number: {ctx.getText()}')
659 n = self.context['nth']
660 if n < 1 or n > 5: # months never have more than 5 Foodays
662 raise ParseException(f'Invalid nth number: {ctx.getText()}')
663 del self.context['nth']
664 self.context['delta_int'] = n
666 year = self.context.get('year', self.today.year)
667 if 'month' not in self.context:
668 raise ParseException(f'Missing month expression: {ctx.getText()}')
669 month = self.context['month']
671 dow = self.context['dow']
672 del self.context['dow']
673 self.context['delta_unit'] = dow
675 # For the nth Fooday in Month, start at the last day of
676 # the previous month count ahead N Foodays. For the last
677 # Fooday in Month, start at the last of the month and
678 # count back one Fooday.
684 tmp_date = datetime.date(year=year, month=month, day=1)
685 tmp_date = tmp_date - datetime.timedelta(days=1)
687 # The delta adjustment code can handle the case where
688 # the last day of the month is the day we're looking
691 tmp_date = datetime.date(year=year, month=month, day=1)
692 tmp_date = tmp_date - datetime.timedelta(days=1)
694 self.context['year'] = tmp_date.year
695 self.context['month'] = tmp_date.month
696 self.context['day'] = tmp_date.day
697 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
698 except Exception as e:
699 raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}') from e
701 def exitFirstLastWeekdayInMonthMaybeYearExpr(
703 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
705 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
707 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
709 i = DateParser._get_int(ctx.getText())
710 except Exception as e:
711 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
713 self.context['nth'] = i
715 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
723 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
724 except Exception as e:
725 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
727 self.context['nth'] = txt
729 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
731 dow = ctx.getText().lower()[:3]
732 dow = self.day_name_to_number.get(dow, None)
733 except Exception as e:
734 raise ParseException('Bad day of week') from e
736 self.context['dow'] = dow
738 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
740 day = ctx.getText().lower()
742 self.context['day'] = 'ide'
745 self.context['day'] = 'non'
748 self.context['day'] = 1
750 day = DateParser._get_int(day)
751 if day < 1 or day > 31:
752 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
753 except Exception as e:
754 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
755 self.context['day'] = day
757 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
759 month = ctx.getText()
760 while month[0] == '/' or month[0] == '-':
762 month = month[:3].lower()
763 month = self.month_name_to_number.get(month, None)
765 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
766 except Exception as e:
767 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
769 self.context['month'] = month
771 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
773 month = DateParser._get_int(ctx.getText())
774 if month < 1 or month > 12:
775 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
776 except Exception as e:
777 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
779 self.context['month'] = month
781 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
783 year = DateParser._get_int(ctx.getText())
785 raise ParseException(f'Bad year expression: {ctx.getText()}')
786 except Exception as e:
787 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
789 self.saw_overt_year = True
790 self.context['year'] = year
792 def exitSpecialDateMaybeYearExpr(
793 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
796 special = ctx.specialDate().getText().lower()
797 self.context['special'] = special
798 except Exception as e:
799 raise ParseException(
800 f'Bad specialDate expression: {ctx.specialDate().getText()}'
803 mod = ctx.thisNextLast()
805 if mod.THIS() is not None:
806 self.context['special_next_last'] = 'this'
807 elif mod.NEXT() is not None:
808 self.context['special_next_last'] = 'next'
809 elif mod.LAST() is not None:
810 self.context['special_next_last'] = 'last'
811 except Exception as e:
812 raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}') from e
814 def exitNFoosFromTodayAgoExpr(
815 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
817 d = self.now_datetime
819 count = DateParser._get_int(ctx.unsignedInt().getText())
820 unit = ctx.deltaUnit().getText().lower()
821 ago_from_now = ctx.AGO_FROM_NOW().getText()
822 except Exception as e:
823 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
825 if "ago" in ago_from_now or "back" in ago_from_now:
828 unit = self._figure_out_date_unit(unit)
829 d = n_timeunits_from_base(count, TimeUnit(unit), d)
830 self.context['year'] = d.year
831 self.context['month'] = d.month
832 self.context['day'] = d.day
834 def exitDeltaRelativeToTodayExpr(
835 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
837 # When someone says "next week" they mean a week from now.
838 # Likewise next month or last year. These expressions are now
841 # But when someone says "this Friday" they mean "this coming
842 # Friday". It would be weird to say "this Friday" if today
843 # was already Friday but I'm parsing it to mean: the next day
844 # that is a Friday. So when you say "next Friday" you mean
845 # the Friday after this coming Friday, or 2 Fridays from now.
847 # This set handles this weirdness.
859 d = self.now_datetime
861 mod = ctx.thisNextLast()
862 unit = ctx.deltaUnit().getText().lower()
863 unit = self._figure_out_date_unit(unit)
877 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
878 except Exception as e:
879 raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}') from e
880 d = n_timeunits_from_base(count, TimeUnit(unit), d)
881 self.context['year'] = d.year
882 self.context['month'] = d.month
883 self.context['day'] = d.day
885 def exitSpecialTimeExpr(self, ctx: dateparse_utilsParser.SpecialTimeExprContext) -> None:
887 txt = ctx.specialTime().getText().lower()
888 except Exception as e:
889 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
891 if txt in ('noon', 'midday'):
892 self.context['hour'] = 12
893 self.context['minute'] = 0
894 self.context['seconds'] = 0
895 self.context['micros'] = 0
896 elif txt == 'midnight':
897 self.context['hour'] = 0
898 self.context['minute'] = 0
899 self.context['seconds'] = 0
900 self.context['micros'] = 0
902 raise ParseException(f'Bad special time expression: {txt}')
905 tz = ctx.tzExpr().getText()
906 self.context['tz'] = DateParser._parse_tz(tz)
910 def exitTwelveHourTimeExpr(self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext) -> None:
912 hour = ctx.hour().getText()
913 while not hour[-1].isdigit():
915 hour = DateParser._get_int(hour)
916 except Exception as e:
917 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
918 if hour <= 0 or hour > 12:
919 raise ParseException(f'Bad hour (out of range): {hour}')
922 minute = DateParser._get_int(ctx.minute().getText())
925 if minute < 0 or minute > 59:
926 raise ParseException(f'Bad minute (out of range): {minute}')
927 self.context['minute'] = minute
930 seconds = DateParser._get_int(ctx.second().getText())
933 if seconds < 0 or seconds > 59:
934 raise ParseException(f'Bad second (out of range): {seconds}')
935 self.context['seconds'] = seconds
938 micros = DateParser._get_int(ctx.micros().getText())
941 if micros < 0 or micros > 1000000:
942 raise ParseException(f'Bad micros (out of range): {micros}')
943 self.context['micros'] = micros
946 ampm = ctx.ampm().getText()
947 except Exception as e:
948 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
953 self.context['hour'] = hour
956 tz = ctx.tzExpr().getText()
957 self.context['tz'] = DateParser._parse_tz(tz)
961 def exitTwentyFourHourTimeExpr(
962 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
965 hour = ctx.hour().getText()
966 while not hour[-1].isdigit():
968 hour = DateParser._get_int(hour)
969 except Exception as e:
970 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
971 if hour < 0 or hour > 23:
972 raise ParseException(f'Bad hour (out of range): {hour}')
973 self.context['hour'] = hour
976 minute = DateParser._get_int(ctx.minute().getText())
979 if minute < 0 or minute > 59:
980 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
981 self.context['minute'] = minute
984 seconds = DateParser._get_int(ctx.second().getText())
987 if seconds < 0 or seconds > 59:
988 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
989 self.context['seconds'] = seconds
992 micros = DateParser._get_int(ctx.micros().getText())
995 if micros < 0 or micros >= 1000000:
996 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
997 self.context['micros'] = micros
1000 tz = ctx.tzExpr().getText()
1001 self.context['tz'] = DateParser._parse_tz(tz)
1006 @bootstrap.initialize
1008 parser = DateParser()
1009 for line in sys.stdin:
1011 line = re.sub(r"#.*$", "", line)
1012 if re.match(r"^ *$", line) is not None:
1015 dt = parser.parse(line)
1016 except Exception as e:
1018 print("Unrecognized.")
1020 assert dt is not None
1021 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1025 if __name__ == "__main__":