5 import holidays # type: ignore
9 from typing import Any, Callable, Dict, Optional
11 import antlr4 # type: ignore
12 import dateutil.easter
18 import decorator_utils
19 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
20 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
21 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
22 import simple_acl as acl
25 logger = logging.getLogger(__name__)
28 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
29 @functools.wraps(enter_or_exit_f)
30 def debug_parse_wrapper(*args, **kwargs):
36 f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
38 for c in ctx.getChildren():
43 retval = enter_or_exit_f(*args, **kwargs)
45 return debug_parse_wrapper
48 class ParseException(Exception):
49 """An exception thrown during parsing because of unrecognized input."""
50 def __init__(self, message: str) -> None:
52 self.message = message
55 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
56 """An error listener that raises ParseExceptions."""
58 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(debug_parse,
82 acl=acl.StringWildcardBasedACL(
88 order_to_check_allow_deny=acl.ACL_ORDER_DENY_ALLOW,
91 class DateParser(dateparse_utilsListener):
92 PARSE_TYPE_SINGLE_DATE_EXPR = 1
93 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
94 PARSE_TYPE_SINGLE_TIME_EXPR = 3
95 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
100 override_now_for_test_purposes = None
102 """C'tor. Passing a value to override_now_for_test_purposes can be
103 used to force this instance to use a custom date/time for its
104 idea of "now" so that the code can be more easily unittested.
105 Leave as None for real use cases.
107 self.month_name_to_number = {
122 # Used only for ides/nones. Month length on a non-leap year.
123 self.typical_days_per_month = {
138 # N.B. day number is also synched with datetime_utils.TimeUnit values
139 # which allows expressions like "3 wednesdays from now" to work.
140 self.day_name_to_number = {
150 # These TimeUnits are defined in datetime_utils and are used as params
151 # to datetime_utils.n_timeunits_from_base.
152 self.time_delta_unit_to_constant = {
153 'hou': datetime_utils.TimeUnit.HOURS,
154 'min': datetime_utils.TimeUnit.MINUTES,
155 'sec': datetime_utils.TimeUnit.SECONDS,
157 self.delta_unit_to_constant = {
158 'day': datetime_utils.TimeUnit.DAYS,
159 'wor': datetime_utils.TimeUnit.WORKDAYS,
160 'wee': datetime_utils.TimeUnit.WEEKS,
161 'mon': datetime_utils.TimeUnit.MONTHS,
162 'yea': datetime_utils.TimeUnit.YEARS,
164 self.override_now_for_test_purposes = override_now_for_test_purposes
167 def parse(self, date_string: str) -> Optional[datetime.datetime]:
168 """Parse a date/time expression and return a timezone agnostic
169 datetime on success. Also sets self.datetime, self.date and
170 self.time which can each be accessed other methods on the
171 class: get_datetime(), get_date() and get_time(). Raises a
172 ParseException with a helpful(?) message on parse error or
175 To get an idea of what expressions can be parsed, check out
176 the unittest and the grammar.
180 txt = '3 weeks before last tues at 9:15am'
183 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
185 # dt1 and dt2 will be identical other than the fact that
186 # the latter's tzinfo will be set to PST/PDT.
188 This is the main entrypoint to this class for caller code.
191 listener = RaisingErrorListener()
192 input_stream = antlr4.InputStream(date_string)
193 lexer = dateparse_utilsLexer(input_stream)
194 lexer.removeErrorListeners()
195 lexer.addErrorListener(listener)
196 stream = antlr4.CommonTokenStream(lexer)
197 parser = dateparse_utilsParser(stream)
198 parser.removeErrorListeners()
199 parser.addErrorListener(listener)
200 tree = parser.parse()
201 walker = antlr4.ParseTreeWalker()
202 walker.walk(self, tree)
205 def get_date(self) -> Optional[datetime.date]:
206 """Return the date part or None."""
209 def get_time(self) -> Optional[datetime.time]:
210 """Return the time part or None."""
213 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
214 """Return as a datetime. Parsed date expressions without any time
215 part return hours = minutes = seconds = microseconds = 0 (i.e. at
216 midnight that day). Parsed time expressions without any date part
217 default to date = today.
219 The optional tz param allows the caller to request the datetime be
220 timezone aware and sets the tzinfo to the indicated zone. Defaults
221 to timezone naive (i.e. tzinfo = None).
225 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
231 """Reset at init and between parses."""
232 if self.override_now_for_test_purposes is None:
233 self.now_datetime = datetime.datetime.now()
234 self.today = datetime.date.today()
236 self.now_datetime = self.override_now_for_test_purposes
237 self.today = datetime_utils.datetime_to_date(
238 self.override_now_for_test_purposes
240 self.date: Optional[datetime.date] = None
241 self.time: Optional[datetime.time] = None
242 self.datetime: Optional[datetime.datetime] = None
243 self.context: Dict[str, Any] = {}
244 self.timedelta = datetime.timedelta(seconds=0)
247 def _normalize_special_day_name(name: str) -> str:
248 """String normalization / canonicalization for date expressions."""
250 name = name.replace("'", '')
251 name = name.replace('xmas', 'christmas')
252 name = name.replace('mlk', 'martin luther king')
253 name = name.replace(' ', '')
254 eve = 'eve' if name[-3:] == 'eve' else ''
255 name = name[:5] + eve
256 name = name.replace('washi', 'presi')
259 def _figure_out_date_unit(self, orig: str) -> int:
260 """Figure out what unit a date expression piece is talking about."""
262 return datetime_utils.TimeUnit.MONTHS
263 txt = orig.lower()[:3]
264 if txt in self.day_name_to_number:
265 return(self.day_name_to_number[txt])
266 elif txt in self.delta_unit_to_constant:
267 return(self.delta_unit_to_constant[txt])
268 raise ParseException(f'Invalid date unit: {orig}')
270 def _figure_out_time_unit(self, orig: str) -> int:
271 """Figure out what unit a time expression piece is talking about."""
272 txt = orig.lower()[:3]
273 if txt in self.time_delta_unit_to_constant:
274 return(self.time_delta_unit_to_constant[txt])
275 raise ParseException(f'Invalid time unit: {orig}')
277 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
278 """Parse what we think is a special date name and return its datetime
279 (or None if it can't be parsed).
282 year = self.context.get('year', today.year)
283 name = DateParser._normalize_special_day_name(self.context['special'])
285 # Yesterday, today, tomorrow -- ignore any next/last
286 if name == 'today' or name == 'now':
289 return today + datetime.timedelta(days=-1)
291 return today + datetime.timedelta(days=+1)
293 next_last = self.context.get('special_next_last', '')
294 if next_last == 'next':
296 elif next_last == 'last':
301 return dateutil.easter.easter(year=year)
302 elif name == 'hallo':
303 return datetime.date(year=year, month=10, day=31)
305 for holiday_date, holiday_name in sorted(
306 holidays.US(years=year).items()
308 if 'Observed' not in holiday_name:
309 holiday_name = DateParser._normalize_special_day_name(
312 if name == holiday_name:
314 if name == 'chriseve':
315 return datetime.date(year=year, month=12, day=24)
316 elif name == 'newyeeve':
317 return datetime.date(year=year, month=12, day=31)
320 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
321 """Handle date expressions like "the ides of March" which require
322 both the "ides" and the month since the definition of the "ides"
323 changes based on the length of the month.
325 assert 'ide' in day or 'non' in day
326 assert month_number in self.typical_days_per_month
327 typical_days_per_month = self.typical_days_per_month[month_number]
330 if typical_days_per_month == 31:
331 if self.context['day'] == 'ide':
338 if self.context['day'] == 'ide':
343 def _parse_normal_date(self) -> datetime.date:
344 if 'dow' in self.context:
346 while d.weekday() != self.context['dow']:
347 d += datetime.timedelta(days=1)
350 if 'month' not in self.context:
351 raise ParseException('Missing month')
352 if 'day' not in self.context:
353 raise ParseException('Missing day')
354 if 'year' not in self.context:
355 self.context['year'] = self.today.year
357 # Handling "ides" and "nones" requires both the day and month.
359 self.context['day'] == 'ide' or
360 self.context['day'] == 'non'
362 self.context['day'] = self._resolve_ides_nones(
363 self.context['day'], self.context['month']
366 return datetime.date(
367 year=self.context['year'],
368 month=self.context['month'],
369 day=self.context['day'],
372 def _parse_tz(self, txt: str) -> Any:
378 tz = pytz.timezone(txt)
386 tz = dateutil.tz.gettz(txt)
392 # Try constructing an offset in seconds
395 if sign == '-' or sign == '+':
396 sign = +1 if sign == '+' else -1
398 minute = int(txt[-2:])
399 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
400 tzoffset = dateutil.tz.tzoffset(txt, offset)
406 def _get_int(self, txt: str) -> int:
407 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
409 while not txt[-1].isdigit():
413 # -- overridden methods invoked by parse walk --
415 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
418 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
421 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
422 """Populate self.datetime."""
423 if self.date is None:
424 self.date = self.today
425 year = self.date.year
426 month = self.date.month
429 if self.time is None:
430 self.time = datetime.time(0, 0, 0)
431 hour = self.time.hour
432 minute = self.time.minute
433 second = self.time.second
434 micros = self.time.microsecond
436 self.datetime = datetime.datetime(
437 year, month, day, hour, minute, second, micros,
438 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,
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' or
491 before_after == 'until' or
492 before_after == 'til' or
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 = datetime_utils.n_timeunits_from_base(
504 datetime_utils.date_to_datetime(self.date)
506 self.date = datetime_utils.datetime_to_date(dt)
508 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
510 self.time = datetime.time(
511 self.context['hour'],
512 self.context['minute'],
513 self.context['seconds'],
514 self.context['micros'],
515 tzinfo=self.context.get('tz', None),
517 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
520 # If we get here there (should be) a relative adjustment to
522 if 'nth' in self.context:
523 count = self.context['nth']
524 elif 'time_delta_int' in self.context:
525 count = self.context['time_delta_int']
527 raise ParseException('Missing delta in relative time.')
531 # Adjust count's sign based on the presence of 'before' or 'after'.
532 if 'time_delta_before_after' in self.context:
533 before_after = self.context['time_delta_before_after'].lower()
535 before_after == 'before' or
536 before_after == 'until' or
537 before_after == 'til' or
542 # What are we counting units of... assume minutes.
543 if 'time_delta_unit' not in self.context:
544 self.timedelta += datetime.timedelta(minutes=count)
546 unit = self.context['time_delta_unit']
547 if unit == datetime_utils.TimeUnit.SECONDS:
548 self.timedelta += datetime.timedelta(seconds=count)
549 elif unit == datetime_utils.TimeUnit.MINUTES:
550 self.timedelta = datetime.timedelta(minutes=count)
551 elif unit == datetime_utils.TimeUnit.HOURS:
552 self.timedelta = datetime.timedelta(hours=count)
554 raise ParseException()
556 def exitDeltaPlusMinusExpr(
557 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
562 raise ParseException(
563 f'Bad N in Delta +/- Expr: {ctx.getText()}'
567 unit = self._figure_out_date_unit(
568 ctx.deltaUnit().getText().lower()
571 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
573 self.context['delta_int'] = n
574 self.context['delta_unit'] = unit
576 def exitNextLastUnit(
577 self, ctx: dateparse_utilsParser.DeltaUnitContext
580 unit = self._figure_out_date_unit(ctx.getText().lower())
582 raise ParseException(f'Bad delta unit: {ctx.getText()}')
584 self.context['delta_unit'] = unit
586 def exitDeltaNextLast(
587 self, ctx: dateparse_utilsParser.DeltaNextLastContext
590 txt = ctx.getText().lower()
592 raise ParseException(f'Bad next/last: {ctx.getText()}')
594 'month' in self.context or
595 'day' in self.context or
596 'year' in self.context
598 raise ParseException(
599 'Next/last expression expected to be relative to today.'
601 if txt[:4] == 'next':
602 self.context['delta_int'] = +1
603 self.context['day'] = self.now_datetime.day
604 self.context['month'] = self.now_datetime.month
605 self.context['year'] = self.now_datetime.year
606 elif txt[:4] == 'last':
607 self.context['delta_int'] = -1
608 self.context['day'] = self.now_datetime.day
609 self.context['month'] = self.now_datetime.month
610 self.context['year'] = self.now_datetime.year
612 raise ParseException(f'Bad next/last: {ctx.getText()}')
614 def exitCountUnitsBeforeAfterTimeExpr(
615 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
617 if 'nth' not in self.context:
618 raise ParseException(
619 f'Bad count expression: {ctx.getText()}'
622 unit = self._figure_out_time_unit(
623 ctx.deltaTimeUnit().getText().lower()
625 self.context['time_delta_unit'] = unit
627 raise ParseException(f'Bad delta unit: {ctx.getText()}')
628 if 'time_delta_before_after' not in self.context:
629 raise ParseException(
630 f'Bad Before/After: {ctx.getText()}'
633 def exitDeltaTimeFraction(
634 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
637 txt = ctx.getText().lower()[:4]
639 self.context['time_delta_int'] = 15
642 ] = datetime_utils.TimeUnit.MINUTES
644 self.context['time_delta_int'] = 30
647 ] = datetime_utils.TimeUnit.MINUTES
649 raise ParseException(f'Bad time fraction {ctx.getText()}')
651 raise ParseException(f'Bad time fraction {ctx.getText()}')
653 def exitDeltaBeforeAfter(
654 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
657 txt = ctx.getText().lower()
659 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
661 self.context['delta_before_after'] = txt
663 def exitDeltaTimeBeforeAfter(
664 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
667 txt = ctx.getText().lower()
669 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
671 self.context['time_delta_before_after'] = txt
673 def exitNthWeekdayInMonthMaybeYearExpr(
674 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
676 """Do a bunch of work to convert expressions like...
678 'the 2nd Friday of June' -and-
679 'the last Wednesday in October'
681 ...into base + offset expressions instead.
684 if 'nth' not in self.context:
685 raise ParseException(f'Missing nth number: {ctx.getText()}')
686 n = self.context['nth']
687 if n < 1 or n > 5: # months never have more than 5 Foodays
689 raise ParseException(f'Invalid nth number: {ctx.getText()}')
690 del self.context['nth']
691 self.context['delta_int'] = n
693 year = self.context.get('year', self.today.year)
694 if 'month' not in self.context:
695 raise ParseException(
696 f'Missing month expression: {ctx.getText()}'
698 month = self.context['month']
700 dow = self.context['dow']
701 del self.context['dow']
702 self.context['delta_unit'] = dow
704 # For the nth Fooday in Month, start at the 1st of the
705 # month and count ahead N Foodays. For the last Fooday in
706 # Month, start at the last of the month and count back one
713 tmp_date = datetime.date(year=year, month=month, day=1)
714 tmp_date = tmp_date - datetime.timedelta(days=1)
716 self.context['year'] = tmp_date.year
717 self.context['month'] = tmp_date.month
718 self.context['day'] = tmp_date.day
720 # The delta adjustment code can handle the case where
721 # the last day of the month is the day we're looking
724 self.context['year'] = year
725 self.context['month'] = month
726 self.context['day'] = 1
727 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
729 raise ParseException(
730 f'Invalid nthWeekday expression: {ctx.getText()}'
733 def exitFirstLastWeekdayInMonthMaybeYearExpr(
735 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
737 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
739 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
741 i = self._get_int(ctx.getText())
743 raise ParseException(f'Bad nth expression: {ctx.getText()}')
745 self.context['nth'] = i
748 self, ctx: dateparse_utilsParser.FirstOrLastContext
757 raise ParseException(
758 f'Bad first|last expression: {ctx.getText()}'
761 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
763 self.context['nth'] = txt
765 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
767 dow = ctx.getText().lower()[:3]
768 dow = self.day_name_to_number.get(dow, None)
770 raise ParseException('Bad day of week')
772 self.context['dow'] = dow
775 self, ctx: dateparse_utilsParser.DayOfMonthContext
778 day = ctx.getText().lower()
780 self.context['day'] = 'ide'
783 self.context['day'] = 'non'
786 self.context['day'] = 1
788 day = self._get_int(day)
789 if day < 1 or day > 31:
790 raise ParseException(
791 f'Bad dayOfMonth expression: {ctx.getText()}'
794 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
795 self.context['day'] = day
798 self, ctx: dateparse_utilsParser.MonthNameContext
801 month = ctx.getText()
802 while month[0] == '/' or month[0] == '-':
804 month = month[:3].lower()
805 month = self.month_name_to_number.get(month, None)
807 raise ParseException(
808 f'Bad monthName expression: {ctx.getText()}'
811 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
813 self.context['month'] = month
816 self, ctx: dateparse_utilsParser.MonthNumberContext
819 month = self._get_int(ctx.getText())
820 if month < 1 or month > 12:
821 raise ParseException(
822 f'Bad monthNumber expression: {ctx.getText()}'
825 raise ParseException(
826 f'Bad monthNumber expression: {ctx.getText()}'
829 self.context['month'] = month
831 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
833 year = self._get_int(ctx.getText())
835 raise ParseException(f'Bad year expression: {ctx.getText()}')
837 raise ParseException(f'Bad year expression: {ctx.getText()}')
839 self.context['year'] = year
841 def exitSpecialDateMaybeYearExpr(
842 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
845 special = ctx.specialDate().getText().lower()
846 self.context['special'] = special
848 raise ParseException(
849 f'Bad specialDate expression: {ctx.specialDate().getText()}'
852 mod = ctx.thisNextLast()
854 if mod.THIS() is not None:
855 self.context['special_next_last'] = 'this'
856 elif mod.NEXT() is not None:
857 self.context['special_next_last'] = 'next'
858 elif mod.LAST() is not None:
859 self.context['special_next_last'] = 'last'
861 raise ParseException(
862 f'Bad specialDateNextLast expression: {ctx.getText()}'
865 def exitNFoosFromTodayAgoExpr(
866 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
868 d = self.now_datetime
870 count = self._get_int(ctx.unsignedInt().getText())
871 unit = ctx.deltaUnit().getText().lower()
872 ago_from_now = ctx.AGO_FROM_NOW().getText()
874 raise ParseException(
875 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
878 if "ago" in ago_from_now or "back" in ago_from_now:
881 unit = self._figure_out_date_unit(unit)
882 d = datetime_utils.n_timeunits_from_base(
886 self.context['year'] = d.year
887 self.context['month'] = d.month
888 self.context['day'] = d.day
890 def exitDeltaRelativeToTodayExpr(
891 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
893 d = self.now_datetime
895 mod = ctx.thisNextLast()
903 raise ParseException(
904 f'Bad This/Next/Last modifier: {mod}'
906 unit = ctx.deltaUnit().getText().lower()
908 raise ParseException(
909 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
911 unit = self._figure_out_date_unit(unit)
912 d = datetime_utils.n_timeunits_from_base(
916 self.context['year'] = d.year
917 self.context['month'] = d.month
918 self.context['day'] = d.day
920 def exitSpecialTimeExpr(
921 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
924 txt = ctx.specialTime().getText().lower()
926 raise ParseException(
927 f'Bad special time expression: {ctx.getText()}'
930 if txt == 'noon' or txt == 'midday':
931 self.context['hour'] = 12
932 self.context['minute'] = 0
933 self.context['seconds'] = 0
934 self.context['micros'] = 0
935 elif txt == 'midnight':
936 self.context['hour'] = 0
937 self.context['minute'] = 0
938 self.context['seconds'] = 0
939 self.context['micros'] = 0
941 raise ParseException(f'Bad special time expression: {txt}')
944 tz = ctx.tzExpr().getText()
945 self.context['tz'] = self._parse_tz(tz)
949 def exitTwelveHourTimeExpr(
950 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
953 hour = ctx.hour().getText()
954 while not hour[-1].isdigit():
956 hour = self._get_int(hour)
958 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
959 if hour <= 0 or hour > 12:
960 raise ParseException(f'Bad hour (out of range): {hour}')
963 minute = self._get_int(ctx.minute().getText())
966 if minute < 0 or minute > 59:
967 raise ParseException(f'Bad minute (out of range): {minute}')
968 self.context['minute'] = minute
971 seconds = self._get_int(ctx.second().getText())
974 if seconds < 0 or seconds > 59:
975 raise ParseException(f'Bad second (out of range): {seconds}')
976 self.context['seconds'] = seconds
979 micros = self._get_int(ctx.micros().getText())
982 if micros < 0 or micros > 1000000:
983 raise ParseException(f'Bad micros (out of range): {micros}')
984 self.context['micros'] = micros
987 ampm = ctx.ampm().getText()
989 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
994 self.context['hour'] = hour
997 tz = ctx.tzExpr().getText()
998 self.context['tz'] = self._parse_tz(tz)
1002 def exitTwentyFourHourTimeExpr(
1003 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1006 hour = ctx.hour().getText()
1007 while not hour[-1].isdigit():
1009 hour = self._get_int(hour)
1011 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
1012 if hour < 0 or hour > 23:
1013 raise ParseException(f'Bad hour (out of range): {hour}')
1014 self.context['hour'] = hour
1017 minute = self._get_int(ctx.minute().getText())
1020 if minute < 0 or minute > 59:
1021 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1022 self.context['minute'] = minute
1025 seconds = self._get_int(ctx.second().getText())
1028 if seconds < 0 or seconds > 59:
1029 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1030 self.context['seconds'] = seconds
1033 micros = self._get_int(ctx.micros().getText())
1036 if micros < 0 or micros >= 1000000:
1037 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1038 self.context['micros'] = micros
1041 tz = ctx.tzExpr().getText()
1042 self.context['tz'] = self._parse_tz(tz)
1048 parser = DateParser()
1049 for line in sys.stdin:
1051 line = re.sub(r"#.*$", "", line)
1052 if re.match(r"^ *$", line) is not None:
1055 dt = parser.parse(line)
1056 except Exception as e:
1057 print("Unrecognized.")
1059 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1063 if __name__ == "__main__":
1064 main = bootstrap.initialize(main)