5 import holidays # type: ignore
9 from typing import Any, Callable, Dict, Optional
11 import antlr4 # type: ignore
12 import dateutil.easter
18 from datetime_utils import (
19 TimeUnit, n_timeunits_from_base, datetime_to_date, date_to_datetime
21 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
22 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
23 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
24 import decorator_utils
27 logger = logging.getLogger(__name__)
30 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
31 @functools.wraps(enter_or_exit_f)
32 def debug_parse_wrapper(*args, **kwargs):
38 f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
40 for c in ctx.getChildren():
45 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."""
52 def __init__(self, message: str) -> None:
53 self.message = message
56 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
57 """An error listener that raises ParseExceptions."""
59 self, recognizer, offendingSymbol, line, column, msg, e
61 raise ParseException(msg)
64 self, recognizer, dfa, startIndex, stopIndex, exact,
69 def reportAttemptingFullContext(
70 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts,
75 def reportContextSensitivity(
76 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
81 @decorator_utils.decorate_matching_methods_with(
83 acl=acl.StringWildcardBasedACL(
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
105 override_now_for_test_purposes = None
107 """C'tor. Passing a value to override_now_for_test_purposes can be
108 used to force this instance to use a custom date/time for its
109 idea of "now" so that the code can be more easily unittested.
110 Leave as None for real use cases.
112 self.month_name_to_number = {
127 # Used only for ides/nones. Month length on a non-leap year.
128 self.typical_days_per_month = {
143 # N.B. day number is also synched with datetime_utils.TimeUnit values
144 # which allows expressions like "3 wednesdays from now" to work.
145 self.day_name_to_number = {
155 # These TimeUnits are defined in datetime_utils and are used as params
156 # to datetime_utils.n_timeunits_from_base.
157 self.time_delta_unit_to_constant = {
158 'hou': TimeUnit.HOURS,
159 'min': TimeUnit.MINUTES,
160 'sec': TimeUnit.SECONDS,
162 self.delta_unit_to_constant = {
163 'day': TimeUnit.DAYS,
164 'wor': TimeUnit.WORKDAYS,
165 'wee': TimeUnit.WEEKS,
166 'mon': TimeUnit.MONTHS,
167 'yea': TimeUnit.YEARS,
169 self.override_now_for_test_purposes = override_now_for_test_purposes
172 def parse(self, date_string: str) -> Optional[datetime.datetime]:
173 """Parse a date/time expression and return a timezone agnostic
174 datetime on success. Also sets self.datetime, self.date and
175 self.time which can each be accessed other methods on the
176 class: get_datetime(), get_date() and get_time(). Raises a
177 ParseException with a helpful(?) message on parse error or
180 To get an idea of what expressions can be parsed, check out
181 the unittest and the grammar.
185 txt = '3 weeks before last tues at 9:15am'
188 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
190 # dt1 and dt2 will be identical other than the fact that
191 # the latter's tzinfo will be set to PST/PDT.
193 This is the main entrypoint to this class for caller code.
195 date_string = date_string.strip()
196 date_string = re.sub('\s+', ' ', date_string)
198 listener = RaisingErrorListener()
199 input_stream = antlr4.InputStream(date_string)
200 lexer = dateparse_utilsLexer(input_stream)
201 lexer.removeErrorListeners()
202 lexer.addErrorListener(listener)
203 stream = antlr4.CommonTokenStream(lexer)
204 parser = dateparse_utilsParser(stream)
205 parser.removeErrorListeners()
206 parser.addErrorListener(listener)
207 tree = parser.parse()
208 walker = antlr4.ParseTreeWalker()
209 walker.walk(self, tree)
212 def get_date(self) -> Optional[datetime.date]:
213 """Return the date part or None."""
216 def get_time(self) -> Optional[datetime.time]:
217 """Return the time part or None."""
220 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
221 """Return as a datetime. Parsed date expressions without any time
222 part return hours = minutes = seconds = microseconds = 0 (i.e. at
223 midnight that day). Parsed time expressions without any date part
224 default to date = today.
226 The optional tz param allows the caller to request the datetime be
227 timezone aware and sets the tzinfo to the indicated zone. Defaults
228 to timezone naive (i.e. tzinfo = None).
232 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
238 """Reset at init and between parses."""
239 if self.override_now_for_test_purposes is None:
240 self.now_datetime = datetime.datetime.now()
241 self.today = datetime.date.today()
243 self.now_datetime = self.override_now_for_test_purposes
244 self.today = datetime_to_date(
245 self.override_now_for_test_purposes
247 self.date: Optional[datetime.date] = None
248 self.time: Optional[datetime.time] = None
249 self.datetime: Optional[datetime.datetime] = None
250 self.context: Dict[str, Any] = {}
251 self.timedelta = datetime.timedelta(seconds=0)
254 def _normalize_special_day_name(name: str) -> str:
255 """String normalization / canonicalization for date expressions."""
257 name = name.replace("'", '')
258 name = name.replace('xmas', 'christmas')
259 name = name.replace('mlk', 'martin luther king')
260 name = name.replace(' ', '')
261 eve = 'eve' if name[-3:] == 'eve' else ''
262 name = name[:5] + eve
263 name = name.replace('washi', 'presi')
266 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
267 """Figure out what unit a date expression piece is talking about."""
269 return TimeUnit.MONTHS
270 txt = orig.lower()[:3]
271 if txt in self.day_name_to_number:
272 return(TimeUnit(self.day_name_to_number[txt]))
273 elif txt in self.delta_unit_to_constant:
274 return(TimeUnit(self.delta_unit_to_constant[txt]))
275 raise ParseException(f'Invalid date unit: {orig}')
277 def _figure_out_time_unit(self, orig: str) -> int:
278 """Figure out what unit a time expression piece is talking about."""
279 txt = orig.lower()[:3]
280 if txt in self.time_delta_unit_to_constant:
281 return(self.time_delta_unit_to_constant[txt])
282 raise ParseException(f'Invalid time unit: {orig}')
284 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
285 """Parse what we think is a special date name and return its datetime
286 (or None if it can't be parsed).
289 year = self.context.get('year', today.year)
290 name = DateParser._normalize_special_day_name(self.context['special'])
292 # Yesterday, today, tomorrow -- ignore any next/last
293 if name == 'today' or name == 'now':
296 return today + datetime.timedelta(days=-1)
298 return today + datetime.timedelta(days=+1)
300 next_last = self.context.get('special_next_last', '')
301 if next_last == 'next':
303 elif next_last == 'last':
308 return dateutil.easter.easter(year=year)
309 elif name == 'hallo':
310 return datetime.date(year=year, month=10, day=31)
312 for holiday_date, holiday_name in sorted(
313 holidays.US(years=year).items()
315 if 'Observed' not in holiday_name:
316 holiday_name = DateParser._normalize_special_day_name(
319 if name == holiday_name:
321 if name == 'chriseve':
322 return datetime.date(year=year, month=12, day=24)
323 elif name == 'newyeeve':
324 return datetime.date(year=year, month=12, day=31)
327 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
328 """Handle date expressions like "the ides of March" which require
329 both the "ides" and the month since the definition of the "ides"
330 changes based on the length of the month.
332 assert 'ide' in day or 'non' in day
333 assert month_number in self.typical_days_per_month
334 typical_days_per_month = self.typical_days_per_month[month_number]
337 if typical_days_per_month == 31:
338 if self.context['day'] == 'ide':
345 if self.context['day'] == 'ide':
350 def _parse_normal_date(self) -> datetime.date:
351 if 'dow' in self.context:
353 while d.weekday() != self.context['dow']:
354 d += datetime.timedelta(days=1)
357 if 'month' not in self.context:
358 raise ParseException('Missing month')
359 if 'day' not in self.context:
360 raise ParseException('Missing day')
361 if 'year' not in self.context:
362 self.context['year'] = self.today.year
364 # Handling "ides" and "nones" requires both the day and month.
366 self.context['day'] == 'ide' or
367 self.context['day'] == 'non'
369 self.context['day'] = self._resolve_ides_nones(
370 self.context['day'], self.context['month']
373 return datetime.date(
374 year=self.context['year'],
375 month=self.context['month'],
376 day=self.context['day'],
379 def _parse_tz(self, txt: str) -> Any:
385 tz = pytz.timezone(txt)
393 tz = dateutil.tz.gettz(txt)
399 # Try constructing an offset in seconds
402 if sign == '-' or sign == '+':
403 sign = +1 if sign == '+' else -1
405 minute = int(txt[-2:])
406 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
407 tzoffset = dateutil.tz.tzoffset(txt, offset)
413 def _get_int(self, txt: str) -> int:
414 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
416 while not txt[-1].isdigit():
420 # -- overridden methods invoked by parse walk --
422 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
425 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
428 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
429 """Populate self.datetime."""
430 if self.date is None:
431 self.date = self.today
432 year = self.date.year
433 month = self.date.month
436 if self.time is None:
437 self.time = datetime.time(0, 0, 0)
438 hour = self.time.hour
439 minute = self.time.minute
440 second = self.time.second
441 micros = self.time.microsecond
443 self.datetime = datetime.datetime(
444 year, month, day, hour, minute, second, micros,
445 tzinfo=self.time.tzinfo
448 # Apply resudual adjustments to times here when we have a
450 self.datetime = self.datetime + self.timedelta
451 self.time = datetime.time(
453 self.datetime.minute,
454 self.datetime.second,
455 self.datetime.microsecond,
459 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
461 if ctx.singleDateExpr() is not None:
462 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
463 elif ctx.baseAndOffsetDateExpr() is not None:
464 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
466 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
468 if ctx.singleTimeExpr() is not None:
469 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
470 elif ctx.baseAndOffsetTimeExpr() is not None:
471 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
473 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
474 """When we leave the date expression, populate self.date."""
475 if 'special' in self.context:
476 self.date = self._parse_special_date(self.context['special'])
478 self.date = self._parse_normal_date()
479 assert self.date is not None
481 # For a single date, just return the date we pulled out.
482 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
485 # Otherwise treat self.date as a base date that we're modifying
487 if 'delta_int' not in self.context:
488 raise ParseException('Missing delta_int?!')
489 count = self.context['delta_int']
493 # Adjust count's sign based on the presence of 'before' or 'after'.
494 if 'delta_before_after' in self.context:
495 before_after = self.context['delta_before_after'].lower()
497 before_after == 'before' or
498 before_after == 'until' or
499 before_after == 'til' or
504 # What are we counting units of?
505 if 'delta_unit' not in self.context:
506 raise ParseException('Missing delta_unit?!')
507 unit = self.context['delta_unit']
508 dt = n_timeunits_from_base(
511 date_to_datetime(self.date)
513 self.date = datetime_to_date(dt)
515 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
517 self.time = datetime.time(
518 self.context['hour'],
519 self.context['minute'],
520 self.context['seconds'],
521 self.context['micros'],
522 tzinfo=self.context.get('tz', None),
524 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
527 # If we get here there (should be) a relative adjustment to
529 if 'nth' in self.context:
530 count = self.context['nth']
531 elif 'time_delta_int' in self.context:
532 count = self.context['time_delta_int']
534 raise ParseException('Missing delta in relative time.')
538 # Adjust count's sign based on the presence of 'before' or 'after'.
539 if 'time_delta_before_after' in self.context:
540 before_after = self.context['time_delta_before_after'].lower()
542 before_after == 'before' or
543 before_after == 'until' or
544 before_after == 'til' or
549 # What are we counting units of... assume minutes.
550 if 'time_delta_unit' not in self.context:
551 self.timedelta += datetime.timedelta(minutes=count)
553 unit = self.context['time_delta_unit']
554 if unit == TimeUnit.SECONDS:
555 self.timedelta += datetime.timedelta(seconds=count)
556 elif unit == TimeUnit.MINUTES:
557 self.timedelta = datetime.timedelta(minutes=count)
558 elif unit == TimeUnit.HOURS:
559 self.timedelta = datetime.timedelta(hours=count)
561 raise ParseException()
563 def exitDeltaPlusMinusExpr(
564 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
569 raise ParseException(
570 f'Bad N in Delta +/- Expr: {ctx.getText()}'
574 unit = self._figure_out_date_unit(
575 ctx.deltaUnit().getText().lower()
578 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
580 self.context['delta_int'] = n
581 self.context['delta_unit'] = unit
583 def exitNextLastUnit(
584 self, ctx: dateparse_utilsParser.DeltaUnitContext
587 unit = self._figure_out_date_unit(ctx.getText().lower())
589 raise ParseException(f'Bad delta unit: {ctx.getText()}')
591 self.context['delta_unit'] = unit
593 def exitDeltaNextLast(
594 self, ctx: dateparse_utilsParser.DeltaNextLastContext
597 txt = ctx.getText().lower()
599 raise ParseException(f'Bad next/last: {ctx.getText()}')
601 'month' in self.context or
602 'day' in self.context or
603 'year' in self.context
605 raise ParseException(
606 'Next/last expression expected to be relative to today.'
608 if txt[:4] == 'next':
609 self.context['delta_int'] = +1
610 self.context['day'] = self.now_datetime.day
611 self.context['month'] = self.now_datetime.month
612 self.context['year'] = self.now_datetime.year
613 elif txt[:4] == 'last':
614 self.context['delta_int'] = -1
615 self.context['day'] = self.now_datetime.day
616 self.context['month'] = self.now_datetime.month
617 self.context['year'] = self.now_datetime.year
619 raise ParseException(f'Bad next/last: {ctx.getText()}')
621 def exitCountUnitsBeforeAfterTimeExpr(
622 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
624 if 'nth' not in self.context:
625 raise ParseException(
626 f'Bad count expression: {ctx.getText()}'
629 unit = self._figure_out_time_unit(
630 ctx.deltaTimeUnit().getText().lower()
632 self.context['time_delta_unit'] = unit
634 raise ParseException(f'Bad delta unit: {ctx.getText()}')
635 if 'time_delta_before_after' not in self.context:
636 raise ParseException(
637 f'Bad Before/After: {ctx.getText()}'
640 def exitDeltaTimeFraction(
641 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
644 txt = ctx.getText().lower()[:4]
646 self.context['time_delta_int'] = 15
651 self.context['time_delta_int'] = 30
656 raise ParseException(f'Bad time fraction {ctx.getText()}')
658 raise ParseException(f'Bad time fraction {ctx.getText()}')
660 def exitDeltaBeforeAfter(
661 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
664 txt = ctx.getText().lower()
666 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
668 self.context['delta_before_after'] = txt
670 def exitDeltaTimeBeforeAfter(
671 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
674 txt = ctx.getText().lower()
676 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
678 self.context['time_delta_before_after'] = txt
680 def exitNthWeekdayInMonthMaybeYearExpr(
681 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
683 """Do a bunch of work to convert expressions like...
685 'the 2nd Friday of June' -and-
686 'the last Wednesday in October'
688 ...into base + offset expressions instead.
691 if 'nth' not in self.context:
692 raise ParseException(f'Missing nth number: {ctx.getText()}')
693 n = self.context['nth']
694 if n < 1 or n > 5: # months never have more than 5 Foodays
696 raise ParseException(f'Invalid nth number: {ctx.getText()}')
697 del self.context['nth']
698 self.context['delta_int'] = n
700 year = self.context.get('year', self.today.year)
701 if 'month' not in self.context:
702 raise ParseException(
703 f'Missing month expression: {ctx.getText()}'
705 month = self.context['month']
707 dow = self.context['dow']
708 del self.context['dow']
709 self.context['delta_unit'] = dow
711 # For the nth Fooday in Month, start at the 1st of the
712 # month and count ahead N Foodays. For the last Fooday in
713 # Month, start at the last of the month and count back one
720 tmp_date = datetime.date(year=year, month=month, day=1)
721 tmp_date = tmp_date - datetime.timedelta(days=1)
723 self.context['year'] = tmp_date.year
724 self.context['month'] = tmp_date.month
725 self.context['day'] = tmp_date.day
727 # The delta adjustment code can handle the case where
728 # the last day of the month is the day we're looking
731 self.context['year'] = year
732 self.context['month'] = month
733 self.context['day'] = 1
734 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
736 raise ParseException(
737 f'Invalid nthWeekday expression: {ctx.getText()}'
740 def exitFirstLastWeekdayInMonthMaybeYearExpr(
742 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
744 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
746 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
748 i = self._get_int(ctx.getText())
750 raise ParseException(f'Bad nth expression: {ctx.getText()}')
752 self.context['nth'] = i
755 self, ctx: dateparse_utilsParser.FirstOrLastContext
764 raise ParseException(
765 f'Bad first|last expression: {ctx.getText()}'
768 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
770 self.context['nth'] = txt
772 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
774 dow = ctx.getText().lower()[:3]
775 dow = self.day_name_to_number.get(dow, None)
777 raise ParseException('Bad day of week')
779 self.context['dow'] = dow
782 self, ctx: dateparse_utilsParser.DayOfMonthContext
785 day = ctx.getText().lower()
787 self.context['day'] = 'ide'
790 self.context['day'] = 'non'
793 self.context['day'] = 1
795 day = self._get_int(day)
796 if day < 1 or day > 31:
797 raise ParseException(
798 f'Bad dayOfMonth expression: {ctx.getText()}'
801 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
802 self.context['day'] = day
805 self, ctx: dateparse_utilsParser.MonthNameContext
808 month = ctx.getText()
809 while month[0] == '/' or month[0] == '-':
811 month = month[:3].lower()
812 month = self.month_name_to_number.get(month, None)
814 raise ParseException(
815 f'Bad monthName expression: {ctx.getText()}'
818 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
820 self.context['month'] = month
823 self, ctx: dateparse_utilsParser.MonthNumberContext
826 month = self._get_int(ctx.getText())
827 if month < 1 or month > 12:
828 raise ParseException(
829 f'Bad monthNumber expression: {ctx.getText()}'
832 raise ParseException(
833 f'Bad monthNumber expression: {ctx.getText()}'
836 self.context['month'] = month
838 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
840 year = self._get_int(ctx.getText())
842 raise ParseException(f'Bad year expression: {ctx.getText()}')
844 raise ParseException(f'Bad year expression: {ctx.getText()}')
846 self.context['year'] = year
848 def exitSpecialDateMaybeYearExpr(
849 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
852 special = ctx.specialDate().getText().lower()
853 self.context['special'] = special
855 raise ParseException(
856 f'Bad specialDate expression: {ctx.specialDate().getText()}'
859 mod = ctx.thisNextLast()
861 if mod.THIS() is not None:
862 self.context['special_next_last'] = 'this'
863 elif mod.NEXT() is not None:
864 self.context['special_next_last'] = 'next'
865 elif mod.LAST() is not None:
866 self.context['special_next_last'] = 'last'
868 raise ParseException(
869 f'Bad specialDateNextLast expression: {ctx.getText()}'
872 def exitNFoosFromTodayAgoExpr(
873 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
875 d = self.now_datetime
877 count = self._get_int(ctx.unsignedInt().getText())
878 unit = ctx.deltaUnit().getText().lower()
879 ago_from_now = ctx.AGO_FROM_NOW().getText()
881 raise ParseException(
882 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
885 if "ago" in ago_from_now or "back" in ago_from_now:
888 unit = self._figure_out_date_unit(unit)
889 d = n_timeunits_from_base(
893 self.context['year'] = d.year
894 self.context['month'] = d.month
895 self.context['day'] = d.day
897 def exitDeltaRelativeToTodayExpr(
898 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
900 d = self.now_datetime
902 mod = ctx.thisNextLast()
910 raise ParseException(
911 f'Bad This/Next/Last modifier: {mod}'
913 unit = ctx.deltaUnit().getText().lower()
915 raise ParseException(
916 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
918 unit = self._figure_out_date_unit(unit)
919 d = n_timeunits_from_base(
923 self.context['year'] = d.year
924 self.context['month'] = d.month
925 self.context['day'] = d.day
927 def exitSpecialTimeExpr(
928 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
931 txt = ctx.specialTime().getText().lower()
933 raise ParseException(
934 f'Bad special time expression: {ctx.getText()}'
937 if txt == 'noon' or txt == 'midday':
938 self.context['hour'] = 12
939 self.context['minute'] = 0
940 self.context['seconds'] = 0
941 self.context['micros'] = 0
942 elif txt == 'midnight':
943 self.context['hour'] = 0
944 self.context['minute'] = 0
945 self.context['seconds'] = 0
946 self.context['micros'] = 0
948 raise ParseException(f'Bad special time expression: {txt}')
951 tz = ctx.tzExpr().getText()
952 self.context['tz'] = self._parse_tz(tz)
956 def exitTwelveHourTimeExpr(
957 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
960 hour = ctx.hour().getText()
961 while not hour[-1].isdigit():
963 hour = self._get_int(hour)
965 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
966 if hour <= 0 or hour > 12:
967 raise ParseException(f'Bad hour (out of range): {hour}')
970 minute = self._get_int(ctx.minute().getText())
973 if minute < 0 or minute > 59:
974 raise ParseException(f'Bad minute (out of range): {minute}')
975 self.context['minute'] = minute
978 seconds = self._get_int(ctx.second().getText())
981 if seconds < 0 or seconds > 59:
982 raise ParseException(f'Bad second (out of range): {seconds}')
983 self.context['seconds'] = seconds
986 micros = self._get_int(ctx.micros().getText())
989 if micros < 0 or micros > 1000000:
990 raise ParseException(f'Bad micros (out of range): {micros}')
991 self.context['micros'] = micros
994 ampm = ctx.ampm().getText()
996 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
1001 self.context['hour'] = hour
1004 tz = ctx.tzExpr().getText()
1005 self.context['tz'] = self._parse_tz(tz)
1009 def exitTwentyFourHourTimeExpr(
1010 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1013 hour = ctx.hour().getText()
1014 while not hour[-1].isdigit():
1016 hour = self._get_int(hour)
1018 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
1019 if hour < 0 or hour > 23:
1020 raise ParseException(f'Bad hour (out of range): {hour}')
1021 self.context['hour'] = hour
1024 minute = self._get_int(ctx.minute().getText())
1027 if minute < 0 or minute > 59:
1028 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1029 self.context['minute'] = minute
1032 seconds = self._get_int(ctx.second().getText())
1035 if seconds < 0 or seconds > 59:
1036 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1037 self.context['seconds'] = seconds
1040 micros = self._get_int(ctx.micros().getText())
1043 if micros < 0 or micros >= 1000000:
1044 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1045 self.context['micros'] = micros
1048 tz = ctx.tzExpr().getText()
1049 self.context['tz'] = self._parse_tz(tz)
1054 @bootstrap.initialize
1056 parser = DateParser()
1057 for line in sys.stdin:
1059 line = re.sub(r"#.*$", "", line)
1060 if re.match(r"^ *$", line) is not None:
1063 dt = parser.parse(line)
1064 except Exception as e:
1065 print("Unrecognized.")
1067 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1071 if __name__ == "__main__":