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 1st of the
676 # month and count ahead N Foodays. For the last Fooday in
677 # Month, start at the last of the month and count back one
684 tmp_date = datetime.date(year=year, month=month, day=1)
685 tmp_date = tmp_date - datetime.timedelta(days=1)
687 self.context['year'] = tmp_date.year
688 self.context['month'] = tmp_date.month
689 self.context['day'] = tmp_date.day
691 # The delta adjustment code can handle the case where
692 # the last day of the month is the day we're looking
695 self.context['year'] = year
696 self.context['month'] = month
697 self.context['day'] = 1
698 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
699 except Exception as e:
700 raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}') from e
702 def exitFirstLastWeekdayInMonthMaybeYearExpr(
704 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
706 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
708 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
710 i = DateParser._get_int(ctx.getText())
711 except Exception as e:
712 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
714 self.context['nth'] = i
716 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
724 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
725 except Exception as e:
726 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
728 self.context['nth'] = txt
730 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
732 dow = ctx.getText().lower()[:3]
733 dow = self.day_name_to_number.get(dow, None)
734 except Exception as e:
735 raise ParseException('Bad day of week') from e
737 self.context['dow'] = dow
739 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
741 day = ctx.getText().lower()
743 self.context['day'] = 'ide'
746 self.context['day'] = 'non'
749 self.context['day'] = 1
751 day = DateParser._get_int(day)
752 if day < 1 or day > 31:
753 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
754 except Exception as e:
755 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
756 self.context['day'] = day
758 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
760 month = ctx.getText()
761 while month[0] == '/' or month[0] == '-':
763 month = month[:3].lower()
764 month = self.month_name_to_number.get(month, None)
766 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
767 except Exception as e:
768 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
770 self.context['month'] = month
772 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
774 month = DateParser._get_int(ctx.getText())
775 if month < 1 or month > 12:
776 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
777 except Exception as e:
778 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
780 self.context['month'] = month
782 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
784 year = DateParser._get_int(ctx.getText())
786 raise ParseException(f'Bad year expression: {ctx.getText()}')
787 except Exception as e:
788 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
790 self.saw_overt_year = True
791 self.context['year'] = year
793 def exitSpecialDateMaybeYearExpr(
794 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
797 special = ctx.specialDate().getText().lower()
798 self.context['special'] = special
799 except Exception as e:
800 raise ParseException(
801 f'Bad specialDate expression: {ctx.specialDate().getText()}'
804 mod = ctx.thisNextLast()
806 if mod.THIS() is not None:
807 self.context['special_next_last'] = 'this'
808 elif mod.NEXT() is not None:
809 self.context['special_next_last'] = 'next'
810 elif mod.LAST() is not None:
811 self.context['special_next_last'] = 'last'
812 except Exception as e:
813 raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}') from e
815 def exitNFoosFromTodayAgoExpr(
816 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
818 d = self.now_datetime
820 count = DateParser._get_int(ctx.unsignedInt().getText())
821 unit = ctx.deltaUnit().getText().lower()
822 ago_from_now = ctx.AGO_FROM_NOW().getText()
823 except Exception as e:
824 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
826 if "ago" in ago_from_now or "back" in ago_from_now:
829 unit = self._figure_out_date_unit(unit)
830 d = n_timeunits_from_base(count, TimeUnit(unit), d)
831 self.context['year'] = d.year
832 self.context['month'] = d.month
833 self.context['day'] = d.day
835 def exitDeltaRelativeToTodayExpr(
836 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
838 # When someone says "next week" they mean a week from now.
839 # Likewise next month or last year. These expressions are now
842 # But when someone says "this Friday" they mean "this coming
843 # Friday". It would be weird to say "this Friday" if today
844 # was already Friday but I'm parsing it to mean: the next day
845 # that is a Friday. So when you say "next Friday" you mean
846 # the Friday after this coming Friday, or 2 Fridays from now.
848 # This set handles this weirdness.
860 d = self.now_datetime
862 mod = ctx.thisNextLast()
863 unit = ctx.deltaUnit().getText().lower()
864 unit = self._figure_out_date_unit(unit)
878 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
879 except Exception as e:
880 raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}') from e
881 d = n_timeunits_from_base(count, TimeUnit(unit), d)
882 self.context['year'] = d.year
883 self.context['month'] = d.month
884 self.context['day'] = d.day
886 def exitSpecialTimeExpr(self, ctx: dateparse_utilsParser.SpecialTimeExprContext) -> None:
888 txt = ctx.specialTime().getText().lower()
889 except Exception as e:
890 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
892 if txt in ('noon', 'midday'):
893 self.context['hour'] = 12
894 self.context['minute'] = 0
895 self.context['seconds'] = 0
896 self.context['micros'] = 0
897 elif txt == 'midnight':
898 self.context['hour'] = 0
899 self.context['minute'] = 0
900 self.context['seconds'] = 0
901 self.context['micros'] = 0
903 raise ParseException(f'Bad special time expression: {txt}')
906 tz = ctx.tzExpr().getText()
907 self.context['tz'] = DateParser._parse_tz(tz)
911 def exitTwelveHourTimeExpr(self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext) -> None:
913 hour = ctx.hour().getText()
914 while not hour[-1].isdigit():
916 hour = DateParser._get_int(hour)
917 except Exception as e:
918 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
919 if hour <= 0 or hour > 12:
920 raise ParseException(f'Bad hour (out of range): {hour}')
923 minute = DateParser._get_int(ctx.minute().getText())
926 if minute < 0 or minute > 59:
927 raise ParseException(f'Bad minute (out of range): {minute}')
928 self.context['minute'] = minute
931 seconds = DateParser._get_int(ctx.second().getText())
934 if seconds < 0 or seconds > 59:
935 raise ParseException(f'Bad second (out of range): {seconds}')
936 self.context['seconds'] = seconds
939 micros = DateParser._get_int(ctx.micros().getText())
942 if micros < 0 or micros > 1000000:
943 raise ParseException(f'Bad micros (out of range): {micros}')
944 self.context['micros'] = micros
947 ampm = ctx.ampm().getText()
948 except Exception as e:
949 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
954 self.context['hour'] = hour
957 tz = ctx.tzExpr().getText()
958 self.context['tz'] = DateParser._parse_tz(tz)
962 def exitTwentyFourHourTimeExpr(
963 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
966 hour = ctx.hour().getText()
967 while not hour[-1].isdigit():
969 hour = DateParser._get_int(hour)
970 except Exception as e:
971 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
972 if hour < 0 or hour > 23:
973 raise ParseException(f'Bad hour (out of range): {hour}')
974 self.context['hour'] = hour
977 minute = DateParser._get_int(ctx.minute().getText())
980 if minute < 0 or minute > 59:
981 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
982 self.context['minute'] = minute
985 seconds = DateParser._get_int(ctx.second().getText())
988 if seconds < 0 or seconds > 59:
989 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
990 self.context['seconds'] = seconds
993 micros = DateParser._get_int(ctx.micros().getText())
996 if micros < 0 or micros >= 1000000:
997 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
998 self.context['micros'] = micros
1001 tz = ctx.tzExpr().getText()
1002 self.context['tz'] = DateParser._parse_tz(tz)
1007 @bootstrap.initialize
1009 parser = DateParser()
1010 for line in sys.stdin:
1012 line = re.sub(r"#.*$", "", line)
1013 if re.match(r"^ *$", line) is not None:
1016 dt = parser.parse(line)
1017 except Exception as e:
1019 print("Unrecognized.")
1021 assert dt is not None
1022 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1026 if __name__ == "__main__":