4 Parse dates in a variety of formats.
10 import holidays # type: ignore
14 from typing import Any, Callable, Dict, Optional
16 import antlr4 # type: ignore
17 import dateutil.easter
23 from datetime_utils import (
25 n_timeunits_from_base,
29 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
30 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
31 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
32 import decorator_utils
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)
70 self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs
74 def reportAttemptingFullContext(
75 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
79 def reportContextSensitivity(
80 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
85 @decorator_utils.decorate_matching_methods_with(
87 acl=acl.StringWildcardBasedACL(
92 denied_patterns=['enterEveryRule', 'exitEveryRule'],
93 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
97 class DateParser(dateparse_utilsListener):
98 PARSE_TYPE_SINGLE_DATE_EXPR = 1
99 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
100 PARSE_TYPE_SINGLE_TIME_EXPR = 3
101 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
103 def __init__(self, *, override_now_for_test_purposes=None) -> None:
104 """C'tor. Passing a value to override_now_for_test_purposes can be
105 used to force this instance to use a custom date/time for its
106 idea of "now" so that the code can be more easily unittested.
107 Leave as None for real use cases.
109 self.month_name_to_number = {
124 # Used only for ides/nones. Month length on a non-leap year.
125 self.typical_days_per_month = {
140 # N.B. day number is also synched with datetime_utils.TimeUnit values
141 # which allows expressions like "3 wednesdays from now" to work.
142 self.day_name_to_number = {
152 # These TimeUnits are defined in datetime_utils and are used as params
153 # to datetime_utils.n_timeunits_from_base.
154 self.time_delta_unit_to_constant = {
155 'hou': TimeUnit.HOURS,
156 'min': TimeUnit.MINUTES,
157 'sec': TimeUnit.SECONDS,
159 self.delta_unit_to_constant = {
160 'day': TimeUnit.DAYS,
161 'wor': TimeUnit.WORKDAYS,
162 'wee': TimeUnit.WEEKS,
163 'mon': TimeUnit.MONTHS,
164 'yea': TimeUnit.YEARS,
166 self.override_now_for_test_purposes = override_now_for_test_purposes
169 def parse(self, date_string: str) -> Optional[datetime.datetime]:
170 """Parse a date/time expression and return a timezone agnostic
171 datetime on success. Also sets self.datetime, self.date and
172 self.time which can each be accessed other methods on the
173 class: get_datetime(), get_date() and get_time(). Raises a
174 ParseException with a helpful(?) message on parse error or
177 To get an idea of what expressions can be parsed, check out
178 the unittest and the grammar.
182 txt = '3 weeks before last tues at 9:15am'
185 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
187 # dt1 and dt2 will be identical other than the fact that
188 # the latter's tzinfo will be set to PST/PDT.
190 This is the main entrypoint to this class for caller code.
192 date_string = date_string.strip()
193 date_string = re.sub(r'\s+', ' ', date_string)
195 listener = RaisingErrorListener()
196 input_stream = antlr4.InputStream(date_string)
197 lexer = dateparse_utilsLexer(input_stream)
198 lexer.removeErrorListeners()
199 lexer.addErrorListener(listener)
200 stream = antlr4.CommonTokenStream(lexer)
201 parser = dateparse_utilsParser(stream)
202 parser.removeErrorListeners()
203 parser.addErrorListener(listener)
204 tree = parser.parse()
205 walker = antlr4.ParseTreeWalker()
206 walker.walk(self, tree)
209 def get_date(self) -> Optional[datetime.date]:
210 """Return the date part or None."""
213 def get_time(self) -> Optional[datetime.time]:
214 """Return the time part or None."""
217 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
218 """Return as a datetime. Parsed date expressions without any time
219 part return hours = minutes = seconds = microseconds = 0 (i.e. at
220 midnight that day). Parsed time expressions without any date part
221 default to date = today.
223 The optional tz param allows the caller to request the datetime be
224 timezone aware and sets the tzinfo to the indicated zone. Defaults
225 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 tz = pytz.timezone(txt)
387 tz = dateutil.tz.gettz(txt)
393 # Try constructing an offset in seconds
396 if sign == '-' or sign == '+':
397 sign = +1 if 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 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()
552 def exitDeltaPlusMinusExpr(
553 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
558 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
561 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
563 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
565 self.context['delta_int'] = n
566 self.context['delta_unit'] = unit
568 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
570 unit = self._figure_out_date_unit(ctx.getText().lower())
572 raise ParseException(f'Bad delta unit: {ctx.getText()}')
574 self.context['delta_unit'] = unit
576 def exitDeltaNextLast(
577 self, ctx: dateparse_utilsParser.DeltaNextLastContext
580 txt = ctx.getText().lower()
582 raise ParseException(f'Bad next/last: {ctx.getText()}')
583 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
584 raise ParseException(
585 'Next/last expression expected to be relative to today.'
587 if txt[:4] == 'next':
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
593 elif txt[:4] == 'last':
594 self.context['delta_int'] = -1
595 self.context['day'] = self.now_datetime.day
596 self.context['month'] = self.now_datetime.month
597 self.context['year'] = self.now_datetime.year
598 self.saw_overt_year = True
600 raise ParseException(f'Bad next/last: {ctx.getText()}')
602 def exitCountUnitsBeforeAfterTimeExpr(
603 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
605 if 'nth' not in self.context:
606 raise ParseException(f'Bad count expression: {ctx.getText()}')
608 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
609 self.context['time_delta_unit'] = unit
611 raise ParseException(f'Bad delta unit: {ctx.getText()}')
612 if 'time_delta_before_after' not in self.context:
613 raise ParseException(f'Bad Before/After: {ctx.getText()}')
615 def exitDeltaTimeFraction(
616 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
619 txt = ctx.getText().lower()[:4]
621 self.context['time_delta_int'] = 15
622 self.context['time_delta_unit'] = TimeUnit.MINUTES
624 self.context['time_delta_int'] = 30
625 self.context['time_delta_unit'] = TimeUnit.MINUTES
627 raise ParseException(f'Bad time fraction {ctx.getText()}')
629 raise ParseException(f'Bad time fraction {ctx.getText()}')
631 def exitDeltaBeforeAfter(
632 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
635 txt = ctx.getText().lower()
637 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
639 self.context['delta_before_after'] = txt
641 def exitDeltaTimeBeforeAfter(
642 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
645 txt = ctx.getText().lower()
647 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
649 self.context['time_delta_before_after'] = txt
651 def exitNthWeekdayInMonthMaybeYearExpr(
652 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
654 """Do a bunch of work to convert expressions like...
656 'the 2nd Friday of June' -and-
657 'the last Wednesday in October'
659 ...into base + offset expressions instead.
662 if 'nth' not in self.context:
663 raise ParseException(f'Missing nth number: {ctx.getText()}')
664 n = self.context['nth']
665 if n < 1 or n > 5: # months never have more than 5 Foodays
667 raise ParseException(f'Invalid nth number: {ctx.getText()}')
668 del self.context['nth']
669 self.context['delta_int'] = n
671 year = self.context.get('year', self.today.year)
672 if 'month' not in self.context:
673 raise ParseException(f'Missing month expression: {ctx.getText()}')
674 month = self.context['month']
676 dow = self.context['dow']
677 del self.context['dow']
678 self.context['delta_unit'] = dow
680 # For the nth Fooday in Month, start at the 1st of the
681 # month and count ahead N Foodays. For the last Fooday in
682 # Month, start at the last of the month and count back one
689 tmp_date = datetime.date(year=year, month=month, day=1)
690 tmp_date = tmp_date - datetime.timedelta(days=1)
692 self.context['year'] = tmp_date.year
693 self.context['month'] = tmp_date.month
694 self.context['day'] = tmp_date.day
696 # The delta adjustment code can handle the case where
697 # the last day of the month is the day we're looking
700 self.context['year'] = year
701 self.context['month'] = month
702 self.context['day'] = 1
703 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
705 raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}')
707 def exitFirstLastWeekdayInMonthMaybeYearExpr(
709 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
711 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
713 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
715 i = self._get_int(ctx.getText())
717 raise ParseException(f'Bad nth expression: {ctx.getText()}')
719 self.context['nth'] = i
721 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
729 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
731 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
733 self.context['nth'] = txt
735 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
737 dow = ctx.getText().lower()[:3]
738 dow = self.day_name_to_number.get(dow, None)
740 raise ParseException('Bad day of week')
742 self.context['dow'] = dow
744 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
746 day = ctx.getText().lower()
748 self.context['day'] = 'ide'
751 self.context['day'] = 'non'
754 self.context['day'] = 1
756 day = self._get_int(day)
757 if day < 1 or day > 31:
758 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
760 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
761 self.context['day'] = day
763 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
765 month = ctx.getText()
766 while month[0] == '/' or month[0] == '-':
768 month = month[:3].lower()
769 month = self.month_name_to_number.get(month, None)
771 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
773 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
775 self.context['month'] = month
777 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
779 month = self._get_int(ctx.getText())
780 if month < 1 or month > 12:
781 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
783 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
785 self.context['month'] = month
787 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
789 year = self._get_int(ctx.getText())
791 raise ParseException(f'Bad year expression: {ctx.getText()}')
793 raise ParseException(f'Bad year expression: {ctx.getText()}')
795 self.saw_overt_year = True
796 self.context['year'] = year
798 def exitSpecialDateMaybeYearExpr(
799 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
802 special = ctx.specialDate().getText().lower()
803 self.context['special'] = special
805 raise ParseException(
806 f'Bad specialDate expression: {ctx.specialDate().getText()}'
809 mod = ctx.thisNextLast()
811 if mod.THIS() is not None:
812 self.context['special_next_last'] = 'this'
813 elif mod.NEXT() is not None:
814 self.context['special_next_last'] = 'next'
815 elif mod.LAST() is not None:
816 self.context['special_next_last'] = 'last'
818 raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}')
820 def exitNFoosFromTodayAgoExpr(
821 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
823 d = self.now_datetime
825 count = self._get_int(ctx.unsignedInt().getText())
826 unit = ctx.deltaUnit().getText().lower()
827 ago_from_now = ctx.AGO_FROM_NOW().getText()
829 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}')
831 if "ago" in ago_from_now or "back" in ago_from_now:
834 unit = self._figure_out_date_unit(unit)
835 d = n_timeunits_from_base(count, TimeUnit(unit), d)
836 self.context['year'] = d.year
837 self.context['month'] = d.month
838 self.context['day'] = d.day
840 def exitDeltaRelativeToTodayExpr(
841 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
843 # When someone says "next week" they mean a week from now.
844 # Likewise next month or last year. These expressions are now
847 # But when someone says "this Friday" they mean "this coming
848 # Friday". It would be weird to say "this Friday" if today
849 # was already Friday but I'm parsing it to mean: the next day
850 # that is a Friday. So when you say "next Friday" you mean
851 # the Friday after this coming Friday, or 2 Fridays from now.
853 # This set handles this weirdness.
865 d = self.now_datetime
867 mod = ctx.thisNextLast()
868 unit = ctx.deltaUnit().getText().lower()
869 unit = self._figure_out_date_unit(unit)
883 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
885 raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}')
886 d = n_timeunits_from_base(count, TimeUnit(unit), d)
887 self.context['year'] = d.year
888 self.context['month'] = d.month
889 self.context['day'] = d.day
891 def exitSpecialTimeExpr(
892 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
895 txt = ctx.specialTime().getText().lower()
897 raise ParseException(f'Bad special time expression: {ctx.getText()}')
899 if txt == 'noon' or txt == 'midday':
900 self.context['hour'] = 12
901 self.context['minute'] = 0
902 self.context['seconds'] = 0
903 self.context['micros'] = 0
904 elif txt == 'midnight':
905 self.context['hour'] = 0
906 self.context['minute'] = 0
907 self.context['seconds'] = 0
908 self.context['micros'] = 0
910 raise ParseException(f'Bad special time expression: {txt}')
913 tz = ctx.tzExpr().getText()
914 self.context['tz'] = self._parse_tz(tz)
918 def exitTwelveHourTimeExpr(
919 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
922 hour = ctx.hour().getText()
923 while not hour[-1].isdigit():
925 hour = self._get_int(hour)
927 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
928 if hour <= 0 or hour > 12:
929 raise ParseException(f'Bad hour (out of range): {hour}')
932 minute = self._get_int(ctx.minute().getText())
935 if minute < 0 or minute > 59:
936 raise ParseException(f'Bad minute (out of range): {minute}')
937 self.context['minute'] = minute
940 seconds = self._get_int(ctx.second().getText())
943 if seconds < 0 or seconds > 59:
944 raise ParseException(f'Bad second (out of range): {seconds}')
945 self.context['seconds'] = seconds
948 micros = self._get_int(ctx.micros().getText())
951 if micros < 0 or micros > 1000000:
952 raise ParseException(f'Bad micros (out of range): {micros}')
953 self.context['micros'] = micros
956 ampm = ctx.ampm().getText()
958 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
963 self.context['hour'] = hour
966 tz = ctx.tzExpr().getText()
967 self.context['tz'] = self._parse_tz(tz)
971 def exitTwentyFourHourTimeExpr(
972 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
975 hour = ctx.hour().getText()
976 while not hour[-1].isdigit():
978 hour = self._get_int(hour)
980 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
981 if hour < 0 or hour > 23:
982 raise ParseException(f'Bad hour (out of range): {hour}')
983 self.context['hour'] = hour
986 minute = self._get_int(ctx.minute().getText())
989 if minute < 0 or minute > 59:
990 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
991 self.context['minute'] = minute
994 seconds = self._get_int(ctx.second().getText())
997 if seconds < 0 or seconds > 59:
998 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
999 self.context['seconds'] = seconds
1002 micros = self._get_int(ctx.micros().getText())
1005 if micros < 0 or micros >= 1000000:
1006 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1007 self.context['micros'] = micros
1010 tz = ctx.tzExpr().getText()
1011 self.context['tz'] = self._parse_tz(tz)
1016 @bootstrap.initialize
1018 parser = DateParser()
1019 for line in sys.stdin:
1021 line = re.sub(r"#.*$", "", line)
1022 if re.match(r"^ *$", line) is not None:
1025 dt = parser.parse(line)
1026 except Exception as e:
1028 print("Unrecognized.")
1030 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1034 if __name__ == "__main__":