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 (
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:
59 self.message = message
62 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
63 """An error listener that raises ParseExceptions."""
65 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
66 raise ParseException(msg)
69 self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs
73 def reportAttemptingFullContext(
74 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
78 def reportContextSensitivity(
79 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
84 @decorator_utils.decorate_matching_methods_with(
86 acl=acl.StringWildcardBasedACL(
91 denied_patterns=['enterEveryRule', 'exitEveryRule'],
92 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
96 class DateParser(dateparse_utilsListener):
97 PARSE_TYPE_SINGLE_DATE_EXPR = 1
98 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
99 PARSE_TYPE_SINGLE_TIME_EXPR = 3
100 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
102 def __init__(self, *, override_now_for_test_purposes=None) -> None:
103 """C'tor. Passing a value to override_now_for_test_purposes can be
104 used to force this instance to use a custom date/time for its
105 idea of "now" so that the code can be more easily unittested.
106 Leave as None for real use cases.
108 self.month_name_to_number = {
123 # Used only for ides/nones. Month length on a non-leap year.
124 self.typical_days_per_month = {
139 # N.B. day number is also synched with datetime_utils.TimeUnit values
140 # which allows expressions like "3 wednesdays from now" to work.
141 self.day_name_to_number = {
151 # These TimeUnits are defined in datetime_utils and are used as params
152 # to datetime_utils.n_timeunits_from_base.
153 self.time_delta_unit_to_constant = {
154 'hou': TimeUnit.HOURS,
155 'min': TimeUnit.MINUTES,
156 'sec': TimeUnit.SECONDS,
158 self.delta_unit_to_constant = {
159 'day': TimeUnit.DAYS,
160 'wor': TimeUnit.WORKDAYS,
161 'wee': TimeUnit.WEEKS,
162 'mon': TimeUnit.MONTHS,
163 'yea': TimeUnit.YEARS,
165 self.override_now_for_test_purposes = override_now_for_test_purposes
168 def parse(self, date_string: str) -> Optional[datetime.datetime]:
169 """Parse a date/time expression and return a timezone agnostic
170 datetime on success. Also sets self.datetime, self.date and
171 self.time which can each be accessed other methods on the
172 class: get_datetime(), get_date() and get_time(). Raises a
173 ParseException with a helpful(?) message on parse error or
176 To get an idea of what expressions can be parsed, check out
177 the unittest and the grammar.
181 txt = '3 weeks before last tues at 9:15am'
184 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
186 # dt1 and dt2 will be identical other than the fact that
187 # the latter's tzinfo will be set to PST/PDT.
189 This is the main entrypoint to this class for caller code.
191 date_string = date_string.strip()
192 date_string = re.sub(r'\s+', ' ', date_string)
194 listener = RaisingErrorListener()
195 input_stream = antlr4.InputStream(date_string)
196 lexer = dateparse_utilsLexer(input_stream)
197 lexer.removeErrorListeners()
198 lexer.addErrorListener(listener)
199 stream = antlr4.CommonTokenStream(lexer)
200 parser = dateparse_utilsParser(stream)
201 parser.removeErrorListeners()
202 parser.addErrorListener(listener)
203 tree = parser.parse()
204 walker = antlr4.ParseTreeWalker()
205 walker.walk(self, tree)
208 def get_date(self) -> Optional[datetime.date]:
209 """Return the date part or None."""
212 def get_time(self) -> Optional[datetime.time]:
213 """Return the time part or None."""
216 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
217 """Return as a datetime. Parsed date expressions without any time
218 part return hours = minutes = seconds = microseconds = 0 (i.e. at
219 midnight that day). Parsed time expressions without any date part
220 default to date = today.
222 The optional tz param allows the caller to request the datetime be
223 timezone aware and sets the tzinfo to the indicated zone. Defaults
224 to timezone naive (i.e. tzinfo = None).
229 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
235 """Reset at init and between parses."""
236 if self.override_now_for_test_purposes is None:
237 self.now_datetime = datetime.datetime.now()
238 self.today = datetime.date.today()
240 self.now_datetime = self.override_now_for_test_purposes
241 self.today = datetime_to_date(self.override_now_for_test_purposes)
242 self.date: Optional[datetime.date] = None
243 self.time: Optional[datetime.time] = None
244 self.datetime: Optional[datetime.datetime] = None
245 self.context: Dict[str, Any] = {}
246 self.timedelta = datetime.timedelta(seconds=0)
247 self.saw_overt_year = False
250 def _normalize_special_day_name(name: str) -> str:
251 """String normalization / canonicalization for date expressions."""
253 name = name.replace("'", '')
254 name = name.replace('xmas', 'christmas')
255 name = name.replace('mlk', 'martin luther king')
256 name = name.replace(' ', '')
257 eve = 'eve' if name[-3:] == 'eve' else ''
258 name = name[:5] + eve
259 name = name.replace('washi', 'presi')
262 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
263 """Figure out what unit a date expression piece is talking about."""
265 return TimeUnit.MONTHS
266 txt = orig.lower()[:3]
267 if txt in self.day_name_to_number:
268 return TimeUnit(self.day_name_to_number[txt])
269 elif txt in self.delta_unit_to_constant:
270 return TimeUnit(self.delta_unit_to_constant[txt])
271 raise ParseException(f'Invalid date unit: {orig}')
273 def _figure_out_time_unit(self, orig: str) -> int:
274 """Figure out what unit a time expression piece is talking about."""
275 txt = orig.lower()[:3]
276 if txt in self.time_delta_unit_to_constant:
277 return self.time_delta_unit_to_constant[txt]
278 raise ParseException(f'Invalid time unit: {orig}')
280 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
281 """Parse what we think is a special date name and return its datetime
282 (or None if it can't be parsed).
285 year = self.context.get('year', today.year)
286 name = DateParser._normalize_special_day_name(self.context['special'])
288 # Yesterday, today, tomorrow -- ignore any next/last
289 if name == 'today' or name == 'now':
292 return today + datetime.timedelta(days=-1)
294 return today + datetime.timedelta(days=+1)
296 next_last = self.context.get('special_next_last', '')
297 if next_last == 'next':
299 self.saw_overt_year = True
300 elif next_last == 'last':
302 self.saw_overt_year = True
306 return dateutil.easter.easter(year=year)
307 elif name == 'hallo':
308 return datetime.date(year=year, month=10, day=31)
310 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
311 if 'Observed' not in holiday_name:
312 holiday_name = DateParser._normalize_special_day_name(holiday_name)
313 if name == holiday_name:
315 if name == 'chriseve':
316 return datetime.date(year=year, month=12, day=24)
317 elif name == 'newyeeve':
318 return datetime.date(year=year, month=12, day=31)
321 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
322 """Handle date expressions like "the ides of March" which require
323 both the "ides" and the month since the definition of the "ides"
324 changes based on the length of the month.
326 assert 'ide' in day or 'non' in day
327 assert month_number in self.typical_days_per_month
328 typical_days_per_month = self.typical_days_per_month[month_number]
331 if typical_days_per_month == 31:
332 if self.context['day'] == 'ide':
339 if self.context['day'] == 'ide':
344 def _parse_normal_date(self) -> datetime.date:
345 if 'dow' in self.context:
347 while d.weekday() != self.context['dow']:
348 d += datetime.timedelta(days=1)
351 if 'month' not in self.context:
352 raise ParseException('Missing month')
353 if 'day' not in self.context:
354 raise ParseException('Missing day')
355 if 'year' not in self.context:
356 self.context['year'] = self.today.year
357 self.saw_overt_year = False
359 self.saw_overt_year = True
361 # Handling "ides" and "nones" requires both the day and month.
362 if self.context['day'] == 'ide' or self.context['day'] == 'non':
363 self.context['day'] = self._resolve_ides_nones(
364 self.context['day'], self.context['month']
367 return datetime.date(
368 year=self.context['year'],
369 month=self.context['month'],
370 day=self.context['day'],
373 def _parse_tz(self, txt: str) -> Any:
379 tz1 = pytz.timezone(txt)
387 tz2 = dateutil.tz.gettz(txt)
393 # Try constructing an offset in seconds
396 if txt_sign == '-' or txt_sign == '+':
397 sign = +1 if txt_sign == '+' else -1
399 minute = int(txt[-2:])
400 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
401 tzoffset = dateutil.tz.tzoffset(txt, offset)
407 def _get_int(self, txt: str) -> int:
408 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
410 while not txt[-1].isdigit():
414 # -- overridden methods invoked by parse walk --
416 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
419 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
422 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
423 """Populate self.datetime."""
424 if self.date is None:
425 self.date = self.today
426 year = self.date.year
427 month = self.date.month
430 if self.time is None:
431 self.time = datetime.time(0, 0, 0)
432 hour = self.time.hour
433 minute = self.time.minute
434 second = self.time.second
435 micros = self.time.microsecond
437 self.datetime = datetime.datetime(
438 year, month, day, hour, minute, second, micros, tzinfo=self.time.tzinfo
441 # Apply resudual adjustments to times here when we have a
443 self.datetime = self.datetime + self.timedelta
444 assert self.datetime is not None
445 self.time = datetime.time(
447 self.datetime.minute,
448 self.datetime.second,
449 self.datetime.microsecond,
450 self.datetime.tzinfo,
453 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
455 if ctx.singleDateExpr() is not None:
456 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
457 elif ctx.baseAndOffsetDateExpr() is not None:
458 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
460 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
462 if ctx.singleTimeExpr() is not None:
463 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
464 elif ctx.baseAndOffsetTimeExpr() is not None:
465 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
467 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
468 """When we leave the date expression, populate self.date."""
469 if 'special' in self.context:
470 self.date = self._parse_special_date(self.context['special'])
472 self.date = self._parse_normal_date()
473 assert self.date is not None
475 # For a single date, just return the date we pulled out.
476 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
479 # Otherwise treat self.date as a base date that we're modifying
481 if 'delta_int' not in self.context:
482 raise ParseException('Missing delta_int?!')
483 count = self.context['delta_int']
487 # Adjust count's sign based on the presence of 'before' or 'after'.
488 if 'delta_before_after' in self.context:
489 before_after = self.context['delta_before_after'].lower()
491 before_after == 'before'
492 or before_after == 'until'
493 or before_after == 'til'
494 or before_after == 'to'
498 # What are we counting units of?
499 if 'delta_unit' not in self.context:
500 raise ParseException('Missing delta_unit?!')
501 unit = self.context['delta_unit']
502 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
503 self.date = datetime_to_date(dt)
505 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
507 self.time = datetime.time(
508 self.context['hour'],
509 self.context['minute'],
510 self.context['seconds'],
511 self.context['micros'],
512 tzinfo=self.context.get('tz', None),
514 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
517 # If we get here there (should be) a relative adjustment to
519 if 'nth' in self.context:
520 count = self.context['nth']
521 elif 'time_delta_int' in self.context:
522 count = self.context['time_delta_int']
524 raise ParseException('Missing delta in relative time.')
528 # Adjust count's sign based on the presence of 'before' or 'after'.
529 if 'time_delta_before_after' in self.context:
530 before_after = self.context['time_delta_before_after'].lower()
532 before_after == 'before'
533 or before_after == 'until'
534 or before_after == 'til'
535 or before_after == 'to'
539 # What are we counting units of... assume minutes.
540 if 'time_delta_unit' not in self.context:
541 self.timedelta += datetime.timedelta(minutes=count)
543 unit = self.context['time_delta_unit']
544 if unit == TimeUnit.SECONDS:
545 self.timedelta += datetime.timedelta(seconds=count)
546 elif unit == TimeUnit.MINUTES:
547 self.timedelta = datetime.timedelta(minutes=count)
548 elif unit == TimeUnit.HOURS:
549 self.timedelta = datetime.timedelta(hours=count)
551 raise ParseException(f'Invalid Unit: "{unit}"')
553 def exitDeltaPlusMinusExpr(
554 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
559 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
562 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
564 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
566 self.context['delta_int'] = n
567 self.context['delta_unit'] = unit
569 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
571 unit = self._figure_out_date_unit(ctx.getText().lower())
573 raise ParseException(f'Bad delta unit: {ctx.getText()}')
575 self.context['delta_unit'] = unit
577 def exitDeltaNextLast(
578 self, ctx: dateparse_utilsParser.DeltaNextLastContext
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(
586 'Next/last expression expected to be relative to today.'
588 if txt[:4] == 'next':
589 self.context['delta_int'] = +1
590 self.context['day'] = self.now_datetime.day
591 self.context['month'] = self.now_datetime.month
592 self.context['year'] = self.now_datetime.year
593 self.saw_overt_year = True
594 elif txt[:4] == 'last':
595 self.context['delta_int'] = -1
596 self.context['day'] = self.now_datetime.day
597 self.context['month'] = self.now_datetime.month
598 self.context['year'] = self.now_datetime.year
599 self.saw_overt_year = True
601 raise ParseException(f'Bad next/last: {ctx.getText()}')
603 def exitCountUnitsBeforeAfterTimeExpr(
604 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
606 if 'nth' not in self.context:
607 raise ParseException(f'Bad count expression: {ctx.getText()}')
609 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
610 self.context['time_delta_unit'] = unit
612 raise ParseException(f'Bad delta unit: {ctx.getText()}')
613 if 'time_delta_before_after' not in self.context:
614 raise ParseException(f'Bad Before/After: {ctx.getText()}')
616 def exitDeltaTimeFraction(
617 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
620 txt = ctx.getText().lower()[:4]
622 self.context['time_delta_int'] = 15
623 self.context['time_delta_unit'] = TimeUnit.MINUTES
625 self.context['time_delta_int'] = 30
626 self.context['time_delta_unit'] = TimeUnit.MINUTES
628 raise ParseException(f'Bad time fraction {ctx.getText()}')
630 raise ParseException(f'Bad time fraction {ctx.getText()}')
632 def exitDeltaBeforeAfter(
633 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
636 txt = ctx.getText().lower()
638 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
640 self.context['delta_before_after'] = txt
642 def exitDeltaTimeBeforeAfter(
643 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
646 txt = ctx.getText().lower()
648 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
650 self.context['time_delta_before_after'] = txt
652 def exitNthWeekdayInMonthMaybeYearExpr(
653 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
655 """Do a bunch of work to convert expressions like...
657 'the 2nd Friday of June' -and-
658 'the last Wednesday in October'
660 ...into base + offset expressions instead.
663 if 'nth' not in self.context:
664 raise ParseException(f'Missing nth number: {ctx.getText()}')
665 n = self.context['nth']
666 if n < 1 or n > 5: # months never have more than 5 Foodays
668 raise ParseException(f'Invalid nth number: {ctx.getText()}')
669 del self.context['nth']
670 self.context['delta_int'] = n
672 year = self.context.get('year', self.today.year)
673 if 'month' not in self.context:
674 raise ParseException(f'Missing month expression: {ctx.getText()}')
675 month = self.context['month']
677 dow = self.context['dow']
678 del self.context['dow']
679 self.context['delta_unit'] = dow
681 # For the nth Fooday in Month, start at the 1st of the
682 # month and count ahead N Foodays. For the last Fooday in
683 # Month, start at the last of the month and count back one
690 tmp_date = datetime.date(year=year, month=month, day=1)
691 tmp_date = tmp_date - datetime.timedelta(days=1)
693 self.context['year'] = tmp_date.year
694 self.context['month'] = tmp_date.month
695 self.context['day'] = tmp_date.day
697 # The delta adjustment code can handle the case where
698 # the last day of the month is the day we're looking
701 self.context['year'] = year
702 self.context['month'] = month
703 self.context['day'] = 1
704 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
706 raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}')
708 def exitFirstLastWeekdayInMonthMaybeYearExpr(
710 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
712 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
714 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
716 i = self._get_int(ctx.getText())
718 raise ParseException(f'Bad nth expression: {ctx.getText()}')
720 self.context['nth'] = i
722 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
730 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
732 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
734 self.context['nth'] = txt
736 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
738 dow = ctx.getText().lower()[:3]
739 dow = self.day_name_to_number.get(dow, None)
741 raise ParseException('Bad day of week')
743 self.context['dow'] = dow
745 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
747 day = ctx.getText().lower()
749 self.context['day'] = 'ide'
752 self.context['day'] = 'non'
755 self.context['day'] = 1
757 day = self._get_int(day)
758 if day < 1 or day > 31:
759 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
761 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
762 self.context['day'] = day
764 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
766 month = ctx.getText()
767 while month[0] == '/' or month[0] == '-':
769 month = month[:3].lower()
770 month = self.month_name_to_number.get(month, None)
772 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
774 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
776 self.context['month'] = month
778 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
780 month = self._get_int(ctx.getText())
781 if month < 1 or month > 12:
782 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
784 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
786 self.context['month'] = month
788 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
790 year = self._get_int(ctx.getText())
792 raise ParseException(f'Bad year expression: {ctx.getText()}')
794 raise ParseException(f'Bad year expression: {ctx.getText()}')
796 self.saw_overt_year = True
797 self.context['year'] = year
799 def exitSpecialDateMaybeYearExpr(
800 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
803 special = ctx.specialDate().getText().lower()
804 self.context['special'] = special
806 raise ParseException(
807 f'Bad specialDate expression: {ctx.specialDate().getText()}'
810 mod = ctx.thisNextLast()
812 if mod.THIS() is not None:
813 self.context['special_next_last'] = 'this'
814 elif mod.NEXT() is not None:
815 self.context['special_next_last'] = 'next'
816 elif mod.LAST() is not None:
817 self.context['special_next_last'] = 'last'
819 raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}')
821 def exitNFoosFromTodayAgoExpr(
822 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
824 d = self.now_datetime
826 count = self._get_int(ctx.unsignedInt().getText())
827 unit = ctx.deltaUnit().getText().lower()
828 ago_from_now = ctx.AGO_FROM_NOW().getText()
830 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}')
832 if "ago" in ago_from_now or "back" in ago_from_now:
835 unit = self._figure_out_date_unit(unit)
836 d = n_timeunits_from_base(count, TimeUnit(unit), d)
837 self.context['year'] = d.year
838 self.context['month'] = d.month
839 self.context['day'] = d.day
841 def exitDeltaRelativeToTodayExpr(
842 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
844 # When someone says "next week" they mean a week from now.
845 # Likewise next month or last year. These expressions are now
848 # But when someone says "this Friday" they mean "this coming
849 # Friday". It would be weird to say "this Friday" if today
850 # was already Friday but I'm parsing it to mean: the next day
851 # that is a Friday. So when you say "next Friday" you mean
852 # the Friday after this coming Friday, or 2 Fridays from now.
854 # This set handles this weirdness.
866 d = self.now_datetime
868 mod = ctx.thisNextLast()
869 unit = ctx.deltaUnit().getText().lower()
870 unit = self._figure_out_date_unit(unit)
884 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
886 raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}')
887 d = n_timeunits_from_base(count, TimeUnit(unit), d)
888 self.context['year'] = d.year
889 self.context['month'] = d.month
890 self.context['day'] = d.day
892 def exitSpecialTimeExpr(
893 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
896 txt = ctx.specialTime().getText().lower()
898 raise ParseException(f'Bad special time expression: {ctx.getText()}')
900 if txt == 'noon' or txt == 'midday':
901 self.context['hour'] = 12
902 self.context['minute'] = 0
903 self.context['seconds'] = 0
904 self.context['micros'] = 0
905 elif txt == 'midnight':
906 self.context['hour'] = 0
907 self.context['minute'] = 0
908 self.context['seconds'] = 0
909 self.context['micros'] = 0
911 raise ParseException(f'Bad special time expression: {txt}')
914 tz = ctx.tzExpr().getText()
915 self.context['tz'] = self._parse_tz(tz)
919 def exitTwelveHourTimeExpr(
920 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
923 hour = ctx.hour().getText()
924 while not hour[-1].isdigit():
926 hour = self._get_int(hour)
928 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
929 if hour <= 0 or hour > 12:
930 raise ParseException(f'Bad hour (out of range): {hour}')
933 minute = self._get_int(ctx.minute().getText())
936 if minute < 0 or minute > 59:
937 raise ParseException(f'Bad minute (out of range): {minute}')
938 self.context['minute'] = minute
941 seconds = self._get_int(ctx.second().getText())
944 if seconds < 0 or seconds > 59:
945 raise ParseException(f'Bad second (out of range): {seconds}')
946 self.context['seconds'] = seconds
949 micros = self._get_int(ctx.micros().getText())
952 if micros < 0 or micros > 1000000:
953 raise ParseException(f'Bad micros (out of range): {micros}')
954 self.context['micros'] = micros
957 ampm = ctx.ampm().getText()
959 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
964 self.context['hour'] = hour
967 tz = ctx.tzExpr().getText()
968 self.context['tz'] = self._parse_tz(tz)
972 def exitTwentyFourHourTimeExpr(
973 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
976 hour = ctx.hour().getText()
977 while not hour[-1].isdigit():
979 hour = self._get_int(hour)
981 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
982 if hour < 0 or hour > 23:
983 raise ParseException(f'Bad hour (out of range): {hour}')
984 self.context['hour'] = hour
987 minute = self._get_int(ctx.minute().getText())
990 if minute < 0 or minute > 59:
991 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
992 self.context['minute'] = minute
995 seconds = self._get_int(ctx.second().getText())
998 if seconds < 0 or seconds > 59:
999 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1000 self.context['seconds'] = seconds
1003 micros = self._get_int(ctx.micros().getText())
1006 if micros < 0 or micros >= 1000000:
1007 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1008 self.context['micros'] = micros
1011 tz = ctx.tzExpr().getText()
1012 self.context['tz'] = self._parse_tz(tz)
1017 @bootstrap.initialize
1019 parser = DateParser()
1020 for line in sys.stdin:
1022 line = re.sub(r"#.*$", "", line)
1023 if re.match(r"^ *$", line) is not None:
1026 dt = parser.parse(line)
1027 except Exception as e:
1029 print("Unrecognized.")
1031 assert dt is not None
1032 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1036 if __name__ == "__main__":