5 Parse dates in a variety of formats.
14 from typing import Any, Callable, Dict, Optional
16 import antlr4 # type: ignore
17 import dateutil.easter
19 import holidays # type: ignore
24 import decorator_utils
25 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
26 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
27 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
28 from datetime_utils import TimeUnit, date_to_datetime, datetime_to_date, n_timeunits_from_base
30 logger = logging.getLogger(__name__)
33 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
34 @functools.wraps(enter_or_exit_f)
35 def debug_parse_wrapper(*args, **kwargs):
41 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
43 for c in ctx.getChildren():
44 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
45 retval = enter_or_exit_f(*args, **kwargs)
48 return debug_parse_wrapper
51 class ParseException(Exception):
52 """An exception thrown during parsing because of unrecognized input."""
54 def __init__(self, message: str) -> None:
55 self.message = message
58 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
59 """An error listener that raises ParseExceptions."""
61 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
62 raise ParseException(msg)
64 def reportAmbiguity(self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs):
67 def reportAttemptingFullContext(
68 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
72 def reportContextSensitivity(self, recognizer, dfa, startIndex, stopIndex, prediction, configs):
76 @decorator_utils.decorate_matching_methods_with(
78 acl=acl.StringWildcardBasedACL(
83 denied_patterns=['enterEveryRule', 'exitEveryRule'],
84 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
88 class DateParser(dateparse_utilsListener):
89 PARSE_TYPE_SINGLE_DATE_EXPR = 1
90 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
91 PARSE_TYPE_SINGLE_TIME_EXPR = 3
92 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
94 def __init__(self, *, override_now_for_test_purposes=None) -> None:
95 """C'tor. Passing a value to override_now_for_test_purposes can be
96 used to force this instance to use a custom date/time for its
97 idea of "now" so that the code can be more easily unittested.
98 Leave as None for real use cases.
100 self.month_name_to_number = {
115 # Used only for ides/nones. Month length on a non-leap year.
116 self.typical_days_per_month = {
131 # N.B. day number is also synched with datetime_utils.TimeUnit values
132 # which allows expressions like "3 wednesdays from now" to work.
133 self.day_name_to_number = {
143 # These TimeUnits are defined in datetime_utils and are used as params
144 # to datetime_utils.n_timeunits_from_base.
145 self.time_delta_unit_to_constant = {
146 'hou': TimeUnit.HOURS,
147 'min': TimeUnit.MINUTES,
148 'sec': TimeUnit.SECONDS,
150 self.delta_unit_to_constant = {
151 'day': TimeUnit.DAYS,
152 'wor': TimeUnit.WORKDAYS,
153 'wee': TimeUnit.WEEKS,
154 'mon': TimeUnit.MONTHS,
155 'yea': TimeUnit.YEARS,
157 self.override_now_for_test_purposes = override_now_for_test_purposes
160 def parse(self, date_string: str) -> Optional[datetime.datetime]:
161 """Parse a date/time expression and return a timezone agnostic
162 datetime on success. Also sets self.datetime, self.date and
163 self.time which can each be accessed other methods on the
164 class: get_datetime(), get_date() and get_time(). Raises a
165 ParseException with a helpful(?) message on parse error or
168 To get an idea of what expressions can be parsed, check out
169 the unittest and the grammar.
173 txt = '3 weeks before last tues at 9:15am'
176 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
178 # dt1 and dt2 will be identical other than the fact that
179 # the latter's tzinfo will be set to PST/PDT.
181 This is the main entrypoint to this class for caller code.
183 date_string = date_string.strip()
184 date_string = re.sub(r'\s+', ' ', date_string)
186 listener = RaisingErrorListener()
187 input_stream = antlr4.InputStream(date_string)
188 lexer = dateparse_utilsLexer(input_stream)
189 lexer.removeErrorListeners()
190 lexer.addErrorListener(listener)
191 stream = antlr4.CommonTokenStream(lexer)
192 parser = dateparse_utilsParser(stream)
193 parser.removeErrorListeners()
194 parser.addErrorListener(listener)
195 tree = parser.parse()
196 walker = antlr4.ParseTreeWalker()
197 walker.walk(self, tree)
200 def get_date(self) -> Optional[datetime.date]:
201 """Return the date part or None."""
204 def get_time(self) -> Optional[datetime.time]:
205 """Return the time part or None."""
208 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
209 """Return as a datetime. Parsed date expressions without any time
210 part return hours = minutes = seconds = microseconds = 0 (i.e. at
211 midnight that day). Parsed time expressions without any date part
212 default to date = today.
214 The optional tz param allows the caller to request the datetime be
215 timezone aware and sets the tzinfo to the indicated zone. Defaults
216 to timezone naive (i.e. tzinfo = None).
221 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
227 """Reset at init and between parses."""
228 if self.override_now_for_test_purposes is None:
229 self.now_datetime = datetime.datetime.now()
230 self.today = datetime.date.today()
232 self.now_datetime = self.override_now_for_test_purposes
233 self.today = datetime_to_date(self.override_now_for_test_purposes)
234 self.date: Optional[datetime.date] = None
235 self.time: Optional[datetime.time] = None
236 self.datetime: Optional[datetime.datetime] = None
237 self.context: Dict[str, Any] = {}
238 self.timedelta = datetime.timedelta(seconds=0)
239 self.saw_overt_year = False
242 def _normalize_special_day_name(name: str) -> str:
243 """String normalization / canonicalization for date expressions."""
245 name = name.replace("'", '')
246 name = name.replace('xmas', 'christmas')
247 name = name.replace('mlk', 'martin luther king')
248 name = name.replace(' ', '')
249 eve = 'eve' if name[-3:] == 'eve' else ''
250 name = name[:5] + eve
251 name = name.replace('washi', 'presi')
254 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
255 """Figure out what unit a date expression piece is talking about."""
257 return TimeUnit.MONTHS
258 txt = orig.lower()[:3]
259 if txt in self.day_name_to_number:
260 return TimeUnit(self.day_name_to_number[txt])
261 elif txt in self.delta_unit_to_constant:
262 return TimeUnit(self.delta_unit_to_constant[txt])
263 raise ParseException(f'Invalid date unit: {orig}')
265 def _figure_out_time_unit(self, orig: str) -> int:
266 """Figure out what unit a time expression piece is talking about."""
267 txt = orig.lower()[:3]
268 if txt in self.time_delta_unit_to_constant:
269 return self.time_delta_unit_to_constant[txt]
270 raise ParseException(f'Invalid time unit: {orig}')
272 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
273 """Parse what we think is a special date name and return its datetime
274 (or None if it can't be parsed).
277 year = self.context.get('year', today.year)
278 name = DateParser._normalize_special_day_name(self.context['special'])
280 # Yesterday, today, tomorrow -- ignore any next/last
281 if name == 'today' or name == 'now':
284 return today + datetime.timedelta(days=-1)
286 return today + datetime.timedelta(days=+1)
288 next_last = self.context.get('special_next_last', '')
289 if next_last == 'next':
291 self.saw_overt_year = True
292 elif next_last == 'last':
294 self.saw_overt_year = True
298 return dateutil.easter.easter(year=year)
299 elif name == 'hallo':
300 return datetime.date(year=year, month=10, day=31)
302 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
303 if 'Observed' not in holiday_name:
304 holiday_name = DateParser._normalize_special_day_name(holiday_name)
305 if name == holiday_name:
307 if name == 'chriseve':
308 return datetime.date(year=year, month=12, day=24)
309 elif name == 'newyeeve':
310 return datetime.date(year=year, month=12, day=31)
313 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
314 """Handle date expressions like "the ides of March" which require
315 both the "ides" and the month since the definition of the "ides"
316 changes based on the length of the month.
318 assert 'ide' in day or 'non' in day
319 assert month_number in self.typical_days_per_month
320 typical_days_per_month = self.typical_days_per_month[month_number]
323 if typical_days_per_month == 31:
324 if self.context['day'] == 'ide':
331 if self.context['day'] == 'ide':
336 def _parse_normal_date(self) -> datetime.date:
337 if 'dow' in self.context:
339 while d.weekday() != self.context['dow']:
340 d += datetime.timedelta(days=1)
343 if 'month' not in self.context:
344 raise ParseException('Missing month')
345 if 'day' not in self.context:
346 raise ParseException('Missing day')
347 if 'year' not in self.context:
348 self.context['year'] = self.today.year
349 self.saw_overt_year = False
351 self.saw_overt_year = True
353 # Handling "ides" and "nones" requires both the day and month.
354 if self.context['day'] == 'ide' or self.context['day'] == 'non':
355 self.context['day'] = self._resolve_ides_nones(
356 self.context['day'], self.context['month']
359 return datetime.date(
360 year=self.context['year'],
361 month=self.context['month'],
362 day=self.context['day'],
365 def _parse_tz(self, txt: str) -> Any:
371 tz1 = pytz.timezone(txt)
379 tz2 = dateutil.tz.gettz(txt)
385 # Try constructing an offset in seconds
388 if txt_sign == '-' or txt_sign == '+':
389 sign = +1 if txt_sign == '+' else -1
391 minute = int(txt[-2:])
392 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
393 tzoffset = dateutil.tz.tzoffset(txt, offset)
399 def _get_int(self, txt: str) -> int:
400 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
402 while not txt[-1].isdigit():
406 # -- overridden methods invoked by parse walk --
408 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
411 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
414 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
415 """Populate self.datetime."""
416 if self.date is None:
417 self.date = self.today
418 year = self.date.year
419 month = self.date.month
422 if self.time is None:
423 self.time = datetime.time(0, 0, 0)
424 hour = self.time.hour
425 minute = self.time.minute
426 second = self.time.second
427 micros = self.time.microsecond
429 self.datetime = datetime.datetime(
437 tzinfo=self.time.tzinfo,
440 # Apply resudual adjustments to times here when we have a
442 self.datetime = self.datetime + self.timedelta
443 assert self.datetime is not None
444 self.time = datetime.time(
446 self.datetime.minute,
447 self.datetime.second,
448 self.datetime.microsecond,
449 self.datetime.tzinfo,
452 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
454 if ctx.singleDateExpr() is not None:
455 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
456 elif ctx.baseAndOffsetDateExpr() is not None:
457 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
459 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
461 if ctx.singleTimeExpr() is not None:
462 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
463 elif ctx.baseAndOffsetTimeExpr() is not None:
464 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
466 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
467 """When we leave the date expression, populate self.date."""
468 if 'special' in self.context:
469 self.date = self._parse_special_date(self.context['special'])
471 self.date = self._parse_normal_date()
472 assert self.date is not None
474 # For a single date, just return the date we pulled out.
475 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
478 # Otherwise treat self.date as a base date that we're modifying
480 if 'delta_int' not in self.context:
481 raise ParseException('Missing delta_int?!')
482 count = self.context['delta_int']
486 # Adjust count's sign based on the presence of 'before' or 'after'.
487 if 'delta_before_after' in self.context:
488 before_after = self.context['delta_before_after'].lower()
490 before_after == 'before'
491 or before_after == 'until'
492 or before_after == 'til'
493 or before_after == 'to'
497 # What are we counting units of?
498 if 'delta_unit' not in self.context:
499 raise ParseException('Missing delta_unit?!')
500 unit = self.context['delta_unit']
501 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
502 self.date = datetime_to_date(dt)
504 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
506 self.time = datetime.time(
507 self.context['hour'],
508 self.context['minute'],
509 self.context['seconds'],
510 self.context['micros'],
511 tzinfo=self.context.get('tz', None),
513 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
516 # If we get here there (should be) a relative adjustment to
518 if 'nth' in self.context:
519 count = self.context['nth']
520 elif 'time_delta_int' in self.context:
521 count = self.context['time_delta_int']
523 raise ParseException('Missing delta in relative time.')
527 # Adjust count's sign based on the presence of 'before' or 'after'.
528 if 'time_delta_before_after' in self.context:
529 before_after = self.context['time_delta_before_after'].lower()
531 before_after == 'before'
532 or before_after == 'until'
533 or before_after == 'til'
534 or before_after == 'to'
538 # What are we counting units of... assume minutes.
539 if 'time_delta_unit' not in self.context:
540 self.timedelta += datetime.timedelta(minutes=count)
542 unit = self.context['time_delta_unit']
543 if unit == TimeUnit.SECONDS:
544 self.timedelta += datetime.timedelta(seconds=count)
545 elif unit == TimeUnit.MINUTES:
546 self.timedelta = datetime.timedelta(minutes=count)
547 elif unit == TimeUnit.HOURS:
548 self.timedelta = datetime.timedelta(hours=count)
550 raise ParseException(f'Invalid Unit: "{unit}"')
552 def exitDeltaPlusMinusExpr(self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext) -> None:
556 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
559 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
561 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
563 self.context['delta_int'] = n
564 self.context['delta_unit'] = unit
566 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
568 unit = self._figure_out_date_unit(ctx.getText().lower())
570 raise ParseException(f'Bad delta unit: {ctx.getText()}')
572 self.context['delta_unit'] = unit
574 def exitDeltaNextLast(self, ctx: dateparse_utilsParser.DeltaNextLastContext) -> None:
576 txt = ctx.getText().lower()
578 raise ParseException(f'Bad next/last: {ctx.getText()}')
579 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
580 raise ParseException('Next/last expression expected to be relative to today.')
581 if txt[:4] == 'next':
582 self.context['delta_int'] = +1
583 self.context['day'] = self.now_datetime.day
584 self.context['month'] = self.now_datetime.month
585 self.context['year'] = self.now_datetime.year
586 self.saw_overt_year = True
587 elif txt[:4] == 'last':
588 self.context['delta_int'] = -1
589 self.context['day'] = self.now_datetime.day
590 self.context['month'] = self.now_datetime.month
591 self.context['year'] = self.now_datetime.year
592 self.saw_overt_year = True
594 raise ParseException(f'Bad next/last: {ctx.getText()}')
596 def exitCountUnitsBeforeAfterTimeExpr(
597 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
599 if 'nth' not in self.context:
600 raise ParseException(f'Bad count expression: {ctx.getText()}')
602 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
603 self.context['time_delta_unit'] = unit
605 raise ParseException(f'Bad delta unit: {ctx.getText()}')
606 if 'time_delta_before_after' not in self.context:
607 raise ParseException(f'Bad Before/After: {ctx.getText()}')
609 def exitDeltaTimeFraction(self, ctx: dateparse_utilsParser.DeltaTimeFractionContext) -> None:
611 txt = ctx.getText().lower()[:4]
613 self.context['time_delta_int'] = 15
614 self.context['time_delta_unit'] = TimeUnit.MINUTES
616 self.context['time_delta_int'] = 30
617 self.context['time_delta_unit'] = TimeUnit.MINUTES
619 raise ParseException(f'Bad time fraction {ctx.getText()}')
621 raise ParseException(f'Bad time fraction {ctx.getText()}')
623 def exitDeltaBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
625 txt = ctx.getText().lower()
627 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
629 self.context['delta_before_after'] = txt
631 def exitDeltaTimeBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
633 txt = ctx.getText().lower()
635 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
637 self.context['time_delta_before_after'] = txt
639 def exitNthWeekdayInMonthMaybeYearExpr(
640 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
642 """Do a bunch of work to convert expressions like...
644 'the 2nd Friday of June' -and-
645 'the last Wednesday in October'
647 ...into base + offset expressions instead.
650 if 'nth' not in self.context:
651 raise ParseException(f'Missing nth number: {ctx.getText()}')
652 n = self.context['nth']
653 if n < 1 or n > 5: # months never have more than 5 Foodays
655 raise ParseException(f'Invalid nth number: {ctx.getText()}')
656 del self.context['nth']
657 self.context['delta_int'] = n
659 year = self.context.get('year', self.today.year)
660 if 'month' not in self.context:
661 raise ParseException(f'Missing month expression: {ctx.getText()}')
662 month = self.context['month']
664 dow = self.context['dow']
665 del self.context['dow']
666 self.context['delta_unit'] = dow
668 # For the nth Fooday in Month, start at the 1st of the
669 # month and count ahead N Foodays. For the last Fooday in
670 # Month, start at the last of the month and count back one
677 tmp_date = datetime.date(year=year, month=month, day=1)
678 tmp_date = tmp_date - datetime.timedelta(days=1)
680 self.context['year'] = tmp_date.year
681 self.context['month'] = tmp_date.month
682 self.context['day'] = tmp_date.day
684 # The delta adjustment code can handle the case where
685 # the last day of the month is the day we're looking
688 self.context['year'] = year
689 self.context['month'] = month
690 self.context['day'] = 1
691 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
693 raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}')
695 def exitFirstLastWeekdayInMonthMaybeYearExpr(
697 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
699 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
701 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
703 i = self._get_int(ctx.getText())
705 raise ParseException(f'Bad nth expression: {ctx.getText()}')
707 self.context['nth'] = i
709 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
717 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
719 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
721 self.context['nth'] = txt
723 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
725 dow = ctx.getText().lower()[:3]
726 dow = self.day_name_to_number.get(dow, None)
728 raise ParseException('Bad day of week')
730 self.context['dow'] = dow
732 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
734 day = ctx.getText().lower()
736 self.context['day'] = 'ide'
739 self.context['day'] = 'non'
742 self.context['day'] = 1
744 day = self._get_int(day)
745 if day < 1 or day > 31:
746 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
748 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
749 self.context['day'] = day
751 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
753 month = ctx.getText()
754 while month[0] == '/' or month[0] == '-':
756 month = month[:3].lower()
757 month = self.month_name_to_number.get(month, None)
759 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
761 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
763 self.context['month'] = month
765 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
767 month = self._get_int(ctx.getText())
768 if month < 1 or month > 12:
769 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
771 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
773 self.context['month'] = month
775 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
777 year = self._get_int(ctx.getText())
779 raise ParseException(f'Bad year expression: {ctx.getText()}')
781 raise ParseException(f'Bad year expression: {ctx.getText()}')
783 self.saw_overt_year = True
784 self.context['year'] = year
786 def exitSpecialDateMaybeYearExpr(
787 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
790 special = ctx.specialDate().getText().lower()
791 self.context['special'] = special
793 raise ParseException(f'Bad specialDate expression: {ctx.specialDate().getText()}')
795 mod = ctx.thisNextLast()
797 if mod.THIS() is not None:
798 self.context['special_next_last'] = 'this'
799 elif mod.NEXT() is not None:
800 self.context['special_next_last'] = 'next'
801 elif mod.LAST() is not None:
802 self.context['special_next_last'] = 'last'
804 raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}')
806 def exitNFoosFromTodayAgoExpr(
807 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
809 d = self.now_datetime
811 count = self._get_int(ctx.unsignedInt().getText())
812 unit = ctx.deltaUnit().getText().lower()
813 ago_from_now = ctx.AGO_FROM_NOW().getText()
815 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}')
817 if "ago" in ago_from_now or "back" in ago_from_now:
820 unit = self._figure_out_date_unit(unit)
821 d = n_timeunits_from_base(count, TimeUnit(unit), d)
822 self.context['year'] = d.year
823 self.context['month'] = d.month
824 self.context['day'] = d.day
826 def exitDeltaRelativeToTodayExpr(
827 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
829 # When someone says "next week" they mean a week from now.
830 # Likewise next month or last year. These expressions are now
833 # But when someone says "this Friday" they mean "this coming
834 # Friday". It would be weird to say "this Friday" if today
835 # was already Friday but I'm parsing it to mean: the next day
836 # that is a Friday. So when you say "next Friday" you mean
837 # the Friday after this coming Friday, or 2 Fridays from now.
839 # This set handles this weirdness.
851 d = self.now_datetime
853 mod = ctx.thisNextLast()
854 unit = ctx.deltaUnit().getText().lower()
855 unit = self._figure_out_date_unit(unit)
869 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
871 raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}')
872 d = n_timeunits_from_base(count, TimeUnit(unit), d)
873 self.context['year'] = d.year
874 self.context['month'] = d.month
875 self.context['day'] = d.day
877 def exitSpecialTimeExpr(self, ctx: dateparse_utilsParser.SpecialTimeExprContext) -> None:
879 txt = ctx.specialTime().getText().lower()
881 raise ParseException(f'Bad special time expression: {ctx.getText()}')
883 if txt == 'noon' or txt == 'midday':
884 self.context['hour'] = 12
885 self.context['minute'] = 0
886 self.context['seconds'] = 0
887 self.context['micros'] = 0
888 elif txt == 'midnight':
889 self.context['hour'] = 0
890 self.context['minute'] = 0
891 self.context['seconds'] = 0
892 self.context['micros'] = 0
894 raise ParseException(f'Bad special time expression: {txt}')
897 tz = ctx.tzExpr().getText()
898 self.context['tz'] = self._parse_tz(tz)
902 def exitTwelveHourTimeExpr(self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext) -> None:
904 hour = ctx.hour().getText()
905 while not hour[-1].isdigit():
907 hour = self._get_int(hour)
909 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
910 if hour <= 0 or hour > 12:
911 raise ParseException(f'Bad hour (out of range): {hour}')
914 minute = self._get_int(ctx.minute().getText())
917 if minute < 0 or minute > 59:
918 raise ParseException(f'Bad minute (out of range): {minute}')
919 self.context['minute'] = minute
922 seconds = self._get_int(ctx.second().getText())
925 if seconds < 0 or seconds > 59:
926 raise ParseException(f'Bad second (out of range): {seconds}')
927 self.context['seconds'] = seconds
930 micros = self._get_int(ctx.micros().getText())
933 if micros < 0 or micros > 1000000:
934 raise ParseException(f'Bad micros (out of range): {micros}')
935 self.context['micros'] = micros
938 ampm = ctx.ampm().getText()
940 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
945 self.context['hour'] = hour
948 tz = ctx.tzExpr().getText()
949 self.context['tz'] = self._parse_tz(tz)
953 def exitTwentyFourHourTimeExpr(
954 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
957 hour = ctx.hour().getText()
958 while not hour[-1].isdigit():
960 hour = self._get_int(hour)
962 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
963 if hour < 0 or hour > 23:
964 raise ParseException(f'Bad hour (out of range): {hour}')
965 self.context['hour'] = hour
968 minute = self._get_int(ctx.minute().getText())
971 if minute < 0 or minute > 59:
972 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
973 self.context['minute'] = minute
976 seconds = self._get_int(ctx.second().getText())
979 if seconds < 0 or seconds > 59:
980 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
981 self.context['seconds'] = seconds
984 micros = self._get_int(ctx.micros().getText())
987 if micros < 0 or micros >= 1000000:
988 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
989 self.context['micros'] = micros
992 tz = ctx.tzExpr().getText()
993 self.context['tz'] = self._parse_tz(tz)
998 @bootstrap.initialize
1000 parser = DateParser()
1001 for line in sys.stdin:
1003 line = re.sub(r"#.*$", "", line)
1004 if re.match(r"^ *$", line) is not None:
1007 dt = parser.parse(line)
1008 except Exception as e:
1010 print("Unrecognized.")
1012 assert dt is not None
1013 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1017 if __name__ == "__main__":