3 # pylint: disable=W0201
4 # pylint: disable=R0904
6 # © Copyright 2021-2022, Scott Gasch
8 """Parse dates in a variety of formats."""
15 from typing import Any, Callable, Dict, Optional
17 import antlr4 # type: ignore
18 import dateutil.easter
20 import holidays # type: ignore
23 from pyutils import bootstrap, decorator_utils
24 from pyutils.datetimez.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
25 from pyutils.datetimez.dateparse_utilsListener import (
26 dateparse_utilsListener,
28 from pyutils.datetimez.dateparse_utilsParser import (
29 dateparse_utilsParser,
31 from pyutils.datetimez.datetime_utils import (
35 n_timeunits_from_base,
37 from pyutils.security import acl
39 logger = logging.getLogger(__name__)
42 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
43 @functools.wraps(enter_or_exit_f)
44 def debug_parse_wrapper(*args, **kwargs):
50 + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
52 for c in ctx.getChildren():
53 logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
54 retval = enter_or_exit_f(*args, **kwargs)
57 return debug_parse_wrapper
60 class ParseException(Exception):
61 """An exception thrown during parsing because of unrecognized input."""
63 def __init__(self, message: str) -> None:
65 self.message = message
68 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
69 """An error listener that raises ParseExceptions."""
71 def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
72 raise ParseException(msg)
75 self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs
79 def reportAttemptingFullContext(
80 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
84 def reportContextSensitivity(
85 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
90 @decorator_utils.decorate_matching_methods_with(
92 acl=acl.StringWildcardBasedACL(
97 denied_patterns=['enterEveryRule', 'exitEveryRule'],
98 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
102 class DateParser(dateparse_utilsListener):
103 """A class to parse dates expressed in human language. Example usage::
106 d.parse('next wednesday')
107 dt = d.get_datetime()
109 Note that the interface is somewhat klunky here because this class is
110 conforming to interfaces auto-generated by ANTLR as it parses the grammar.
111 See also: string_utils.parse_date.
114 PARSE_TYPE_SINGLE_DATE_EXPR = 1
115 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
116 PARSE_TYPE_SINGLE_TIME_EXPR = 3
117 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
119 def __init__(self, *, override_now_for_test_purposes=None) -> None:
120 """C'tor. Passing a value to override_now_for_test_purposes can be
121 used to force this instance to use a custom date/time for its
122 idea of "now" so that the code can be more easily unittested.
123 Leave as None for real use cases.
125 self.month_name_to_number = {
140 # Used only for ides/nones. Month length on a non-leap year.
141 self.typical_days_per_month = {
156 # N.B. day number is also synched with datetime_utils.TimeUnit values
157 # which allows expressions like "3 wednesdays from now" to work.
158 self.day_name_to_number = {
168 # These TimeUnits are defined in datetime_utils and are used as params
169 # to datetime_utils.n_timeunits_from_base.
170 self.time_delta_unit_to_constant = {
171 'hou': TimeUnit.HOURS,
172 'min': TimeUnit.MINUTES,
173 'sec': TimeUnit.SECONDS,
175 self.delta_unit_to_constant = {
176 'day': TimeUnit.DAYS,
177 'wor': TimeUnit.WORKDAYS,
178 'wee': TimeUnit.WEEKS,
179 'mon': TimeUnit.MONTHS,
180 'yea': TimeUnit.YEARS,
182 self.override_now_for_test_purposes = override_now_for_test_purposes
184 # Note: _reset defines several class fields. It is used both here
185 # in the c'tor but also in between parse operations to restore the
186 # class' state and allow it to be reused.
190 def parse(self, date_string: str) -> Optional[datetime.datetime]:
191 """Parse a date/time expression and return a timezone agnostic
192 datetime on success. Also sets self.datetime, self.date and
193 self.time which can each be accessed other methods on the
194 class: get_datetime(), get_date() and get_time(). Raises a
195 ParseException with a helpful(?) message on parse error or
198 To get an idea of what expressions can be parsed, check out
199 the unittest and the grammar.
203 txt = '3 weeks before last tues at 9:15am'
206 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
208 # dt1 and dt2 will be identical other than the fact that
209 # the latter's tzinfo will be set to PST/PDT.
211 This is the main entrypoint to this class for caller code.
213 date_string = date_string.strip()
214 date_string = re.sub(r'\s+', ' ', date_string)
216 listener = RaisingErrorListener()
217 input_stream = antlr4.InputStream(date_string)
218 lexer = dateparse_utilsLexer(input_stream)
219 lexer.removeErrorListeners()
220 lexer.addErrorListener(listener)
221 stream = antlr4.CommonTokenStream(lexer)
222 parser = dateparse_utilsParser(stream)
223 parser.removeErrorListeners()
224 parser.addErrorListener(listener)
225 tree = parser.parse()
226 walker = antlr4.ParseTreeWalker()
227 walker.walk(self, tree)
230 def get_date(self) -> Optional[datetime.date]:
231 """Return the date part or None."""
234 def get_time(self) -> Optional[datetime.time]:
235 """Return the time part or None."""
238 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
239 """Return as a datetime. Parsed date expressions without any time
240 part return hours = minutes = seconds = microseconds = 0 (i.e. at
241 midnight that day). Parsed time expressions without any date part
242 default to date = today.
244 The optional tz param allows the caller to request the datetime be
245 timezone aware and sets the tzinfo to the indicated zone. Defaults
246 to timezone naive (i.e. tzinfo = None).
251 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
257 """Reset at init and between parses."""
258 if self.override_now_for_test_purposes is None:
259 self.now_datetime = datetime.datetime.now()
260 self.today = datetime.date.today()
262 self.now_datetime = self.override_now_for_test_purposes
263 self.today = datetime_to_date(self.override_now_for_test_purposes)
264 self.date: Optional[datetime.date] = None
265 self.time: Optional[datetime.time] = None
266 self.datetime: Optional[datetime.datetime] = None
267 self.context: Dict[str, Any] = {}
268 self.timedelta = datetime.timedelta(seconds=0)
269 self.saw_overt_year = False
272 def _normalize_special_day_name(name: str) -> str:
273 """String normalization / canonicalization for date expressions."""
275 name = name.replace("'", '')
276 name = name.replace('xmas', 'christmas')
277 name = name.replace('mlk', 'martin luther king')
278 name = name.replace(' ', '')
279 eve = 'eve' if name[-3:] == 'eve' else ''
280 name = name[:5] + eve
281 name = name.replace('washi', 'presi')
284 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
285 """Figure out what unit a date expression piece is talking about."""
287 return TimeUnit.MONTHS
288 txt = orig.lower()[:3]
289 if txt in self.day_name_to_number:
290 return TimeUnit(self.day_name_to_number[txt])
291 elif txt in self.delta_unit_to_constant:
292 return TimeUnit(self.delta_unit_to_constant[txt])
293 raise ParseException(f'Invalid date unit: {orig}')
295 def _figure_out_time_unit(self, orig: str) -> int:
296 """Figure out what unit a time expression piece is talking about."""
297 txt = orig.lower()[:3]
298 if txt in self.time_delta_unit_to_constant:
299 return self.time_delta_unit_to_constant[txt]
300 raise ParseException(f'Invalid time unit: {orig}')
302 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
303 """Parse what we think is a special date name and return its datetime
304 (or None if it can't be parsed).
307 year = self.context.get('year', today.year)
308 name = DateParser._normalize_special_day_name(self.context['special'])
310 # Yesterday, today, tomorrow -- ignore any next/last
311 if name in ('today', 'now'):
314 return today + datetime.timedelta(days=-1)
316 return today + datetime.timedelta(days=+1)
318 next_last = self.context.get('special_next_last', '')
319 if next_last == 'next':
321 self.saw_overt_year = True
322 elif next_last == 'last':
324 self.saw_overt_year = True
328 return dateutil.easter.easter(year=year)
329 elif name == 'hallo':
330 return datetime.date(year=year, month=10, day=31)
332 for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
333 if 'Observed' not in holiday_name:
334 holiday_name = DateParser._normalize_special_day_name(holiday_name)
335 if name == holiday_name:
337 if name == 'chriseve':
338 return datetime.date(year=year, month=12, day=24)
339 elif name == 'newyeeve':
340 return datetime.date(year=year, month=12, day=31)
343 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
344 """Handle date expressions like "the ides of March" which require
345 both the "ides" and the month since the definition of the "ides"
346 changes based on the length of the month.
348 assert 'ide' in day or 'non' in day
349 assert month_number in self.typical_days_per_month
350 typical_days_per_month = self.typical_days_per_month[month_number]
353 if typical_days_per_month == 31:
354 if self.context['day'] == 'ide':
361 if self.context['day'] == 'ide':
366 def _parse_normal_date(self) -> datetime.date:
367 if 'dow' in self.context and 'month' not in self.context:
369 while d.weekday() != self.context['dow']:
370 d += datetime.timedelta(days=1)
373 if 'month' not in self.context:
374 raise ParseException('Missing month')
375 if 'day' not in self.context:
376 raise ParseException('Missing day')
377 if 'year' not in self.context:
378 self.context['year'] = self.today.year
379 self.saw_overt_year = False
381 self.saw_overt_year = True
383 # Handling "ides" and "nones" requires both the day and month.
384 if self.context['day'] == 'ide' or self.context['day'] == 'non':
385 self.context['day'] = self._resolve_ides_nones(
386 self.context['day'], self.context['month']
389 return datetime.date(
390 year=self.context['year'],
391 month=self.context['month'],
392 day=self.context['day'],
396 def _parse_tz(txt: str) -> Any:
402 tz1 = pytz.timezone(txt)
410 tz2 = dateutil.tz.gettz(txt)
416 # Try constructing an offset in seconds
419 if txt_sign in ('-', '+'):
420 sign = +1 if txt_sign == '+' else -1
422 minute = int(txt[-2:])
423 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
424 tzoffset = dateutil.tz.tzoffset(txt, offset)
431 def _get_int(txt: str) -> int:
432 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
434 while not txt[-1].isdigit():
438 # -- overridden methods invoked by parse walk. Note: not part of the class'
441 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
444 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
447 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
448 """Populate self.datetime."""
449 if self.date is None:
450 self.date = self.today
451 year = self.date.year
452 month = self.date.month
455 if self.time is None:
456 self.time = datetime.time(0, 0, 0)
457 hour = self.time.hour
458 minute = self.time.minute
459 second = self.time.second
460 micros = self.time.microsecond
462 self.datetime = datetime.datetime(
470 tzinfo=self.time.tzinfo,
473 # Apply resudual adjustments to times here when we have a
475 self.datetime = self.datetime + self.timedelta
476 assert self.datetime is not None
477 self.time = datetime.time(
479 self.datetime.minute,
480 self.datetime.second,
481 self.datetime.microsecond,
482 self.datetime.tzinfo,
485 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
487 if ctx.singleDateExpr() is not None:
488 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
489 elif ctx.baseAndOffsetDateExpr() is not None:
490 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
492 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
494 if ctx.singleTimeExpr() is not None:
495 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
496 elif ctx.baseAndOffsetTimeExpr() is not None:
497 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
499 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
500 """When we leave the date expression, populate self.date."""
501 if 'special' in self.context:
502 self.date = self._parse_special_date(self.context['special'])
504 self.date = self._parse_normal_date()
505 assert self.date is not None
507 # For a single date, just return the date we pulled out.
508 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
511 # Otherwise treat self.date as a base date that we're modifying
513 if 'delta_int' not in self.context:
514 raise ParseException('Missing delta_int?!')
515 count = self.context['delta_int']
519 # Adjust count's sign based on the presence of 'before' or 'after'.
520 if 'delta_before_after' in self.context:
521 before_after = self.context['delta_before_after'].lower()
522 if before_after in ('before', 'until', 'til', 'to'):
525 # What are we counting units of?
526 if 'delta_unit' not in self.context:
527 raise ParseException('Missing delta_unit?!')
528 unit = self.context['delta_unit']
529 dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
530 self.date = datetime_to_date(dt)
532 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
534 self.time = datetime.time(
535 self.context['hour'],
536 self.context['minute'],
537 self.context['seconds'],
538 self.context['micros'],
539 tzinfo=self.context.get('tz', None),
541 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
544 # If we get here there (should be) a relative adjustment to
546 if 'nth' in self.context:
547 count = self.context['nth']
548 elif 'time_delta_int' in self.context:
549 count = self.context['time_delta_int']
551 raise ParseException('Missing delta in relative time.')
555 # Adjust count's sign based on the presence of 'before' or 'after'.
556 if 'time_delta_before_after' in self.context:
557 before_after = self.context['time_delta_before_after'].lower()
558 if before_after in ('before', 'until', 'til', 'to'):
561 # What are we counting units of... assume minutes.
562 if 'time_delta_unit' not in self.context:
563 self.timedelta += datetime.timedelta(minutes=count)
565 unit = self.context['time_delta_unit']
566 if unit == TimeUnit.SECONDS:
567 self.timedelta += datetime.timedelta(seconds=count)
568 elif unit == TimeUnit.MINUTES:
569 self.timedelta = datetime.timedelta(minutes=count)
570 elif unit == TimeUnit.HOURS:
571 self.timedelta = datetime.timedelta(hours=count)
573 raise ParseException(f'Invalid Unit: "{unit}"')
575 def exitDeltaPlusMinusExpr(
576 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
581 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
583 n = DateParser._get_int(n)
584 unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
585 except Exception as e:
586 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
588 self.context['delta_int'] = n
589 self.context['delta_unit'] = unit
591 def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
593 unit = self._figure_out_date_unit(ctx.getText().lower())
594 except Exception as e:
595 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
597 self.context['delta_unit'] = unit
599 def exitDeltaNextLast(
600 self, ctx: dateparse_utilsParser.DeltaNextLastContext
603 txt = ctx.getText().lower()
604 except Exception as e:
605 raise ParseException(f'Bad next/last: {ctx.getText()}') from e
606 if 'month' in self.context or 'day' in self.context or 'year' in self.context:
607 raise ParseException(
608 'Next/last expression expected to be relative to today.'
610 if txt[:4] == 'next':
611 self.context['delta_int'] = +1
612 self.context['day'] = self.now_datetime.day
613 self.context['month'] = self.now_datetime.month
614 self.context['year'] = self.now_datetime.year
615 self.saw_overt_year = True
616 elif txt[:4] == 'last':
617 self.context['delta_int'] = -1
618 self.context['day'] = self.now_datetime.day
619 self.context['month'] = self.now_datetime.month
620 self.context['year'] = self.now_datetime.year
621 self.saw_overt_year = True
623 raise ParseException(f'Bad next/last: {ctx.getText()}')
625 def exitCountUnitsBeforeAfterTimeExpr(
626 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
628 if 'nth' not in self.context:
629 raise ParseException(f'Bad count expression: {ctx.getText()}')
631 unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
632 self.context['time_delta_unit'] = unit
633 except Exception as e:
634 raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
635 if 'time_delta_before_after' not in self.context:
636 raise ParseException(f'Bad Before/After: {ctx.getText()}')
638 def exitDeltaTimeFraction(
639 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
642 txt = ctx.getText().lower()[:4]
644 self.context['time_delta_int'] = 15
645 self.context['time_delta_unit'] = TimeUnit.MINUTES
647 self.context['time_delta_int'] = 30
648 self.context['time_delta_unit'] = TimeUnit.MINUTES
650 raise ParseException(f'Bad time fraction {ctx.getText()}')
651 except Exception as e:
652 raise ParseException(f'Bad time fraction {ctx.getText()}') from e
654 def exitDeltaBeforeAfter(
655 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
658 txt = ctx.getText().lower()
659 except Exception as e:
660 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
662 self.context['delta_before_after'] = txt
664 def exitDeltaTimeBeforeAfter(
665 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
668 txt = ctx.getText().lower()
669 except Exception as e:
670 raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
672 self.context['time_delta_before_after'] = txt
674 def exitNthWeekdayInMonthMaybeYearExpr(
675 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
677 """Do a bunch of work to convert expressions like...
679 'the 2nd Friday of June' -and-
680 'the last Wednesday in October'
682 ...into base + offset expressions instead.
685 if 'nth' not in self.context:
686 raise ParseException(f'Missing nth number: {ctx.getText()}')
687 n = self.context['nth']
688 if n < 1 or n > 5: # months never have more than 5 Foodays
690 raise ParseException(f'Invalid nth number: {ctx.getText()}')
691 del self.context['nth']
692 self.context['delta_int'] = n
694 year = self.context.get('year', self.today.year)
695 if 'month' not in self.context:
696 raise ParseException(f'Missing month expression: {ctx.getText()}')
697 month = self.context['month']
699 dow = self.context['dow']
700 del self.context['dow']
701 self.context['delta_unit'] = dow
703 # For the nth Fooday in Month, start at the last day of
704 # the previous month count ahead N Foodays. For the last
705 # Fooday in Month, start at the last of the month and
706 # count back one Fooday.
712 tmp_date = datetime.date(year=year, month=month, day=1)
713 tmp_date = tmp_date - datetime.timedelta(days=1)
715 # The delta adjustment code can handle the case where
716 # the last day of the month is the day we're looking
719 tmp_date = datetime.date(year=year, month=month, day=1)
720 tmp_date = tmp_date - datetime.timedelta(days=1)
722 self.context['year'] = tmp_date.year
723 self.context['month'] = tmp_date.month
724 self.context['day'] = tmp_date.day
725 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
726 except Exception as e:
727 raise ParseException(
728 f'Invalid nthWeekday expression: {ctx.getText()}'
731 def exitFirstLastWeekdayInMonthMaybeYearExpr(
733 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
735 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
737 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
739 i = DateParser._get_int(ctx.getText())
740 except Exception as e:
741 raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
743 self.context['nth'] = i
745 def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
753 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
754 except Exception as e:
755 raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
757 self.context['nth'] = txt
759 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
761 dow = ctx.getText().lower()[:3]
762 dow = self.day_name_to_number.get(dow, None)
763 except Exception as e:
764 raise ParseException('Bad day of week') from e
766 self.context['dow'] = dow
768 def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
770 day = ctx.getText().lower()
772 self.context['day'] = 'ide'
775 self.context['day'] = 'non'
778 self.context['day'] = 1
780 day = DateParser._get_int(day)
781 if day < 1 or day > 31:
782 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
783 except Exception as e:
784 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
785 self.context['day'] = day
787 def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
789 month = ctx.getText()
790 while month[0] == '/' or month[0] == '-':
792 month = month[:3].lower()
793 month = self.month_name_to_number.get(month, None)
795 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
796 except Exception as e:
797 raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
799 self.context['month'] = month
801 def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
803 month = DateParser._get_int(ctx.getText())
804 if month < 1 or month > 12:
805 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
806 except Exception as e:
807 raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
809 self.context['month'] = month
811 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
813 year = DateParser._get_int(ctx.getText())
815 raise ParseException(f'Bad year expression: {ctx.getText()}')
816 except Exception as e:
817 raise ParseException(f'Bad year expression: {ctx.getText()}') from e
819 self.saw_overt_year = True
820 self.context['year'] = year
822 def exitSpecialDateMaybeYearExpr(
823 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
826 special = ctx.specialDate().getText().lower()
827 self.context['special'] = special
828 except Exception as e:
829 raise ParseException(
830 f'Bad specialDate expression: {ctx.specialDate().getText()}'
833 mod = ctx.thisNextLast()
835 if mod.THIS() is not None:
836 self.context['special_next_last'] = 'this'
837 elif mod.NEXT() is not None:
838 self.context['special_next_last'] = 'next'
839 elif mod.LAST() is not None:
840 self.context['special_next_last'] = 'last'
841 except Exception as e:
842 raise ParseException(
843 f'Bad specialDateNextLast expression: {ctx.getText()}'
846 def exitNFoosFromTodayAgoExpr(
847 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
849 d = self.now_datetime
851 count = DateParser._get_int(ctx.unsignedInt().getText())
852 unit = ctx.deltaUnit().getText().lower()
853 ago_from_now = ctx.AGO_FROM_NOW().getText()
854 except Exception as e:
855 raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
857 if "ago" in ago_from_now or "back" in ago_from_now:
860 unit = self._figure_out_date_unit(unit)
861 d = n_timeunits_from_base(count, TimeUnit(unit), d)
862 self.context['year'] = d.year
863 self.context['month'] = d.month
864 self.context['day'] = d.day
866 def exitDeltaRelativeToTodayExpr(
867 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
869 # When someone says "next week" they mean a week from now.
870 # Likewise next month or last year. These expressions are now
873 # But when someone says "this Friday" they mean "this coming
874 # Friday". It would be weird to say "this Friday" if today
875 # was already Friday but I'm parsing it to mean: the next day
876 # that is a Friday. So when you say "next Friday" you mean
877 # the Friday after this coming Friday, or 2 Fridays from now.
879 # This set handles this weirdness.
891 d = self.now_datetime
893 mod = ctx.thisNextLast()
894 unit = ctx.deltaUnit().getText().lower()
895 unit = self._figure_out_date_unit(unit)
909 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
910 except Exception as e:
911 raise ParseException(
912 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
914 d = n_timeunits_from_base(count, TimeUnit(unit), d)
915 self.context['year'] = d.year
916 self.context['month'] = d.month
917 self.context['day'] = d.day
919 def exitSpecialTimeExpr(
920 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
923 txt = ctx.specialTime().getText().lower()
924 except Exception as e:
925 raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
927 if txt in ('noon', 'midday'):
928 self.context['hour'] = 12
929 self.context['minute'] = 0
930 self.context['seconds'] = 0
931 self.context['micros'] = 0
932 elif txt == 'midnight':
933 self.context['hour'] = 0
934 self.context['minute'] = 0
935 self.context['seconds'] = 0
936 self.context['micros'] = 0
938 raise ParseException(f'Bad special time expression: {txt}')
941 tz = ctx.tzExpr().getText()
942 self.context['tz'] = DateParser._parse_tz(tz)
946 def exitTwelveHourTimeExpr(
947 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
950 hour = ctx.hour().getText()
951 while not hour[-1].isdigit():
953 hour = DateParser._get_int(hour)
954 except Exception as e:
955 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
956 if hour <= 0 or hour > 12:
957 raise ParseException(f'Bad hour (out of range): {hour}')
960 minute = DateParser._get_int(ctx.minute().getText())
963 if minute < 0 or minute > 59:
964 raise ParseException(f'Bad minute (out of range): {minute}')
965 self.context['minute'] = minute
968 seconds = DateParser._get_int(ctx.second().getText())
971 if seconds < 0 or seconds > 59:
972 raise ParseException(f'Bad second (out of range): {seconds}')
973 self.context['seconds'] = seconds
976 micros = DateParser._get_int(ctx.micros().getText())
979 if micros < 0 or micros > 1000000:
980 raise ParseException(f'Bad micros (out of range): {micros}')
981 self.context['micros'] = micros
984 ampm = ctx.ampm().getText()
985 except Exception as e:
986 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
991 self.context['hour'] = hour
994 tz = ctx.tzExpr().getText()
995 self.context['tz'] = DateParser._parse_tz(tz)
999 def exitTwentyFourHourTimeExpr(
1000 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1003 hour = ctx.hour().getText()
1004 while not hour[-1].isdigit():
1006 hour = DateParser._get_int(hour)
1007 except Exception as e:
1008 raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
1009 if hour < 0 or hour > 23:
1010 raise ParseException(f'Bad hour (out of range): {hour}')
1011 self.context['hour'] = hour
1014 minute = DateParser._get_int(ctx.minute().getText())
1017 if minute < 0 or minute > 59:
1018 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1019 self.context['minute'] = minute
1022 seconds = DateParser._get_int(ctx.second().getText())
1025 if seconds < 0 or seconds > 59:
1026 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1027 self.context['seconds'] = seconds
1030 micros = DateParser._get_int(ctx.micros().getText())
1033 if micros < 0 or micros >= 1000000:
1034 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1035 self.context['micros'] = micros
1038 tz = ctx.tzExpr().getText()
1039 self.context['tz'] = DateParser._parse_tz(tz)
1044 @bootstrap.initialize
1046 parser = DateParser()
1047 for line in sys.stdin:
1049 line = re.sub(r"#.*$", "", line)
1050 if re.match(r"^ *$", line) is not None:
1053 dt = parser.parse(line)
1054 except Exception as e:
1056 print("Unrecognized.")
1058 assert dt is not None
1059 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1063 if __name__ == "__main__":