3 # pylint: disable=W0201
4 # pylint: disable=R0904
6 """Parse dates in a variety of formats."""
13 from typing import Any, Callable, Dict, Optional
15 import antlr4 # type: ignore
16 import dateutil.easter
18 import holidays # type: ignore
23 import decorator_utils
24 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
25 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
26 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
27 from datetime_utils import (
31 n_timeunits_from_base,
34 logger = logging.getLogger(__name__)
37 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
38 @functools.wraps(enter_or_exit_f)
39 def debug_parse_wrapper(*args, **kwargs):
45 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
47 for c in ctx.getChildren():
48 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
49 retval = enter_or_exit_f(*args, **kwargs)
52 return debug_parse_wrapper
55 class ParseException(Exception):
56 """An exception thrown during parsing because of unrecognized input."""
58 def __init__(self, message: str) -> None:
60 self.message = message
63 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
64 """An error listener that raises ParseExceptions."""
66 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
67 raise ParseException(msg)
69 def reportAmbiguity(self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs):
72 def reportAttemptingFullContext(
73 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
77 def reportContextSensitivity(self, recognizer, dfa, startIndex, stopIndex, prediction, configs):
81 @decorator_utils.decorate_matching_methods_with(
83 acl=acl.StringWildcardBasedACL(
88 denied_patterns=['enterEveryRule', 'exitEveryRule'],
89 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
93 class DateParser(dateparse_utilsListener):
94 """A class to parse dates expressed in human language."""
96 PARSE_TYPE_SINGLE_DATE_EXPR = 1
97 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
98 PARSE_TYPE_SINGLE_TIME_EXPR = 3
99 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
101 def __init__(self, *, override_now_for_test_purposes=None) -> None:
102 """C'tor. Passing a value to override_now_for_test_purposes can be
103 used to force this instance to use a custom date/time for its
104 idea of "now" so that the code can be more easily unittested.
105 Leave as None for real use cases.
107 self.month_name_to_number = {
122 # Used only for ides/nones. Month length on a non-leap year.
123 self.typical_days_per_month = {
138 # N.B. day number is also synched with datetime_utils.TimeUnit values
139 # which allows expressions like "3 wednesdays from now" to work.
140 self.day_name_to_number = {
150 # These TimeUnits are defined in datetime_utils and are used as params
151 # to datetime_utils.n_timeunits_from_base.
152 self.time_delta_unit_to_constant = {
153 'hou': TimeUnit.HOURS,
154 'min': TimeUnit.MINUTES,
155 'sec': TimeUnit.SECONDS,
157 self.delta_unit_to_constant = {
158 'day': TimeUnit.DAYS,
159 'wor': TimeUnit.WORKDAYS,
160 'wee': TimeUnit.WEEKS,
161 'mon': TimeUnit.MONTHS,
162 'yea': TimeUnit.YEARS,
164 self.override_now_for_test_purposes = override_now_for_test_purposes
166 # Note: _reset defines several class fields. It is used both here
167 # in the c'tor but also in between parse operations to restore the
168 # class' state and allow it to be reused.
172 def parse(self, date_string: str) -> Optional[datetime.datetime]:
173 """Parse a date/time expression and return a timezone agnostic
174 datetime on success. Also sets self.datetime, self.date and
175 self.time which can each be accessed other methods on the
176 class: get_datetime(), get_date() and get_time(). Raises a
177 ParseException with a helpful(?) message on parse error or
180 To get an idea of what expressions can be parsed, check out
181 the unittest and the grammar.
185 txt = '3 weeks before last tues at 9:15am'
188 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
190 # dt1 and dt2 will be identical other than the fact that
191 # the latter's tzinfo will be set to PST/PDT.
193 This is the main entrypoint to this class for caller code.
195 date_string = date_string.strip()
196 date_string = re.sub(r'\s+', ' ', date_string)
198 listener = RaisingErrorListener()
199 input_stream = antlr4.InputStream(date_string)
200 lexer = dateparse_utilsLexer(input_stream)
201 lexer.removeErrorListeners()
202 lexer.addErrorListener(listener)
203 stream = antlr4.CommonTokenStream(lexer)
204 parser = dateparse_utilsParser(stream)
205 parser.removeErrorListeners()
206 parser.addErrorListener(listener)
207 tree = parser.parse()
208 walker = antlr4.ParseTreeWalker()
209 walker.walk(self, tree)
212 def get_date(self) -> Optional[datetime.date]:
213 """Return the date part or None."""
216 def get_time(self) -> Optional[datetime.time]:
217 """Return the time part or None."""
220 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
221 """Return as a datetime. Parsed date expressions without any time
222 part return hours = minutes = seconds = microseconds = 0 (i.e. at
223 midnight that day). Parsed time expressions without any date part
224 default to date = today.
226 The optional tz param allows the caller to request the datetime be
227 timezone aware and sets the tzinfo to the indicated zone. Defaults
228 to timezone naive (i.e. tzinfo = None).
233 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
239 """Reset at init and between parses."""
240 if self.override_now_for_test_purposes is None:
241 self.now_datetime = datetime.datetime.now()
242 self.today = datetime.date.today()
244 self.now_datetime = self.override_now_for_test_purposes
245 self.today = datetime_to_date(self.override_now_for_test_purposes)
246 self.date: Optional[datetime.date] = None
247 self.time: Optional[datetime.time] = None
248 self.datetime: Optional[datetime.datetime] = None
249 self.context: Dict[str, Any] = {}
250 self.timedelta = datetime.timedelta(seconds=0)
251 self.saw_overt_year = False
254 def _normalize_special_day_name(name: str) -> str:
255 """String normalization / canonicalization for date expressions."""
257 name = name.replace("'", '')
258 name = name.replace('xmas', 'christmas')
259 name = name.replace('mlk', 'martin luther king')
260 name = name.replace(' ', '')
261 eve = 'eve' if name[-3:] == 'eve' else ''
262 name = name[:5] + eve
263 name = name.replace('washi', 'presi')
266 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
267 """Figure out what unit a date expression piece is talking about."""
269 return TimeUnit.MONTHS
270 txt = orig.lower()[:3]
271 if txt in self.day_name_to_number:
272 return TimeUnit(self.day_name_to_number[txt])
273 elif txt in self.delta_unit_to_constant:
274 return TimeUnit(self.delta_unit_to_constant[txt])
275 raise ParseException(f'Invalid date unit: {orig}')
277 def _figure_out_time_unit(self, orig: str) -> int:
278 """Figure out what unit a time expression piece is talking about."""
279 txt = orig.lower()[:3]
280 if txt in self.time_delta_unit_to_constant:
281 return self.time_delta_unit_to_constant[txt]
282 raise ParseException(f'Invalid time unit: {orig}')
284 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
285 """Parse what we think is a special date name and return its datetime
286 (or None if it can't be parsed).
289 year = self.context.get('year', today.year)
290 name = DateParser._normalize_special_day_name(self.context['special'])
292 # Yesterday, today, tomorrow -- ignore any next/last
293 if name in ('today', 'now'):
296 return today + datetime.timedelta(days=-1)
298 return today + datetime.timedelta(days=+1)
300 next_last = self.context.get('special_next_last', '')
301 if next_last == 'next':
303 self.saw_overt_year = True
304 elif next_last == 'last':
306 self.saw_overt_year = True
310 return dateutil.easter.easter(year=year)
311 elif name == 'hallo':
312 return datetime.date(year=year, month=10, day=31)
314 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
315 if 'Observed' not in holiday_name:
316 holiday_name = DateParser._normalize_special_day_name(holiday_name)
317 if name == holiday_name:
319 if name == 'chriseve':
320 return datetime.date(year=year, month=12, day=24)
321 elif name == 'newyeeve':
322 return datetime.date(year=year, month=12, day=31)
325 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
326 """Handle date expressions like "the ides of March" which require
327 both the "ides" and the month since the definition of the "ides"
328 changes based on the length of the month.
330 assert 'ide' in day or 'non' in day
331 assert month_number in self.typical_days_per_month
332 typical_days_per_month = self.typical_days_per_month[month_number]
335 if typical_days_per_month == 31:
336 if self.context['day'] == 'ide':
343 if self.context['day'] == 'ide':
348 def _parse_normal_date(self) -> datetime.date:
349 if 'dow' in self.context:
351 while d.weekday() != self.context['dow']:
352 d += datetime.timedelta(days=1)
355 if 'month' not in self.context:
356 raise ParseException('Missing month')
357 if 'day' not in self.context:
358 raise ParseException('Missing day')
359 if 'year' not in self.context:
360 self.context['year'] = self.today.year
361 self.saw_overt_year = False
363 self.saw_overt_year = True
365 # Handling "ides" and "nones" requires both the day and month.
366 if self.context['day'] == 'ide' or self.context['day'] == 'non':
367 self.context['day'] = self._resolve_ides_nones(
368 self.context['day'], self.context['month']
371 return datetime.date(
372 year=self.context['year'],
373 month=self.context['month'],
374 day=self.context['day'],
378 def _parse_tz(txt: str) -> Any:
384 tz1 = pytz.timezone(txt)
392 tz2 = dateutil.tz.gettz(txt)
398 # Try constructing an offset in seconds
401 if txt_sign in ('-', '+'):
402 sign = +1 if txt_sign == '+' else -1
404 minute = int(txt[-2:])
405 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
406 tzoffset = dateutil.tz.tzoffset(txt, offset)
413 def _get_int(txt: str) -> int:
414 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
416 while not txt[-1].isdigit():
420 # -- overridden methods invoked by parse walk. Note: not part of the class'
423 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
426 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
429 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
430 """Populate self.datetime."""
431 if self.date is None:
432 self.date = self.today
433 year = self.date.year
434 month = self.date.month
437 if self.time is None:
438 self.time = datetime.time(0, 0, 0)
439 hour = self.time.hour
440 minute = self.time.minute
441 second = self.time.second
442 micros = self.time.microsecond
444 self.datetime = datetime.datetime(
452 tzinfo=self.time.tzinfo,
455 # Apply resudual adjustments to times here when we have a
457 self.datetime = self.datetime + self.timedelta
458 assert self.datetime is not None
459 self.time = datetime.time(
461 self.datetime.minute,
462 self.datetime.second,
463 self.datetime.microsecond,
464 self.datetime.tzinfo,
467 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
469 if ctx.singleDateExpr() is not None:
470 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
471 elif ctx.baseAndOffsetDateExpr() is not None:
472 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
474 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
476 if ctx.singleTimeExpr() is not None:
477 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
478 elif ctx.baseAndOffsetTimeExpr() is not None:
479 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
481 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
482 """When we leave the date expression, populate self.date."""
483 if 'special' in self.context:
484 self.date = self._parse_special_date(self.context['special'])
486 self.date = self._parse_normal_date()
487 assert self.date is not None
489 # For a single date, just return the date we pulled out.
490 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
493 # Otherwise treat self.date as a base date that we're modifying
495 if 'delta_int' not in self.context:
496 raise ParseException('Missing delta_int?!')
497 count = self.context['delta_int']
501 # Adjust count's sign based on the presence of 'before' or 'after'.
502 if 'delta_before_after' in self.context:
503 before_after = self.context['delta_before_after'].lower()
504 if before_after in ('before', 'until', 'til', 'to'):
507 # What are we counting units of?
508 if 'delta_unit' not in self.context:
509 raise ParseException('Missing delta_unit?!')
510 unit = self.context['delta_unit']
511 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
512 self.date = datetime_to_date(dt)
514 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
516 self.time = datetime.time(
517 self.context['hour'],
518 self.context['minute'],
519 self.context['seconds'],
520 self.context['micros'],
521 tzinfo=self.context.get('tz', None),
523 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
526 # If we get here there (should be) a relative adjustment to
528 if 'nth' in self.context:
529 count = self.context['nth']
530 elif 'time_delta_int' in self.context:
531 count = self.context['time_delta_int']
533 raise ParseException('Missing delta in relative time.')
537 # Adjust count's sign based on the presence of 'before' or 'after'.
538 if 'time_delta_before_after' in self.context:
539 before_after = self.context['time_delta_before_after'].lower()
540 if before_after in ('before', 'until', 'til', 'to'):
543 # What are we counting units of... assume minutes.
544 if 'time_delta_unit' not in self.context:
545 self.timedelta += datetime.timedelta(minutes=count)
547 unit = self.context['time_delta_unit']
548 if unit == TimeUnit.SECONDS:
549 self.timedelta += datetime.timedelta(seconds=count)
550 elif unit == TimeUnit.MINUTES:
551 self.timedelta = datetime.timedelta(minutes=count)
552 elif unit == TimeUnit.HOURS:
553 self.timedelta = datetime.timedelta(hours=count)
555 raise ParseException(f'Invalid Unit: "{unit}"')
557 def exitDeltaPlusMinusExpr(self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext) -> None:
561 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
563 n = DateParser._get_int(n)
564 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
565 except Exception as e:
566 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
568 self.context['delta_int'] = n
569 self.context['delta_unit'] = unit
571 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
573 unit = self._figure_out_date_unit(ctx.getText().lower())
574 except Exception as e:
575 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
577 self.context['delta_unit'] = unit
579 def exitDeltaNextLast(self, ctx: dateparse_utilsParser.DeltaNextLastContext) -> None:
581 txt = ctx.getText().lower()
582 except Exception as e:
583 raise ParseException(f'Bad next/last: {ctx.getText()}') from e
584 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
585 raise ParseException('Next/last expression expected to be relative to today.')
586 if txt[:4] == 'next':
587 self.context['delta_int'] = +1
588 self.context['day'] = self.now_datetime.day
589 self.context['month'] = self.now_datetime.month
590 self.context['year'] = self.now_datetime.year
591 self.saw_overt_year = True
592 elif txt[:4] == 'last':
593 self.context['delta_int'] = -1
594 self.context['day'] = self.now_datetime.day
595 self.context['month'] = self.now_datetime.month
596 self.context['year'] = self.now_datetime.year
597 self.saw_overt_year = True
599 raise ParseException(f'Bad next/last: {ctx.getText()}')
601 def exitCountUnitsBeforeAfterTimeExpr(
602 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
604 if 'nth' not in self.context:
605 raise ParseException(f'Bad count expression: {ctx.getText()}')
607 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
608 self.context['time_delta_unit'] = unit
609 except Exception as e:
610 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
611 if 'time_delta_before_after' not in self.context:
612 raise ParseException(f'Bad Before/After: {ctx.getText()}')
614 def exitDeltaTimeFraction(self, ctx: dateparse_utilsParser.DeltaTimeFractionContext) -> None:
616 txt = ctx.getText().lower()[:4]
618 self.context['time_delta_int'] = 15
619 self.context['time_delta_unit'] = TimeUnit.MINUTES
621 self.context['time_delta_int'] = 30
622 self.context['time_delta_unit'] = TimeUnit.MINUTES
624 raise ParseException(f'Bad time fraction {ctx.getText()}')
625 except Exception as e:
626 raise ParseException(f'Bad time fraction {ctx.getText()}') from e
628 def exitDeltaBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
630 txt = ctx.getText().lower()
631 except Exception as e:
632 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
634 self.context['delta_before_after'] = txt
636 def exitDeltaTimeBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
638 txt = ctx.getText().lower()
639 except Exception as e:
640 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
642 self.context['time_delta_before_after'] = txt
644 def exitNthWeekdayInMonthMaybeYearExpr(
645 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
647 """Do a bunch of work to convert expressions like...
649 'the 2nd Friday of June' -and-
650 'the last Wednesday in October'
652 ...into base + offset expressions instead.
655 if 'nth' not in self.context:
656 raise ParseException(f'Missing nth number: {ctx.getText()}')
657 n = self.context['nth']
658 if n < 1 or n > 5: # months never have more than 5 Foodays
660 raise ParseException(f'Invalid nth number: {ctx.getText()}')
661 del self.context['nth']
662 self.context['delta_int'] = n
664 year = self.context.get('year', self.today.year)
665 if 'month' not in self.context:
666 raise ParseException(f'Missing month expression: {ctx.getText()}')
667 month = self.context['month']
669 dow = self.context['dow']
670 del self.context['dow']
671 self.context['delta_unit'] = dow
673 # For the nth Fooday in Month, start at the 1st of the
674 # month and count ahead N Foodays. For the last Fooday in
675 # Month, start at the last of the month and count back one
682 tmp_date = datetime.date(year=year, month=month, day=1)
683 tmp_date = tmp_date - datetime.timedelta(days=1)
685 self.context['year'] = tmp_date.year
686 self.context['month'] = tmp_date.month
687 self.context['day'] = tmp_date.day
689 # The delta adjustment code can handle the case where
690 # the last day of the month is the day we're looking
693 self.context['year'] = year
694 self.context['month'] = month
695 self.context['day'] = 1
696 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
697 except Exception as e:
698 raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}') from e
700 def exitFirstLastWeekdayInMonthMaybeYearExpr(
702 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
704 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
706 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
708 i = DateParser._get_int(ctx.getText())
709 except Exception as e:
710 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
712 self.context['nth'] = i
714 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
722 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
723 except Exception as e:
724 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
726 self.context['nth'] = txt
728 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
730 dow = ctx.getText().lower()[:3]
731 dow = self.day_name_to_number.get(dow, None)
732 except Exception as e:
733 raise ParseException('Bad day of week') from e
735 self.context['dow'] = dow
737 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
739 day = ctx.getText().lower()
741 self.context['day'] = 'ide'
744 self.context['day'] = 'non'
747 self.context['day'] = 1
749 day = DateParser._get_int(day)
750 if day < 1 or day > 31:
751 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
752 except Exception as e:
753 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
754 self.context['day'] = day
756 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
758 month = ctx.getText()
759 while month[0] == '/' or month[0] == '-':
761 month = month[:3].lower()
762 month = self.month_name_to_number.get(month, None)
764 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
765 except Exception as e:
766 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
768 self.context['month'] = month
770 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
772 month = DateParser._get_int(ctx.getText())
773 if month < 1 or month > 12:
774 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
775 except Exception as e:
776 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
778 self.context['month'] = month
780 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
782 year = DateParser._get_int(ctx.getText())
784 raise ParseException(f'Bad year expression: {ctx.getText()}')
785 except Exception as e:
786 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
788 self.saw_overt_year = True
789 self.context['year'] = year
791 def exitSpecialDateMaybeYearExpr(
792 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
795 special = ctx.specialDate().getText().lower()
796 self.context['special'] = special
797 except Exception as e:
798 raise ParseException(
799 f'Bad specialDate expression: {ctx.specialDate().getText()}'
802 mod = ctx.thisNextLast()
804 if mod.THIS() is not None:
805 self.context['special_next_last'] = 'this'
806 elif mod.NEXT() is not None:
807 self.context['special_next_last'] = 'next'
808 elif mod.LAST() is not None:
809 self.context['special_next_last'] = 'last'
810 except Exception as e:
811 raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}') from e
813 def exitNFoosFromTodayAgoExpr(
814 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
816 d = self.now_datetime
818 count = DateParser._get_int(ctx.unsignedInt().getText())
819 unit = ctx.deltaUnit().getText().lower()
820 ago_from_now = ctx.AGO_FROM_NOW().getText()
821 except Exception as e:
822 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
824 if "ago" in ago_from_now or "back" in ago_from_now:
827 unit = self._figure_out_date_unit(unit)
828 d = n_timeunits_from_base(count, TimeUnit(unit), d)
829 self.context['year'] = d.year
830 self.context['month'] = d.month
831 self.context['day'] = d.day
833 def exitDeltaRelativeToTodayExpr(
834 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
836 # When someone says "next week" they mean a week from now.
837 # Likewise next month or last year. These expressions are now
840 # But when someone says "this Friday" they mean "this coming
841 # Friday". It would be weird to say "this Friday" if today
842 # was already Friday but I'm parsing it to mean: the next day
843 # that is a Friday. So when you say "next Friday" you mean
844 # the Friday after this coming Friday, or 2 Fridays from now.
846 # This set handles this weirdness.
858 d = self.now_datetime
860 mod = ctx.thisNextLast()
861 unit = ctx.deltaUnit().getText().lower()
862 unit = self._figure_out_date_unit(unit)
876 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
877 except Exception as e:
878 raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}') from e
879 d = n_timeunits_from_base(count, TimeUnit(unit), d)
880 self.context['year'] = d.year
881 self.context['month'] = d.month
882 self.context['day'] = d.day
884 def exitSpecialTimeExpr(self, ctx: dateparse_utilsParser.SpecialTimeExprContext) -> None:
886 txt = ctx.specialTime().getText().lower()
887 except Exception as e:
888 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
890 if txt in ('noon', 'midday'):
891 self.context['hour'] = 12
892 self.context['minute'] = 0
893 self.context['seconds'] = 0
894 self.context['micros'] = 0
895 elif txt == 'midnight':
896 self.context['hour'] = 0
897 self.context['minute'] = 0
898 self.context['seconds'] = 0
899 self.context['micros'] = 0
901 raise ParseException(f'Bad special time expression: {txt}')
904 tz = ctx.tzExpr().getText()
905 self.context['tz'] = DateParser._parse_tz(tz)
909 def exitTwelveHourTimeExpr(self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext) -> None:
911 hour = ctx.hour().getText()
912 while not hour[-1].isdigit():
914 hour = DateParser._get_int(hour)
915 except Exception as e:
916 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
917 if hour <= 0 or hour > 12:
918 raise ParseException(f'Bad hour (out of range): {hour}')
921 minute = DateParser._get_int(ctx.minute().getText())
924 if minute < 0 or minute > 59:
925 raise ParseException(f'Bad minute (out of range): {minute}')
926 self.context['minute'] = minute
929 seconds = DateParser._get_int(ctx.second().getText())
932 if seconds < 0 or seconds > 59:
933 raise ParseException(f'Bad second (out of range): {seconds}')
934 self.context['seconds'] = seconds
937 micros = DateParser._get_int(ctx.micros().getText())
940 if micros < 0 or micros > 1000000:
941 raise ParseException(f'Bad micros (out of range): {micros}')
942 self.context['micros'] = micros
945 ampm = ctx.ampm().getText()
946 except Exception as e:
947 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
952 self.context['hour'] = hour
955 tz = ctx.tzExpr().getText()
956 self.context['tz'] = DateParser._parse_tz(tz)
960 def exitTwentyFourHourTimeExpr(
961 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
964 hour = ctx.hour().getText()
965 while not hour[-1].isdigit():
967 hour = DateParser._get_int(hour)
968 except Exception as e:
969 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
970 if hour < 0 or hour > 23:
971 raise ParseException(f'Bad hour (out of range): {hour}')
972 self.context['hour'] = hour
975 minute = DateParser._get_int(ctx.minute().getText())
978 if minute < 0 or minute > 59:
979 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
980 self.context['minute'] = minute
983 seconds = DateParser._get_int(ctx.second().getText())
986 if seconds < 0 or seconds > 59:
987 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
988 self.context['seconds'] = seconds
991 micros = DateParser._get_int(ctx.micros().getText())
994 if micros < 0 or micros >= 1000000:
995 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
996 self.context['micros'] = micros
999 tz = ctx.tzExpr().getText()
1000 self.context['tz'] = DateParser._parse_tz(tz)
1005 @bootstrap.initialize
1007 parser = DateParser()
1008 for line in sys.stdin:
1010 line = re.sub(r"#.*$", "", line)
1011 if re.match(r"^ *$", line) is not None:
1014 dt = parser.parse(line)
1015 except Exception as e:
1017 print("Unrecognized.")
1019 assert dt is not None
1020 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1024 if __name__ == "__main__":