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:
54 self.message = message
57 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
58 """An error listener that raises ParseExceptions."""
60 self, recognizer, offendingSymbol, line, column, msg, e
63 raise ParseException(msg)
66 self, recognizer, dfa, startIndex, stopIndex, exact,
71 def reportAttemptingFullContext(
72 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts,
77 def reportContextSensitivity(
78 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
83 @decorator_utils.decorate_matching_methods_with(
85 acl=acl.StringWildcardBasedACL(
94 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
98 class DateParser(dateparse_utilsListener):
99 PARSE_TYPE_SINGLE_DATE_EXPR = 1
100 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
101 PARSE_TYPE_SINGLE_TIME_EXPR = 3
102 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
107 override_now_for_test_purposes = None
109 """C'tor. Passing a value to override_now_for_test_purposes can be
110 used to force this instance to use a custom date/time for its
111 idea of "now" so that the code can be more easily unittested.
112 Leave as None for real use cases.
114 self.month_name_to_number = {
129 # Used only for ides/nones. Month length on a non-leap year.
130 self.typical_days_per_month = {
145 # N.B. day number is also synched with datetime_utils.TimeUnit values
146 # which allows expressions like "3 wednesdays from now" to work.
147 self.day_name_to_number = {
157 # These TimeUnits are defined in datetime_utils and are used as params
158 # to datetime_utils.n_timeunits_from_base.
159 self.time_delta_unit_to_constant = {
160 'hou': TimeUnit.HOURS,
161 'min': TimeUnit.MINUTES,
162 'sec': TimeUnit.SECONDS,
164 self.delta_unit_to_constant = {
165 'day': TimeUnit.DAYS,
166 'wor': TimeUnit.WORKDAYS,
167 'wee': TimeUnit.WEEKS,
168 'mon': TimeUnit.MONTHS,
169 'yea': TimeUnit.YEARS,
171 self.override_now_for_test_purposes = override_now_for_test_purposes
174 def parse(self, date_string: str) -> Optional[datetime.datetime]:
175 """Parse a date/time expression and return a timezone agnostic
176 datetime on success. Also sets self.datetime, self.date and
177 self.time which can each be accessed other methods on the
178 class: get_datetime(), get_date() and get_time(). Raises a
179 ParseException with a helpful(?) message on parse error or
182 To get an idea of what expressions can be parsed, check out
183 the unittest and the grammar.
187 txt = '3 weeks before last tues at 9:15am'
190 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
192 # dt1 and dt2 will be identical other than the fact that
193 # the latter's tzinfo will be set to PST/PDT.
195 This is the main entrypoint to this class for caller code.
197 date_string = date_string.strip()
198 date_string = re.sub('\s+', ' ', date_string)
200 listener = RaisingErrorListener()
201 input_stream = antlr4.InputStream(date_string)
202 lexer = dateparse_utilsLexer(input_stream)
203 lexer.removeErrorListeners()
204 lexer.addErrorListener(listener)
205 stream = antlr4.CommonTokenStream(lexer)
206 parser = dateparse_utilsParser(stream)
207 parser.removeErrorListeners()
208 parser.addErrorListener(listener)
209 tree = parser.parse()
210 walker = antlr4.ParseTreeWalker()
211 walker.walk(self, tree)
214 def get_date(self) -> Optional[datetime.date]:
215 """Return the date part or None."""
218 def get_time(self) -> Optional[datetime.time]:
219 """Return the time part or None."""
222 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
223 """Return as a datetime. Parsed date expressions without any time
224 part return hours = minutes = seconds = microseconds = 0 (i.e. at
225 midnight that day). Parsed time expressions without any date part
226 default to date = today.
228 The optional tz param allows the caller to request the datetime be
229 timezone aware and sets the tzinfo to the indicated zone. Defaults
230 to timezone naive (i.e. tzinfo = None).
234 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
240 """Reset at init and between parses."""
241 if self.override_now_for_test_purposes is None:
242 self.now_datetime = datetime.datetime.now()
243 self.today = datetime.date.today()
245 self.now_datetime = self.override_now_for_test_purposes
246 self.today = datetime_to_date(
247 self.override_now_for_test_purposes
249 self.date: Optional[datetime.date] = None
250 self.time: Optional[datetime.time] = None
251 self.datetime: Optional[datetime.datetime] = None
252 self.context: Dict[str, Any] = {}
253 self.timedelta = datetime.timedelta(seconds=0)
256 def _normalize_special_day_name(name: str) -> str:
257 """String normalization / canonicalization for date expressions."""
259 name = name.replace("'", '')
260 name = name.replace('xmas', 'christmas')
261 name = name.replace('mlk', 'martin luther king')
262 name = name.replace(' ', '')
263 eve = 'eve' if name[-3:] == 'eve' else ''
264 name = name[:5] + eve
265 name = name.replace('washi', 'presi')
268 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
269 """Figure out what unit a date expression piece is talking about."""
271 return TimeUnit.MONTHS
272 txt = orig.lower()[:3]
273 if txt in self.day_name_to_number:
274 return(TimeUnit(self.day_name_to_number[txt]))
275 elif txt in self.delta_unit_to_constant:
276 return(TimeUnit(self.delta_unit_to_constant[txt]))
277 raise ParseException(f'Invalid date unit: {orig}')
279 def _figure_out_time_unit(self, orig: str) -> int:
280 """Figure out what unit a time expression piece is talking about."""
281 txt = orig.lower()[:3]
282 if txt in self.time_delta_unit_to_constant:
283 return(self.time_delta_unit_to_constant[txt])
284 raise ParseException(f'Invalid time unit: {orig}')
286 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
287 """Parse what we think is a special date name and return its datetime
288 (or None if it can't be parsed).
291 year = self.context.get('year', today.year)
292 name = DateParser._normalize_special_day_name(self.context['special'])
294 # Yesterday, today, tomorrow -- ignore any next/last
295 if name == 'today' or name == 'now':
298 return today + datetime.timedelta(days=-1)
300 return today + datetime.timedelta(days=+1)
302 next_last = self.context.get('special_next_last', '')
303 if next_last == 'next':
305 elif next_last == 'last':
310 return dateutil.easter.easter(year=year)
311 elif name == 'hallo':
312 return datetime.date(year=year, month=10, day=31)
314 for holiday_date, holiday_name in sorted(
315 holidays.US(years=year).items()
317 if 'Observed' not in holiday_name:
318 holiday_name = DateParser._normalize_special_day_name(
321 if name == holiday_name:
323 if name == 'chriseve':
324 return datetime.date(year=year, month=12, day=24)
325 elif name == 'newyeeve':
326 return datetime.date(year=year, month=12, day=31)
329 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
330 """Handle date expressions like "the ides of March" which require
331 both the "ides" and the month since the definition of the "ides"
332 changes based on the length of the month.
334 assert 'ide' in day or 'non' in day
335 assert month_number in self.typical_days_per_month
336 typical_days_per_month = self.typical_days_per_month[month_number]
339 if typical_days_per_month == 31:
340 if self.context['day'] == 'ide':
347 if self.context['day'] == 'ide':
352 def _parse_normal_date(self) -> datetime.date:
353 if 'dow' in self.context:
355 while d.weekday() != self.context['dow']:
356 d += datetime.timedelta(days=1)
359 if 'month' not in self.context:
360 raise ParseException('Missing month')
361 if 'day' not in self.context:
362 raise ParseException('Missing day')
363 if 'year' not in self.context:
364 self.context['year'] = self.today.year
366 # Handling "ides" and "nones" requires both the day and month.
368 self.context['day'] == 'ide' or
369 self.context['day'] == 'non'
371 self.context['day'] = self._resolve_ides_nones(
372 self.context['day'], self.context['month']
375 return datetime.date(
376 year=self.context['year'],
377 month=self.context['month'],
378 day=self.context['day'],
381 def _parse_tz(self, txt: str) -> Any:
387 tz = pytz.timezone(txt)
395 tz = dateutil.tz.gettz(txt)
401 # Try constructing an offset in seconds
404 if sign == '-' or sign == '+':
405 sign = +1 if sign == '+' else -1
407 minute = int(txt[-2:])
408 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
409 tzoffset = dateutil.tz.tzoffset(txt, offset)
415 def _get_int(self, txt: str) -> int:
416 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
418 while not txt[-1].isdigit():
422 # -- overridden methods invoked by parse walk --
424 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
427 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
430 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
431 """Populate self.datetime."""
432 if self.date is None:
433 self.date = self.today
434 year = self.date.year
435 month = self.date.month
438 if self.time is None:
439 self.time = datetime.time(0, 0, 0)
440 hour = self.time.hour
441 minute = self.time.minute
442 second = self.time.second
443 micros = self.time.microsecond
445 self.datetime = datetime.datetime(
446 year, month, day, hour, minute, second, micros,
447 tzinfo=self.time.tzinfo
450 # Apply resudual adjustments to times here when we have a
452 self.datetime = self.datetime + self.timedelta
453 self.time = datetime.time(
455 self.datetime.minute,
456 self.datetime.second,
457 self.datetime.microsecond,
461 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
463 if ctx.singleDateExpr() is not None:
464 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
465 elif ctx.baseAndOffsetDateExpr() is not None:
466 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
468 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
470 if ctx.singleTimeExpr() is not None:
471 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
472 elif ctx.baseAndOffsetTimeExpr() is not None:
473 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
475 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
476 """When we leave the date expression, populate self.date."""
477 if 'special' in self.context:
478 self.date = self._parse_special_date(self.context['special'])
480 self.date = self._parse_normal_date()
481 assert self.date is not None
483 # For a single date, just return the date we pulled out.
484 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
487 # Otherwise treat self.date as a base date that we're modifying
489 if 'delta_int' not in self.context:
490 raise ParseException('Missing delta_int?!')
491 count = self.context['delta_int']
495 # Adjust count's sign based on the presence of 'before' or 'after'.
496 if 'delta_before_after' in self.context:
497 before_after = self.context['delta_before_after'].lower()
499 before_after == 'before' or
500 before_after == 'until' or
501 before_after == 'til' or
506 # What are we counting units of?
507 if 'delta_unit' not in self.context:
508 raise ParseException('Missing delta_unit?!')
509 unit = self.context['delta_unit']
510 dt = n_timeunits_from_base(
513 date_to_datetime(self.date)
515 self.date = datetime_to_date(dt)
517 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
519 self.time = datetime.time(
520 self.context['hour'],
521 self.context['minute'],
522 self.context['seconds'],
523 self.context['micros'],
524 tzinfo=self.context.get('tz', None),
526 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
529 # If we get here there (should be) a relative adjustment to
531 if 'nth' in self.context:
532 count = self.context['nth']
533 elif 'time_delta_int' in self.context:
534 count = self.context['time_delta_int']
536 raise ParseException('Missing delta in relative time.')
540 # Adjust count's sign based on the presence of 'before' or 'after'.
541 if 'time_delta_before_after' in self.context:
542 before_after = self.context['time_delta_before_after'].lower()
544 before_after == 'before' or
545 before_after == 'until' or
546 before_after == 'til' or
551 # What are we counting units of... assume minutes.
552 if 'time_delta_unit' not in self.context:
553 self.timedelta += datetime.timedelta(minutes=count)
555 unit = self.context['time_delta_unit']
556 if unit == TimeUnit.SECONDS:
557 self.timedelta += datetime.timedelta(seconds=count)
558 elif unit == TimeUnit.MINUTES:
559 self.timedelta = datetime.timedelta(minutes=count)
560 elif unit == TimeUnit.HOURS:
561 self.timedelta = datetime.timedelta(hours=count)
563 raise ParseException()
565 def exitDeltaPlusMinusExpr(
566 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
571 raise ParseException(
572 f'Bad N in Delta +/- Expr: {ctx.getText()}'
576 unit = self._figure_out_date_unit(
577 ctx.deltaUnit().getText().lower()
580 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
582 self.context['delta_int'] = n
583 self.context['delta_unit'] = unit
585 def exitNextLastUnit(
586 self, ctx: dateparse_utilsParser.DeltaUnitContext
589 unit = self._figure_out_date_unit(ctx.getText().lower())
591 raise ParseException(f'Bad delta unit: {ctx.getText()}')
593 self.context['delta_unit'] = unit
595 def exitDeltaNextLast(
596 self, ctx: dateparse_utilsParser.DeltaNextLastContext
599 txt = ctx.getText().lower()
601 raise ParseException(f'Bad next/last: {ctx.getText()}')
603 'month' in self.context or
604 'day' in self.context or
605 '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 elif txt[:4] == 'last':
616 self.context['delta_int'] = -1
617 self.context['day'] = self.now_datetime.day
618 self.context['month'] = self.now_datetime.month
619 self.context['year'] = self.now_datetime.year
621 raise ParseException(f'Bad next/last: {ctx.getText()}')
623 def exitCountUnitsBeforeAfterTimeExpr(
624 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
626 if 'nth' not in self.context:
627 raise ParseException(
628 f'Bad count expression: {ctx.getText()}'
631 unit = self._figure_out_time_unit(
632 ctx.deltaTimeUnit().getText().lower()
634 self.context['time_delta_unit'] = unit
636 raise ParseException(f'Bad delta unit: {ctx.getText()}')
637 if 'time_delta_before_after' not in self.context:
638 raise ParseException(
639 f'Bad Before/After: {ctx.getText()}'
642 def exitDeltaTimeFraction(
643 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
646 txt = ctx.getText().lower()[:4]
648 self.context['time_delta_int'] = 15
653 self.context['time_delta_int'] = 30
658 raise ParseException(f'Bad time fraction {ctx.getText()}')
660 raise ParseException(f'Bad time fraction {ctx.getText()}')
662 def exitDeltaBeforeAfter(
663 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
666 txt = ctx.getText().lower()
668 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
670 self.context['delta_before_after'] = txt
672 def exitDeltaTimeBeforeAfter(
673 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
676 txt = ctx.getText().lower()
678 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
680 self.context['time_delta_before_after'] = txt
682 def exitNthWeekdayInMonthMaybeYearExpr(
683 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
685 """Do a bunch of work to convert expressions like...
687 'the 2nd Friday of June' -and-
688 'the last Wednesday in October'
690 ...into base + offset expressions instead.
693 if 'nth' not in self.context:
694 raise ParseException(f'Missing nth number: {ctx.getText()}')
695 n = self.context['nth']
696 if n < 1 or n > 5: # months never have more than 5 Foodays
698 raise ParseException(f'Invalid nth number: {ctx.getText()}')
699 del self.context['nth']
700 self.context['delta_int'] = n
702 year = self.context.get('year', self.today.year)
703 if 'month' not in self.context:
704 raise ParseException(
705 f'Missing month expression: {ctx.getText()}'
707 month = self.context['month']
709 dow = self.context['dow']
710 del self.context['dow']
711 self.context['delta_unit'] = dow
713 # For the nth Fooday in Month, start at the 1st of the
714 # month and count ahead N Foodays. For the last Fooday in
715 # Month, start at the last of the month and count back one
722 tmp_date = datetime.date(year=year, month=month, day=1)
723 tmp_date = tmp_date - datetime.timedelta(days=1)
725 self.context['year'] = tmp_date.year
726 self.context['month'] = tmp_date.month
727 self.context['day'] = tmp_date.day
729 # The delta adjustment code can handle the case where
730 # the last day of the month is the day we're looking
733 self.context['year'] = year
734 self.context['month'] = month
735 self.context['day'] = 1
736 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
738 raise ParseException(
739 f'Invalid nthWeekday expression: {ctx.getText()}'
742 def exitFirstLastWeekdayInMonthMaybeYearExpr(
744 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
746 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
748 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
750 i = self._get_int(ctx.getText())
752 raise ParseException(f'Bad nth expression: {ctx.getText()}')
754 self.context['nth'] = i
757 self, ctx: dateparse_utilsParser.FirstOrLastContext
766 raise ParseException(
767 f'Bad first|last expression: {ctx.getText()}'
770 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
772 self.context['nth'] = txt
774 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
776 dow = ctx.getText().lower()[:3]
777 dow = self.day_name_to_number.get(dow, None)
779 raise ParseException('Bad day of week')
781 self.context['dow'] = dow
784 self, ctx: dateparse_utilsParser.DayOfMonthContext
787 day = ctx.getText().lower()
789 self.context['day'] = 'ide'
792 self.context['day'] = 'non'
795 self.context['day'] = 1
797 day = self._get_int(day)
798 if day < 1 or day > 31:
799 raise ParseException(
800 f'Bad dayOfMonth expression: {ctx.getText()}'
803 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
804 self.context['day'] = day
807 self, ctx: dateparse_utilsParser.MonthNameContext
810 month = ctx.getText()
811 while month[0] == '/' or month[0] == '-':
813 month = month[:3].lower()
814 month = self.month_name_to_number.get(month, None)
816 raise ParseException(
817 f'Bad monthName expression: {ctx.getText()}'
820 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
822 self.context['month'] = month
825 self, ctx: dateparse_utilsParser.MonthNumberContext
828 month = self._get_int(ctx.getText())
829 if month < 1 or month > 12:
830 raise ParseException(
831 f'Bad monthNumber expression: {ctx.getText()}'
834 raise ParseException(
835 f'Bad monthNumber expression: {ctx.getText()}'
838 self.context['month'] = month
840 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
842 year = self._get_int(ctx.getText())
844 raise ParseException(f'Bad year expression: {ctx.getText()}')
846 raise ParseException(f'Bad year expression: {ctx.getText()}')
848 self.context['year'] = year
850 def exitSpecialDateMaybeYearExpr(
851 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
854 special = ctx.specialDate().getText().lower()
855 self.context['special'] = special
857 raise ParseException(
858 f'Bad specialDate expression: {ctx.specialDate().getText()}'
861 mod = ctx.thisNextLast()
863 if mod.THIS() is not None:
864 self.context['special_next_last'] = 'this'
865 elif mod.NEXT() is not None:
866 self.context['special_next_last'] = 'next'
867 elif mod.LAST() is not None:
868 self.context['special_next_last'] = 'last'
870 raise ParseException(
871 f'Bad specialDateNextLast expression: {ctx.getText()}'
874 def exitNFoosFromTodayAgoExpr(
875 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
877 d = self.now_datetime
879 count = self._get_int(ctx.unsignedInt().getText())
880 unit = ctx.deltaUnit().getText().lower()
881 ago_from_now = ctx.AGO_FROM_NOW().getText()
883 raise ParseException(
884 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
887 if "ago" in ago_from_now or "back" in ago_from_now:
890 unit = self._figure_out_date_unit(unit)
891 d = n_timeunits_from_base(
895 self.context['year'] = d.year
896 self.context['month'] = d.month
897 self.context['day'] = d.day
899 def exitDeltaRelativeToTodayExpr(
900 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
902 d = self.now_datetime
904 mod = ctx.thisNextLast()
912 raise ParseException(
913 f'Bad This/Next/Last modifier: {mod}'
915 unit = ctx.deltaUnit().getText().lower()
917 raise ParseException(
918 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
920 unit = self._figure_out_date_unit(unit)
921 d = n_timeunits_from_base(
925 self.context['year'] = d.year
926 self.context['month'] = d.month
927 self.context['day'] = d.day
929 def exitSpecialTimeExpr(
930 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
933 txt = ctx.specialTime().getText().lower()
935 raise ParseException(
936 f'Bad special time expression: {ctx.getText()}'
939 if txt == 'noon' or txt == 'midday':
940 self.context['hour'] = 12
941 self.context['minute'] = 0
942 self.context['seconds'] = 0
943 self.context['micros'] = 0
944 elif txt == 'midnight':
945 self.context['hour'] = 0
946 self.context['minute'] = 0
947 self.context['seconds'] = 0
948 self.context['micros'] = 0
950 raise ParseException(f'Bad special time expression: {txt}')
953 tz = ctx.tzExpr().getText()
954 self.context['tz'] = self._parse_tz(tz)
958 def exitTwelveHourTimeExpr(
959 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
962 hour = ctx.hour().getText()
963 while not hour[-1].isdigit():
965 hour = self._get_int(hour)
967 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
968 if hour <= 0 or hour > 12:
969 raise ParseException(f'Bad hour (out of range): {hour}')
972 minute = self._get_int(ctx.minute().getText())
975 if minute < 0 or minute > 59:
976 raise ParseException(f'Bad minute (out of range): {minute}')
977 self.context['minute'] = minute
980 seconds = self._get_int(ctx.second().getText())
983 if seconds < 0 or seconds > 59:
984 raise ParseException(f'Bad second (out of range): {seconds}')
985 self.context['seconds'] = seconds
988 micros = self._get_int(ctx.micros().getText())
991 if micros < 0 or micros > 1000000:
992 raise ParseException(f'Bad micros (out of range): {micros}')
993 self.context['micros'] = micros
996 ampm = ctx.ampm().getText()
998 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
1003 self.context['hour'] = hour
1006 tz = ctx.tzExpr().getText()
1007 self.context['tz'] = self._parse_tz(tz)
1011 def exitTwentyFourHourTimeExpr(
1012 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1015 hour = ctx.hour().getText()
1016 while not hour[-1].isdigit():
1018 hour = self._get_int(hour)
1020 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
1021 if hour < 0 or hour > 23:
1022 raise ParseException(f'Bad hour (out of range): {hour}')
1023 self.context['hour'] = hour
1026 minute = self._get_int(ctx.minute().getText())
1029 if minute < 0 or minute > 59:
1030 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1031 self.context['minute'] = minute
1034 seconds = self._get_int(ctx.second().getText())
1037 if seconds < 0 or seconds > 59:
1038 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1039 self.context['seconds'] = seconds
1042 micros = self._get_int(ctx.micros().getText())
1045 if micros < 0 or micros >= 1000000:
1046 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1047 self.context['micros'] = micros
1050 tz = ctx.tzExpr().getText()
1051 self.context['tz'] = self._parse_tz(tz)
1056 @bootstrap.initialize
1058 parser = DateParser()
1059 for line in sys.stdin:
1061 line = re.sub(r"#.*$", "", line)
1062 if re.match(r"^ *$", line) is not None:
1065 dt = parser.parse(line)
1066 except Exception as e:
1067 print("Unrecognized.")
1069 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1073 if __name__ == "__main__":