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 (
32 n_timeunits_from_base,
35 logger = logging.getLogger(__name__)
38 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
39 @functools.wraps(enter_or_exit_f)
40 def debug_parse_wrapper(*args, **kwargs):
46 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
48 for c in ctx.getChildren():
49 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
50 retval = enter_or_exit_f(*args, **kwargs)
53 return debug_parse_wrapper
56 class ParseException(Exception):
57 """An exception thrown during parsing because of unrecognized input."""
59 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 PARSE_TYPE_SINGLE_DATE_EXPR = 1
95 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
96 PARSE_TYPE_SINGLE_TIME_EXPR = 3
97 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
99 def __init__(self, *, override_now_for_test_purposes=None) -> None:
100 """C'tor. Passing a value to override_now_for_test_purposes can be
101 used to force this instance to use a custom date/time for its
102 idea of "now" so that the code can be more easily unittested.
103 Leave as None for real use cases.
105 self.month_name_to_number = {
120 # Used only for ides/nones. Month length on a non-leap year.
121 self.typical_days_per_month = {
136 # N.B. day number is also synched with datetime_utils.TimeUnit values
137 # which allows expressions like "3 wednesdays from now" to work.
138 self.day_name_to_number = {
148 # These TimeUnits are defined in datetime_utils and are used as params
149 # to datetime_utils.n_timeunits_from_base.
150 self.time_delta_unit_to_constant = {
151 'hou': TimeUnit.HOURS,
152 'min': TimeUnit.MINUTES,
153 'sec': TimeUnit.SECONDS,
155 self.delta_unit_to_constant = {
156 'day': TimeUnit.DAYS,
157 'wor': TimeUnit.WORKDAYS,
158 'wee': TimeUnit.WEEKS,
159 'mon': TimeUnit.MONTHS,
160 'yea': TimeUnit.YEARS,
162 self.override_now_for_test_purposes = override_now_for_test_purposes
165 def parse(self, date_string: str) -> Optional[datetime.datetime]:
166 """Parse a date/time expression and return a timezone agnostic
167 datetime on success. Also sets self.datetime, self.date and
168 self.time which can each be accessed other methods on the
169 class: get_datetime(), get_date() and get_time(). Raises a
170 ParseException with a helpful(?) message on parse error or
173 To get an idea of what expressions can be parsed, check out
174 the unittest and the grammar.
178 txt = '3 weeks before last tues at 9:15am'
181 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
183 # dt1 and dt2 will be identical other than the fact that
184 # the latter's tzinfo will be set to PST/PDT.
186 This is the main entrypoint to this class for caller code.
188 date_string = date_string.strip()
189 date_string = re.sub(r'\s+', ' ', date_string)
191 listener = RaisingErrorListener()
192 input_stream = antlr4.InputStream(date_string)
193 lexer = dateparse_utilsLexer(input_stream)
194 lexer.removeErrorListeners()
195 lexer.addErrorListener(listener)
196 stream = antlr4.CommonTokenStream(lexer)
197 parser = dateparse_utilsParser(stream)
198 parser.removeErrorListeners()
199 parser.addErrorListener(listener)
200 tree = parser.parse()
201 walker = antlr4.ParseTreeWalker()
202 walker.walk(self, tree)
205 def get_date(self) -> Optional[datetime.date]:
206 """Return the date part or None."""
209 def get_time(self) -> Optional[datetime.time]:
210 """Return the time part or None."""
213 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
214 """Return as a datetime. Parsed date expressions without any time
215 part return hours = minutes = seconds = microseconds = 0 (i.e. at
216 midnight that day). Parsed time expressions without any date part
217 default to date = today.
219 The optional tz param allows the caller to request the datetime be
220 timezone aware and sets the tzinfo to the indicated zone. Defaults
221 to timezone naive (i.e. tzinfo = None).
226 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
232 """Reset at init and between parses."""
233 if self.override_now_for_test_purposes is None:
234 self.now_datetime = datetime.datetime.now()
235 self.today = datetime.date.today()
237 self.now_datetime = self.override_now_for_test_purposes
238 self.today = datetime_to_date(self.override_now_for_test_purposes)
239 self.date: Optional[datetime.date] = None
240 self.time: Optional[datetime.time] = None
241 self.datetime: Optional[datetime.datetime] = None
242 self.context: Dict[str, Any] = {}
243 self.timedelta = datetime.timedelta(seconds=0)
244 self.saw_overt_year = False
247 def _normalize_special_day_name(name: str) -> str:
248 """String normalization / canonicalization for date expressions."""
250 name = name.replace("'", '')
251 name = name.replace('xmas', 'christmas')
252 name = name.replace('mlk', 'martin luther king')
253 name = name.replace(' ', '')
254 eve = 'eve' if name[-3:] == 'eve' else ''
255 name = name[:5] + eve
256 name = name.replace('washi', 'presi')
259 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
260 """Figure out what unit a date expression piece is talking about."""
262 return TimeUnit.MONTHS
263 txt = orig.lower()[:3]
264 if txt in self.day_name_to_number:
265 return TimeUnit(self.day_name_to_number[txt])
266 elif txt in self.delta_unit_to_constant:
267 return TimeUnit(self.delta_unit_to_constant[txt])
268 raise ParseException(f'Invalid date unit: {orig}')
270 def _figure_out_time_unit(self, orig: str) -> int:
271 """Figure out what unit a time expression piece is talking about."""
272 txt = orig.lower()[:3]
273 if txt in self.time_delta_unit_to_constant:
274 return self.time_delta_unit_to_constant[txt]
275 raise ParseException(f'Invalid time unit: {orig}')
277 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
278 """Parse what we think is a special date name and return its datetime
279 (or None if it can't be parsed).
282 year = self.context.get('year', today.year)
283 name = DateParser._normalize_special_day_name(self.context['special'])
285 # Yesterday, today, tomorrow -- ignore any next/last
286 if name == 'today' or name == 'now':
289 return today + datetime.timedelta(days=-1)
291 return today + datetime.timedelta(days=+1)
293 next_last = self.context.get('special_next_last', '')
294 if next_last == 'next':
296 self.saw_overt_year = True
297 elif next_last == 'last':
299 self.saw_overt_year = True
303 return dateutil.easter.easter(year=year)
304 elif name == 'hallo':
305 return datetime.date(year=year, month=10, day=31)
307 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
308 if 'Observed' not in holiday_name:
309 holiday_name = DateParser._normalize_special_day_name(holiday_name)
310 if name == holiday_name:
312 if name == 'chriseve':
313 return datetime.date(year=year, month=12, day=24)
314 elif name == 'newyeeve':
315 return datetime.date(year=year, month=12, day=31)
318 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
319 """Handle date expressions like "the ides of March" which require
320 both the "ides" and the month since the definition of the "ides"
321 changes based on the length of the month.
323 assert 'ide' in day or 'non' in day
324 assert month_number in self.typical_days_per_month
325 typical_days_per_month = self.typical_days_per_month[month_number]
328 if typical_days_per_month == 31:
329 if self.context['day'] == 'ide':
336 if self.context['day'] == 'ide':
341 def _parse_normal_date(self) -> datetime.date:
342 if 'dow' in self.context:
344 while d.weekday() != self.context['dow']:
345 d += datetime.timedelta(days=1)
348 if 'month' not in self.context:
349 raise ParseException('Missing month')
350 if 'day' not in self.context:
351 raise ParseException('Missing day')
352 if 'year' not in self.context:
353 self.context['year'] = self.today.year
354 self.saw_overt_year = False
356 self.saw_overt_year = True
358 # Handling "ides" and "nones" requires both the day and month.
359 if self.context['day'] == 'ide' or self.context['day'] == 'non':
360 self.context['day'] = self._resolve_ides_nones(
361 self.context['day'], self.context['month']
364 return datetime.date(
365 year=self.context['year'],
366 month=self.context['month'],
367 day=self.context['day'],
370 def _parse_tz(self, txt: str) -> Any:
376 tz1 = pytz.timezone(txt)
384 tz2 = dateutil.tz.gettz(txt)
390 # Try constructing an offset in seconds
393 if txt_sign == '-' or txt_sign == '+':
394 sign = +1 if txt_sign == '+' else -1
396 minute = int(txt[-2:])
397 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
398 tzoffset = dateutil.tz.tzoffset(txt, offset)
404 def _get_int(self, txt: str) -> int:
405 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
407 while not txt[-1].isdigit():
411 # -- overridden methods invoked by parse walk --
413 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
416 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
419 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
420 """Populate self.datetime."""
421 if self.date is None:
422 self.date = self.today
423 year = self.date.year
424 month = self.date.month
427 if self.time is None:
428 self.time = datetime.time(0, 0, 0)
429 hour = self.time.hour
430 minute = self.time.minute
431 second = self.time.second
432 micros = self.time.microsecond
434 self.datetime = datetime.datetime(
442 tzinfo=self.time.tzinfo,
445 # Apply resudual adjustments to times here when we have a
447 self.datetime = self.datetime + self.timedelta
448 assert self.datetime is not None
449 self.time = datetime.time(
451 self.datetime.minute,
452 self.datetime.second,
453 self.datetime.microsecond,
454 self.datetime.tzinfo,
457 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
459 if ctx.singleDateExpr() is not None:
460 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
461 elif ctx.baseAndOffsetDateExpr() is not None:
462 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
464 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
466 if ctx.singleTimeExpr() is not None:
467 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
468 elif ctx.baseAndOffsetTimeExpr() is not None:
469 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
471 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
472 """When we leave the date expression, populate self.date."""
473 if 'special' in self.context:
474 self.date = self._parse_special_date(self.context['special'])
476 self.date = self._parse_normal_date()
477 assert self.date is not None
479 # For a single date, just return the date we pulled out.
480 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
483 # Otherwise treat self.date as a base date that we're modifying
485 if 'delta_int' not in self.context:
486 raise ParseException('Missing delta_int?!')
487 count = self.context['delta_int']
491 # Adjust count's sign based on the presence of 'before' or 'after'.
492 if 'delta_before_after' in self.context:
493 before_after = self.context['delta_before_after'].lower()
495 before_after == 'before'
496 or before_after == 'until'
497 or before_after == 'til'
498 or before_after == 'to'
502 # What are we counting units of?
503 if 'delta_unit' not in self.context:
504 raise ParseException('Missing delta_unit?!')
505 unit = self.context['delta_unit']
506 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
507 self.date = datetime_to_date(dt)
509 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
511 self.time = datetime.time(
512 self.context['hour'],
513 self.context['minute'],
514 self.context['seconds'],
515 self.context['micros'],
516 tzinfo=self.context.get('tz', None),
518 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
521 # If we get here there (should be) a relative adjustment to
523 if 'nth' in self.context:
524 count = self.context['nth']
525 elif 'time_delta_int' in self.context:
526 count = self.context['time_delta_int']
528 raise ParseException('Missing delta in relative time.')
532 # Adjust count's sign based on the presence of 'before' or 'after'.
533 if 'time_delta_before_after' in self.context:
534 before_after = self.context['time_delta_before_after'].lower()
536 before_after == 'before'
537 or before_after == 'until'
538 or before_after == 'til'
539 or before_after == '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()}')
564 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
566 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
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())
575 raise ParseException(f'Bad delta unit: {ctx.getText()}')
577 self.context['delta_unit'] = unit
579 def exitDeltaNextLast(self, ctx: dateparse_utilsParser.DeltaNextLastContext) -> None:
581 txt = ctx.getText().lower()
583 raise ParseException(f'Bad next/last: {ctx.getText()}')
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
610 raise ParseException(f'Bad delta unit: {ctx.getText()}')
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()}')
626 raise ParseException(f'Bad time fraction {ctx.getText()}')
628 def exitDeltaBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
630 txt = ctx.getText().lower()
632 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
634 self.context['delta_before_after'] = txt
636 def exitDeltaTimeBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
638 txt = ctx.getText().lower()
640 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
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
698 raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}')
700 def exitFirstLastWeekdayInMonthMaybeYearExpr(
702 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
704 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
706 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
708 i = self._get_int(ctx.getText())
710 raise ParseException(f'Bad nth expression: {ctx.getText()}')
712 self.context['nth'] = i
714 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
722 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
724 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
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)
733 raise ParseException('Bad day of week')
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 = self._get_int(day)
750 if day < 1 or day > 31:
751 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
753 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
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()}')
766 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
768 self.context['month'] = month
770 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
772 month = self._get_int(ctx.getText())
773 if month < 1 or month > 12:
774 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
776 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
778 self.context['month'] = month
780 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
782 year = self._get_int(ctx.getText())
784 raise ParseException(f'Bad year expression: {ctx.getText()}')
786 raise ParseException(f'Bad year expression: {ctx.getText()}')
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
798 raise ParseException(f'Bad specialDate expression: {ctx.specialDate().getText()}')
800 mod = ctx.thisNextLast()
802 if mod.THIS() is not None:
803 self.context['special_next_last'] = 'this'
804 elif mod.NEXT() is not None:
805 self.context['special_next_last'] = 'next'
806 elif mod.LAST() is not None:
807 self.context['special_next_last'] = 'last'
809 raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}')
811 def exitNFoosFromTodayAgoExpr(
812 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
814 d = self.now_datetime
816 count = self._get_int(ctx.unsignedInt().getText())
817 unit = ctx.deltaUnit().getText().lower()
818 ago_from_now = ctx.AGO_FROM_NOW().getText()
820 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}')
822 if "ago" in ago_from_now or "back" in ago_from_now:
825 unit = self._figure_out_date_unit(unit)
826 d = n_timeunits_from_base(count, TimeUnit(unit), d)
827 self.context['year'] = d.year
828 self.context['month'] = d.month
829 self.context['day'] = d.day
831 def exitDeltaRelativeToTodayExpr(
832 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
834 # When someone says "next week" they mean a week from now.
835 # Likewise next month or last year. These expressions are now
838 # But when someone says "this Friday" they mean "this coming
839 # Friday". It would be weird to say "this Friday" if today
840 # was already Friday but I'm parsing it to mean: the next day
841 # that is a Friday. So when you say "next Friday" you mean
842 # the Friday after this coming Friday, or 2 Fridays from now.
844 # This set handles this weirdness.
856 d = self.now_datetime
858 mod = ctx.thisNextLast()
859 unit = ctx.deltaUnit().getText().lower()
860 unit = self._figure_out_date_unit(unit)
874 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
876 raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}')
877 d = n_timeunits_from_base(count, TimeUnit(unit), d)
878 self.context['year'] = d.year
879 self.context['month'] = d.month
880 self.context['day'] = d.day
882 def exitSpecialTimeExpr(self, ctx: dateparse_utilsParser.SpecialTimeExprContext) -> None:
884 txt = ctx.specialTime().getText().lower()
886 raise ParseException(f'Bad special time expression: {ctx.getText()}')
888 if txt == 'noon' or txt == 'midday':
889 self.context['hour'] = 12
890 self.context['minute'] = 0
891 self.context['seconds'] = 0
892 self.context['micros'] = 0
893 elif txt == 'midnight':
894 self.context['hour'] = 0
895 self.context['minute'] = 0
896 self.context['seconds'] = 0
897 self.context['micros'] = 0
899 raise ParseException(f'Bad special time expression: {txt}')
902 tz = ctx.tzExpr().getText()
903 self.context['tz'] = self._parse_tz(tz)
907 def exitTwelveHourTimeExpr(self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext) -> None:
909 hour = ctx.hour().getText()
910 while not hour[-1].isdigit():
912 hour = self._get_int(hour)
914 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
915 if hour <= 0 or hour > 12:
916 raise ParseException(f'Bad hour (out of range): {hour}')
919 minute = self._get_int(ctx.minute().getText())
922 if minute < 0 or minute > 59:
923 raise ParseException(f'Bad minute (out of range): {minute}')
924 self.context['minute'] = minute
927 seconds = self._get_int(ctx.second().getText())
930 if seconds < 0 or seconds > 59:
931 raise ParseException(f'Bad second (out of range): {seconds}')
932 self.context['seconds'] = seconds
935 micros = self._get_int(ctx.micros().getText())
938 if micros < 0 or micros > 1000000:
939 raise ParseException(f'Bad micros (out of range): {micros}')
940 self.context['micros'] = micros
943 ampm = ctx.ampm().getText()
945 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
950 self.context['hour'] = hour
953 tz = ctx.tzExpr().getText()
954 self.context['tz'] = self._parse_tz(tz)
958 def exitTwentyFourHourTimeExpr(
959 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
962 hour = ctx.hour().getText()
963 while not hour[-1].isdigit():
965 hour = self._get_int(hour)
967 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
968 if hour < 0 or hour > 23:
969 raise ParseException(f'Bad hour (out of range): {hour}')
970 self.context['hour'] = hour
973 minute = self._get_int(ctx.minute().getText())
976 if minute < 0 or minute > 59:
977 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
978 self.context['minute'] = minute
981 seconds = self._get_int(ctx.second().getText())
984 if seconds < 0 or seconds > 59:
985 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
986 self.context['seconds'] = seconds
989 micros = self._get_int(ctx.micros().getText())
992 if micros < 0 or micros >= 1000000:
993 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
994 self.context['micros'] = micros
997 tz = ctx.tzExpr().getText()
998 self.context['tz'] = self._parse_tz(tz)
1003 @bootstrap.initialize
1005 parser = DateParser()
1006 for line in sys.stdin:
1008 line = re.sub(r"#.*$", "", line)
1009 if re.match(r"^ *$", line) is not None:
1012 dt = parser.parse(line)
1013 except Exception as e:
1015 print("Unrecognized.")
1017 assert dt is not None
1018 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1022 if __name__ == "__main__":