4 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 TimeUnit, date_to_datetime, datetime_to_date, n_timeunits_from_base
29 logger = logging.getLogger(__name__)
32 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
33 @functools.wraps(enter_or_exit_f)
34 def debug_parse_wrapper(*args, **kwargs):
40 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
42 for c in ctx.getChildren():
43 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
44 retval = enter_or_exit_f(*args, **kwargs)
47 return debug_parse_wrapper
50 class ParseException(Exception):
51 """An exception thrown during parsing because of unrecognized input."""
53 def __init__(self, message: str) -> None:
54 self.message = message
57 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
58 """An error listener that raises ParseExceptions."""
60 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
61 raise ParseException(msg)
63 def reportAmbiguity(self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs):
66 def reportAttemptingFullContext(
67 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
71 def reportContextSensitivity(self, recognizer, dfa, startIndex, stopIndex, prediction, configs):
75 @decorator_utils.decorate_matching_methods_with(
77 acl=acl.StringWildcardBasedACL(
82 denied_patterns=['enterEveryRule', 'exitEveryRule'],
83 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
87 class DateParser(dateparse_utilsListener):
88 PARSE_TYPE_SINGLE_DATE_EXPR = 1
89 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
90 PARSE_TYPE_SINGLE_TIME_EXPR = 3
91 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
93 def __init__(self, *, override_now_for_test_purposes=None) -> None:
94 """C'tor. Passing a value to override_now_for_test_purposes can be
95 used to force this instance to use a custom date/time for its
96 idea of "now" so that the code can be more easily unittested.
97 Leave as None for real use cases.
99 self.month_name_to_number = {
114 # Used only for ides/nones. Month length on a non-leap year.
115 self.typical_days_per_month = {
130 # N.B. day number is also synched with datetime_utils.TimeUnit values
131 # which allows expressions like "3 wednesdays from now" to work.
132 self.day_name_to_number = {
142 # These TimeUnits are defined in datetime_utils and are used as params
143 # to datetime_utils.n_timeunits_from_base.
144 self.time_delta_unit_to_constant = {
145 'hou': TimeUnit.HOURS,
146 'min': TimeUnit.MINUTES,
147 'sec': TimeUnit.SECONDS,
149 self.delta_unit_to_constant = {
150 'day': TimeUnit.DAYS,
151 'wor': TimeUnit.WORKDAYS,
152 'wee': TimeUnit.WEEKS,
153 'mon': TimeUnit.MONTHS,
154 'yea': TimeUnit.YEARS,
156 self.override_now_for_test_purposes = override_now_for_test_purposes
159 def parse(self, date_string: str) -> Optional[datetime.datetime]:
160 """Parse a date/time expression and return a timezone agnostic
161 datetime on success. Also sets self.datetime, self.date and
162 self.time which can each be accessed other methods on the
163 class: get_datetime(), get_date() and get_time(). Raises a
164 ParseException with a helpful(?) message on parse error or
167 To get an idea of what expressions can be parsed, check out
168 the unittest and the grammar.
172 txt = '3 weeks before last tues at 9:15am'
175 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
177 # dt1 and dt2 will be identical other than the fact that
178 # the latter's tzinfo will be set to PST/PDT.
180 This is the main entrypoint to this class for caller code.
182 date_string = date_string.strip()
183 date_string = re.sub(r'\s+', ' ', date_string)
185 listener = RaisingErrorListener()
186 input_stream = antlr4.InputStream(date_string)
187 lexer = dateparse_utilsLexer(input_stream)
188 lexer.removeErrorListeners()
189 lexer.addErrorListener(listener)
190 stream = antlr4.CommonTokenStream(lexer)
191 parser = dateparse_utilsParser(stream)
192 parser.removeErrorListeners()
193 parser.addErrorListener(listener)
194 tree = parser.parse()
195 walker = antlr4.ParseTreeWalker()
196 walker.walk(self, tree)
199 def get_date(self) -> Optional[datetime.date]:
200 """Return the date part or None."""
203 def get_time(self) -> Optional[datetime.time]:
204 """Return the time part or None."""
207 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
208 """Return as a datetime. Parsed date expressions without any time
209 part return hours = minutes = seconds = microseconds = 0 (i.e. at
210 midnight that day). Parsed time expressions without any date part
211 default to date = today.
213 The optional tz param allows the caller to request the datetime be
214 timezone aware and sets the tzinfo to the indicated zone. Defaults
215 to timezone naive (i.e. tzinfo = None).
220 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
226 """Reset at init and between parses."""
227 if self.override_now_for_test_purposes is None:
228 self.now_datetime = datetime.datetime.now()
229 self.today = datetime.date.today()
231 self.now_datetime = self.override_now_for_test_purposes
232 self.today = datetime_to_date(self.override_now_for_test_purposes)
233 self.date: Optional[datetime.date] = None
234 self.time: Optional[datetime.time] = None
235 self.datetime: Optional[datetime.datetime] = None
236 self.context: Dict[str, Any] = {}
237 self.timedelta = datetime.timedelta(seconds=0)
238 self.saw_overt_year = False
241 def _normalize_special_day_name(name: str) -> str:
242 """String normalization / canonicalization for date expressions."""
244 name = name.replace("'", '')
245 name = name.replace('xmas', 'christmas')
246 name = name.replace('mlk', 'martin luther king')
247 name = name.replace(' ', '')
248 eve = 'eve' if name[-3:] == 'eve' else ''
249 name = name[:5] + eve
250 name = name.replace('washi', 'presi')
253 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
254 """Figure out what unit a date expression piece is talking about."""
256 return TimeUnit.MONTHS
257 txt = orig.lower()[:3]
258 if txt in self.day_name_to_number:
259 return TimeUnit(self.day_name_to_number[txt])
260 elif txt in self.delta_unit_to_constant:
261 return TimeUnit(self.delta_unit_to_constant[txt])
262 raise ParseException(f'Invalid date unit: {orig}')
264 def _figure_out_time_unit(self, orig: str) -> int:
265 """Figure out what unit a time expression piece is talking about."""
266 txt = orig.lower()[:3]
267 if txt in self.time_delta_unit_to_constant:
268 return self.time_delta_unit_to_constant[txt]
269 raise ParseException(f'Invalid time unit: {orig}')
271 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
272 """Parse what we think is a special date name and return its datetime
273 (or None if it can't be parsed).
276 year = self.context.get('year', today.year)
277 name = DateParser._normalize_special_day_name(self.context['special'])
279 # Yesterday, today, tomorrow -- ignore any next/last
280 if name == 'today' or name == 'now':
283 return today + datetime.timedelta(days=-1)
285 return today + datetime.timedelta(days=+1)
287 next_last = self.context.get('special_next_last', '')
288 if next_last == 'next':
290 self.saw_overt_year = True
291 elif next_last == 'last':
293 self.saw_overt_year = True
297 return dateutil.easter.easter(year=year)
298 elif name == 'hallo':
299 return datetime.date(year=year, month=10, day=31)
301 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
302 if 'Observed' not in holiday_name:
303 holiday_name = DateParser._normalize_special_day_name(holiday_name)
304 if name == holiday_name:
306 if name == 'chriseve':
307 return datetime.date(year=year, month=12, day=24)
308 elif name == 'newyeeve':
309 return datetime.date(year=year, month=12, day=31)
312 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
313 """Handle date expressions like "the ides of March" which require
314 both the "ides" and the month since the definition of the "ides"
315 changes based on the length of the month.
317 assert 'ide' in day or 'non' in day
318 assert month_number in self.typical_days_per_month
319 typical_days_per_month = self.typical_days_per_month[month_number]
322 if typical_days_per_month == 31:
323 if self.context['day'] == 'ide':
330 if self.context['day'] == 'ide':
335 def _parse_normal_date(self) -> datetime.date:
336 if 'dow' in self.context:
338 while d.weekday() != self.context['dow']:
339 d += datetime.timedelta(days=1)
342 if 'month' not in self.context:
343 raise ParseException('Missing month')
344 if 'day' not in self.context:
345 raise ParseException('Missing day')
346 if 'year' not in self.context:
347 self.context['year'] = self.today.year
348 self.saw_overt_year = False
350 self.saw_overt_year = True
352 # Handling "ides" and "nones" requires both the day and month.
353 if self.context['day'] == 'ide' or self.context['day'] == 'non':
354 self.context['day'] = self._resolve_ides_nones(
355 self.context['day'], self.context['month']
358 return datetime.date(
359 year=self.context['year'],
360 month=self.context['month'],
361 day=self.context['day'],
364 def _parse_tz(self, txt: str) -> Any:
370 tz1 = pytz.timezone(txt)
378 tz2 = dateutil.tz.gettz(txt)
384 # Try constructing an offset in seconds
387 if txt_sign == '-' or txt_sign == '+':
388 sign = +1 if txt_sign == '+' else -1
390 minute = int(txt[-2:])
391 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
392 tzoffset = dateutil.tz.tzoffset(txt, offset)
398 def _get_int(self, txt: str) -> int:
399 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
401 while not txt[-1].isdigit():
405 # -- overridden methods invoked by parse walk --
407 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
410 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
413 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
414 """Populate self.datetime."""
415 if self.date is None:
416 self.date = self.today
417 year = self.date.year
418 month = self.date.month
421 if self.time is None:
422 self.time = datetime.time(0, 0, 0)
423 hour = self.time.hour
424 minute = self.time.minute
425 second = self.time.second
426 micros = self.time.microsecond
428 self.datetime = datetime.datetime(
436 tzinfo=self.time.tzinfo,
439 # Apply resudual adjustments to times here when we have a
441 self.datetime = self.datetime + self.timedelta
442 assert self.datetime is not None
443 self.time = datetime.time(
445 self.datetime.minute,
446 self.datetime.second,
447 self.datetime.microsecond,
448 self.datetime.tzinfo,
451 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
453 if ctx.singleDateExpr() is not None:
454 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
455 elif ctx.baseAndOffsetDateExpr() is not None:
456 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
458 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
460 if ctx.singleTimeExpr() is not None:
461 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
462 elif ctx.baseAndOffsetTimeExpr() is not None:
463 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
465 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
466 """When we leave the date expression, populate self.date."""
467 if 'special' in self.context:
468 self.date = self._parse_special_date(self.context['special'])
470 self.date = self._parse_normal_date()
471 assert self.date is not None
473 # For a single date, just return the date we pulled out.
474 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
477 # Otherwise treat self.date as a base date that we're modifying
479 if 'delta_int' not in self.context:
480 raise ParseException('Missing delta_int?!')
481 count = self.context['delta_int']
485 # Adjust count's sign based on the presence of 'before' or 'after'.
486 if 'delta_before_after' in self.context:
487 before_after = self.context['delta_before_after'].lower()
489 before_after == 'before'
490 or before_after == 'until'
491 or before_after == 'til'
492 or before_after == 'to'
496 # What are we counting units of?
497 if 'delta_unit' not in self.context:
498 raise ParseException('Missing delta_unit?!')
499 unit = self.context['delta_unit']
500 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
501 self.date = datetime_to_date(dt)
503 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
505 self.time = datetime.time(
506 self.context['hour'],
507 self.context['minute'],
508 self.context['seconds'],
509 self.context['micros'],
510 tzinfo=self.context.get('tz', None),
512 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
515 # If we get here there (should be) a relative adjustment to
517 if 'nth' in self.context:
518 count = self.context['nth']
519 elif 'time_delta_int' in self.context:
520 count = self.context['time_delta_int']
522 raise ParseException('Missing delta in relative time.')
526 # Adjust count's sign based on the presence of 'before' or 'after'.
527 if 'time_delta_before_after' in self.context:
528 before_after = self.context['time_delta_before_after'].lower()
530 before_after == 'before'
531 or before_after == 'until'
532 or before_after == 'til'
533 or before_after == 'to'
537 # What are we counting units of... assume minutes.
538 if 'time_delta_unit' not in self.context:
539 self.timedelta += datetime.timedelta(minutes=count)
541 unit = self.context['time_delta_unit']
542 if unit == TimeUnit.SECONDS:
543 self.timedelta += datetime.timedelta(seconds=count)
544 elif unit == TimeUnit.MINUTES:
545 self.timedelta = datetime.timedelta(minutes=count)
546 elif unit == TimeUnit.HOURS:
547 self.timedelta = datetime.timedelta(hours=count)
549 raise ParseException(f'Invalid Unit: "{unit}"')
551 def exitDeltaPlusMinusExpr(self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext) -> None:
555 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
558 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
560 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
562 self.context['delta_int'] = n
563 self.context['delta_unit'] = unit
565 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
567 unit = self._figure_out_date_unit(ctx.getText().lower())
569 raise ParseException(f'Bad delta unit: {ctx.getText()}')
571 self.context['delta_unit'] = unit
573 def exitDeltaNextLast(self, ctx: dateparse_utilsParser.DeltaNextLastContext) -> None:
575 txt = ctx.getText().lower()
577 raise ParseException(f'Bad next/last: {ctx.getText()}')
578 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
579 raise ParseException('Next/last expression expected to be relative to today.')
580 if txt[:4] == 'next':
581 self.context['delta_int'] = +1
582 self.context['day'] = self.now_datetime.day
583 self.context['month'] = self.now_datetime.month
584 self.context['year'] = self.now_datetime.year
585 self.saw_overt_year = True
586 elif txt[:4] == 'last':
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
593 raise ParseException(f'Bad next/last: {ctx.getText()}')
595 def exitCountUnitsBeforeAfterTimeExpr(
596 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
598 if 'nth' not in self.context:
599 raise ParseException(f'Bad count expression: {ctx.getText()}')
601 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
602 self.context['time_delta_unit'] = unit
604 raise ParseException(f'Bad delta unit: {ctx.getText()}')
605 if 'time_delta_before_after' not in self.context:
606 raise ParseException(f'Bad Before/After: {ctx.getText()}')
608 def exitDeltaTimeFraction(self, ctx: dateparse_utilsParser.DeltaTimeFractionContext) -> None:
610 txt = ctx.getText().lower()[:4]
612 self.context['time_delta_int'] = 15
613 self.context['time_delta_unit'] = TimeUnit.MINUTES
615 self.context['time_delta_int'] = 30
616 self.context['time_delta_unit'] = TimeUnit.MINUTES
618 raise ParseException(f'Bad time fraction {ctx.getText()}')
620 raise ParseException(f'Bad time fraction {ctx.getText()}')
622 def exitDeltaBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
624 txt = ctx.getText().lower()
626 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
628 self.context['delta_before_after'] = txt
630 def exitDeltaTimeBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
632 txt = ctx.getText().lower()
634 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
636 self.context['time_delta_before_after'] = txt
638 def exitNthWeekdayInMonthMaybeYearExpr(
639 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
641 """Do a bunch of work to convert expressions like...
643 'the 2nd Friday of June' -and-
644 'the last Wednesday in October'
646 ...into base + offset expressions instead.
649 if 'nth' not in self.context:
650 raise ParseException(f'Missing nth number: {ctx.getText()}')
651 n = self.context['nth']
652 if n < 1 or n > 5: # months never have more than 5 Foodays
654 raise ParseException(f'Invalid nth number: {ctx.getText()}')
655 del self.context['nth']
656 self.context['delta_int'] = n
658 year = self.context.get('year', self.today.year)
659 if 'month' not in self.context:
660 raise ParseException(f'Missing month expression: {ctx.getText()}')
661 month = self.context['month']
663 dow = self.context['dow']
664 del self.context['dow']
665 self.context['delta_unit'] = dow
667 # For the nth Fooday in Month, start at the 1st of the
668 # month and count ahead N Foodays. For the last Fooday in
669 # Month, start at the last of the month and count back one
676 tmp_date = datetime.date(year=year, month=month, day=1)
677 tmp_date = tmp_date - datetime.timedelta(days=1)
679 self.context['year'] = tmp_date.year
680 self.context['month'] = tmp_date.month
681 self.context['day'] = tmp_date.day
683 # The delta adjustment code can handle the case where
684 # the last day of the month is the day we're looking
687 self.context['year'] = year
688 self.context['month'] = month
689 self.context['day'] = 1
690 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
692 raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}')
694 def exitFirstLastWeekdayInMonthMaybeYearExpr(
696 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
698 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
700 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
702 i = self._get_int(ctx.getText())
704 raise ParseException(f'Bad nth expression: {ctx.getText()}')
706 self.context['nth'] = i
708 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
716 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
718 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
720 self.context['nth'] = txt
722 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
724 dow = ctx.getText().lower()[:3]
725 dow = self.day_name_to_number.get(dow, None)
727 raise ParseException('Bad day of week')
729 self.context['dow'] = dow
731 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
733 day = ctx.getText().lower()
735 self.context['day'] = 'ide'
738 self.context['day'] = 'non'
741 self.context['day'] = 1
743 day = self._get_int(day)
744 if day < 1 or day > 31:
745 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
747 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
748 self.context['day'] = day
750 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
752 month = ctx.getText()
753 while month[0] == '/' or month[0] == '-':
755 month = month[:3].lower()
756 month = self.month_name_to_number.get(month, None)
758 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
760 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
762 self.context['month'] = month
764 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
766 month = self._get_int(ctx.getText())
767 if month < 1 or month > 12:
768 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
770 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
772 self.context['month'] = month
774 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
776 year = self._get_int(ctx.getText())
778 raise ParseException(f'Bad year expression: {ctx.getText()}')
780 raise ParseException(f'Bad year expression: {ctx.getText()}')
782 self.saw_overt_year = True
783 self.context['year'] = year
785 def exitSpecialDateMaybeYearExpr(
786 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
789 special = ctx.specialDate().getText().lower()
790 self.context['special'] = special
792 raise ParseException(f'Bad specialDate expression: {ctx.specialDate().getText()}')
794 mod = ctx.thisNextLast()
796 if mod.THIS() is not None:
797 self.context['special_next_last'] = 'this'
798 elif mod.NEXT() is not None:
799 self.context['special_next_last'] = 'next'
800 elif mod.LAST() is not None:
801 self.context['special_next_last'] = 'last'
803 raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}')
805 def exitNFoosFromTodayAgoExpr(
806 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
808 d = self.now_datetime
810 count = self._get_int(ctx.unsignedInt().getText())
811 unit = ctx.deltaUnit().getText().lower()
812 ago_from_now = ctx.AGO_FROM_NOW().getText()
814 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}')
816 if "ago" in ago_from_now or "back" in ago_from_now:
819 unit = self._figure_out_date_unit(unit)
820 d = n_timeunits_from_base(count, TimeUnit(unit), d)
821 self.context['year'] = d.year
822 self.context['month'] = d.month
823 self.context['day'] = d.day
825 def exitDeltaRelativeToTodayExpr(
826 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
828 # When someone says "next week" they mean a week from now.
829 # Likewise next month or last year. These expressions are now
832 # But when someone says "this Friday" they mean "this coming
833 # Friday". It would be weird to say "this Friday" if today
834 # was already Friday but I'm parsing it to mean: the next day
835 # that is a Friday. So when you say "next Friday" you mean
836 # the Friday after this coming Friday, or 2 Fridays from now.
838 # This set handles this weirdness.
850 d = self.now_datetime
852 mod = ctx.thisNextLast()
853 unit = ctx.deltaUnit().getText().lower()
854 unit = self._figure_out_date_unit(unit)
868 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
870 raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}')
871 d = n_timeunits_from_base(count, TimeUnit(unit), d)
872 self.context['year'] = d.year
873 self.context['month'] = d.month
874 self.context['day'] = d.day
876 def exitSpecialTimeExpr(self, ctx: dateparse_utilsParser.SpecialTimeExprContext) -> None:
878 txt = ctx.specialTime().getText().lower()
880 raise ParseException(f'Bad special time expression: {ctx.getText()}')
882 if txt == 'noon' or txt == 'midday':
883 self.context['hour'] = 12
884 self.context['minute'] = 0
885 self.context['seconds'] = 0
886 self.context['micros'] = 0
887 elif txt == 'midnight':
888 self.context['hour'] = 0
889 self.context['minute'] = 0
890 self.context['seconds'] = 0
891 self.context['micros'] = 0
893 raise ParseException(f'Bad special time expression: {txt}')
896 tz = ctx.tzExpr().getText()
897 self.context['tz'] = self._parse_tz(tz)
901 def exitTwelveHourTimeExpr(self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext) -> None:
903 hour = ctx.hour().getText()
904 while not hour[-1].isdigit():
906 hour = self._get_int(hour)
908 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
909 if hour <= 0 or hour > 12:
910 raise ParseException(f'Bad hour (out of range): {hour}')
913 minute = self._get_int(ctx.minute().getText())
916 if minute < 0 or minute > 59:
917 raise ParseException(f'Bad minute (out of range): {minute}')
918 self.context['minute'] = minute
921 seconds = self._get_int(ctx.second().getText())
924 if seconds < 0 or seconds > 59:
925 raise ParseException(f'Bad second (out of range): {seconds}')
926 self.context['seconds'] = seconds
929 micros = self._get_int(ctx.micros().getText())
932 if micros < 0 or micros > 1000000:
933 raise ParseException(f'Bad micros (out of range): {micros}')
934 self.context['micros'] = micros
937 ampm = ctx.ampm().getText()
939 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
944 self.context['hour'] = hour
947 tz = ctx.tzExpr().getText()
948 self.context['tz'] = self._parse_tz(tz)
952 def exitTwentyFourHourTimeExpr(
953 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
956 hour = ctx.hour().getText()
957 while not hour[-1].isdigit():
959 hour = self._get_int(hour)
961 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
962 if hour < 0 or hour > 23:
963 raise ParseException(f'Bad hour (out of range): {hour}')
964 self.context['hour'] = hour
967 minute = self._get_int(ctx.minute().getText())
970 if minute < 0 or minute > 59:
971 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
972 self.context['minute'] = minute
975 seconds = self._get_int(ctx.second().getText())
978 if seconds < 0 or seconds > 59:
979 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
980 self.context['seconds'] = seconds
983 micros = self._get_int(ctx.micros().getText())
986 if micros < 0 or micros >= 1000000:
987 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
988 self.context['micros'] = micros
991 tz = ctx.tzExpr().getText()
992 self.context['tz'] = self._parse_tz(tz)
997 @bootstrap.initialize
999 parser = DateParser()
1000 for line in sys.stdin:
1002 line = re.sub(r"#.*$", "", line)
1003 if re.match(r"^ *$", line) is not None:
1006 dt = parser.parse(line)
1007 except Exception as e:
1009 print("Unrecognized.")
1011 assert dt is not None
1012 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1016 if __name__ == "__main__":