4 Parse dates in a variety of formats.
10 import holidays # type: ignore
14 from typing import Any, Callable, Dict, Optional
16 import antlr4 # type: ignore
17 import dateutil.easter
23 from datetime_utils import (
24 TimeUnit, n_timeunits_from_base, datetime_to_date, date_to_datetime
26 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
27 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
28 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
29 import decorator_utils
32 logger = logging.getLogger(__name__)
35 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
36 @functools.wraps(enter_or_exit_f)
37 def debug_parse_wrapper(*args, **kwargs):
43 f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
45 for c in ctx.getChildren():
50 retval = enter_or_exit_f(*args, **kwargs)
52 return debug_parse_wrapper
55 class ParseException(Exception):
56 """An exception thrown during parsing because of unrecognized input."""
57 def __init__(self, message: str) -> None:
58 self.message = message
61 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
62 """An error listener that raises ParseExceptions."""
64 self, recognizer, offendingSymbol, line, column, msg, e
66 raise ParseException(msg)
69 self, recognizer, dfa, startIndex, stopIndex, exact,
74 def reportAttemptingFullContext(
75 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts,
80 def reportContextSensitivity(
81 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
86 @decorator_utils.decorate_matching_methods_with(
88 acl=acl.StringWildcardBasedACL(
97 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
101 class DateParser(dateparse_utilsListener):
102 PARSE_TYPE_SINGLE_DATE_EXPR = 1
103 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
104 PARSE_TYPE_SINGLE_TIME_EXPR = 3
105 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
110 override_now_for_test_purposes = None
112 """C'tor. Passing a value to override_now_for_test_purposes can be
113 used to force this instance to use a custom date/time for its
114 idea of "now" so that the code can be more easily unittested.
115 Leave as None for real use cases.
117 self.month_name_to_number = {
132 # Used only for ides/nones. Month length on a non-leap year.
133 self.typical_days_per_month = {
148 # N.B. day number is also synched with datetime_utils.TimeUnit values
149 # which allows expressions like "3 wednesdays from now" to work.
150 self.day_name_to_number = {
160 # These TimeUnits are defined in datetime_utils and are used as params
161 # to datetime_utils.n_timeunits_from_base.
162 self.time_delta_unit_to_constant = {
163 'hou': TimeUnit.HOURS,
164 'min': TimeUnit.MINUTES,
165 'sec': TimeUnit.SECONDS,
167 self.delta_unit_to_constant = {
168 'day': TimeUnit.DAYS,
169 'wor': TimeUnit.WORKDAYS,
170 'wee': TimeUnit.WEEKS,
171 'mon': TimeUnit.MONTHS,
172 'yea': TimeUnit.YEARS,
174 self.override_now_for_test_purposes = override_now_for_test_purposes
177 def parse(self, date_string: str) -> Optional[datetime.datetime]:
178 """Parse a date/time expression and return a timezone agnostic
179 datetime on success. Also sets self.datetime, self.date and
180 self.time which can each be accessed other methods on the
181 class: get_datetime(), get_date() and get_time(). Raises a
182 ParseException with a helpful(?) message on parse error or
185 To get an idea of what expressions can be parsed, check out
186 the unittest and the grammar.
190 txt = '3 weeks before last tues at 9:15am'
193 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
195 # dt1 and dt2 will be identical other than the fact that
196 # the latter's tzinfo will be set to PST/PDT.
198 This is the main entrypoint to this class for caller code.
200 date_string = date_string.strip()
201 date_string = re.sub('\s+', ' ', date_string)
203 listener = RaisingErrorListener()
204 input_stream = antlr4.InputStream(date_string)
205 lexer = dateparse_utilsLexer(input_stream)
206 lexer.removeErrorListeners()
207 lexer.addErrorListener(listener)
208 stream = antlr4.CommonTokenStream(lexer)
209 parser = dateparse_utilsParser(stream)
210 parser.removeErrorListeners()
211 parser.addErrorListener(listener)
212 tree = parser.parse()
213 walker = antlr4.ParseTreeWalker()
214 walker.walk(self, tree)
217 def get_date(self) -> Optional[datetime.date]:
218 """Return the date part or None."""
221 def get_time(self) -> Optional[datetime.time]:
222 """Return the time part or None."""
225 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
226 """Return as a datetime. Parsed date expressions without any time
227 part return hours = minutes = seconds = microseconds = 0 (i.e. at
228 midnight that day). Parsed time expressions without any date part
229 default to date = today.
231 The optional tz param allows the caller to request the datetime be
232 timezone aware and sets the tzinfo to the indicated zone. Defaults
233 to timezone naive (i.e. tzinfo = None).
237 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
243 """Reset at init and between parses."""
244 if self.override_now_for_test_purposes is None:
245 self.now_datetime = datetime.datetime.now()
246 self.today = datetime.date.today()
248 self.now_datetime = self.override_now_for_test_purposes
249 self.today = datetime_to_date(
250 self.override_now_for_test_purposes
252 self.date: Optional[datetime.date] = None
253 self.time: Optional[datetime.time] = None
254 self.datetime: Optional[datetime.datetime] = None
255 self.context: Dict[str, Any] = {}
256 self.timedelta = datetime.timedelta(seconds=0)
257 self.saw_overt_year = False
260 def _normalize_special_day_name(name: str) -> str:
261 """String normalization / canonicalization for date expressions."""
263 name = name.replace("'", '')
264 name = name.replace('xmas', 'christmas')
265 name = name.replace('mlk', 'martin luther king')
266 name = name.replace(' ', '')
267 eve = 'eve' if name[-3:] == 'eve' else ''
268 name = name[:5] + eve
269 name = name.replace('washi', 'presi')
272 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
273 """Figure out what unit a date expression piece is talking about."""
275 return TimeUnit.MONTHS
276 txt = orig.lower()[:3]
277 if txt in self.day_name_to_number:
278 return(TimeUnit(self.day_name_to_number[txt]))
279 elif txt in self.delta_unit_to_constant:
280 return(TimeUnit(self.delta_unit_to_constant[txt]))
281 raise ParseException(f'Invalid date unit: {orig}')
283 def _figure_out_time_unit(self, orig: str) -> int:
284 """Figure out what unit a time expression piece is talking about."""
285 txt = orig.lower()[:3]
286 if txt in self.time_delta_unit_to_constant:
287 return(self.time_delta_unit_to_constant[txt])
288 raise ParseException(f'Invalid time unit: {orig}')
290 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
291 """Parse what we think is a special date name and return its datetime
292 (or None if it can't be parsed).
295 year = self.context.get('year', today.year)
296 name = DateParser._normalize_special_day_name(self.context['special'])
298 # Yesterday, today, tomorrow -- ignore any next/last
299 if name == 'today' or name == 'now':
302 return today + datetime.timedelta(days=-1)
304 return today + datetime.timedelta(days=+1)
306 next_last = self.context.get('special_next_last', '')
307 if next_last == 'next':
309 self.saw_overt_year = True
310 elif next_last == 'last':
312 self.saw_overt_year = True
316 return dateutil.easter.easter(year=year)
317 elif name == 'hallo':
318 return datetime.date(year=year, month=10, day=31)
320 for holiday_date, holiday_name in sorted(
321 holidays.US(years=year).items()
323 if 'Observed' not in holiday_name:
324 holiday_name = DateParser._normalize_special_day_name(
327 if name == holiday_name:
329 if name == 'chriseve':
330 return datetime.date(year=year, month=12, day=24)
331 elif name == 'newyeeve':
332 return datetime.date(year=year, month=12, day=31)
335 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
336 """Handle date expressions like "the ides of March" which require
337 both the "ides" and the month since the definition of the "ides"
338 changes based on the length of the month.
340 assert 'ide' in day or 'non' in day
341 assert month_number in self.typical_days_per_month
342 typical_days_per_month = self.typical_days_per_month[month_number]
345 if typical_days_per_month == 31:
346 if self.context['day'] == 'ide':
353 if self.context['day'] == 'ide':
358 def _parse_normal_date(self) -> datetime.date:
359 if 'dow' in self.context:
361 while d.weekday() != self.context['dow']:
362 d += datetime.timedelta(days=1)
365 if 'month' not in self.context:
366 raise ParseException('Missing month')
367 if 'day' not in self.context:
368 raise ParseException('Missing day')
369 if 'year' not in self.context:
370 self.context['year'] = self.today.year
371 self.saw_overt_year = False
373 self.saw_overt_year = True
375 # Handling "ides" and "nones" requires both the day and month.
377 self.context['day'] == 'ide' or
378 self.context['day'] == 'non'
380 self.context['day'] = self._resolve_ides_nones(
381 self.context['day'], self.context['month']
384 return datetime.date(
385 year=self.context['year'],
386 month=self.context['month'],
387 day=self.context['day'],
390 def _parse_tz(self, txt: str) -> Any:
396 tz = pytz.timezone(txt)
404 tz = dateutil.tz.gettz(txt)
410 # Try constructing an offset in seconds
413 if sign == '-' or sign == '+':
414 sign = +1 if sign == '+' else -1
416 minute = int(txt[-2:])
417 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
418 tzoffset = dateutil.tz.tzoffset(txt, offset)
424 def _get_int(self, txt: str) -> int:
425 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
427 while not txt[-1].isdigit():
431 # -- overridden methods invoked by parse walk --
433 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
436 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
439 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
440 """Populate self.datetime."""
441 if self.date is None:
442 self.date = self.today
443 year = self.date.year
444 month = self.date.month
447 if self.time is None:
448 self.time = datetime.time(0, 0, 0)
449 hour = self.time.hour
450 minute = self.time.minute
451 second = self.time.second
452 micros = self.time.microsecond
454 self.datetime = datetime.datetime(
455 year, month, day, hour, minute, second, micros,
456 tzinfo=self.time.tzinfo
459 # Apply resudual adjustments to times here when we have a
461 self.datetime = self.datetime + self.timedelta
462 self.time = datetime.time(
464 self.datetime.minute,
465 self.datetime.second,
466 self.datetime.microsecond,
470 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
472 if ctx.singleDateExpr() is not None:
473 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
474 elif ctx.baseAndOffsetDateExpr() is not None:
475 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
477 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
479 if ctx.singleTimeExpr() is not None:
480 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
481 elif ctx.baseAndOffsetTimeExpr() is not None:
482 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
484 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
485 """When we leave the date expression, populate self.date."""
486 if 'special' in self.context:
487 self.date = self._parse_special_date(self.context['special'])
489 self.date = self._parse_normal_date()
490 assert self.date is not None
492 # For a single date, just return the date we pulled out.
493 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
496 # Otherwise treat self.date as a base date that we're modifying
498 if 'delta_int' not in self.context:
499 raise ParseException('Missing delta_int?!')
500 count = self.context['delta_int']
504 # Adjust count's sign based on the presence of 'before' or 'after'.
505 if 'delta_before_after' in self.context:
506 before_after = self.context['delta_before_after'].lower()
508 before_after == 'before' or
509 before_after == 'until' or
510 before_after == 'til' or
515 # What are we counting units of?
516 if 'delta_unit' not in self.context:
517 raise ParseException('Missing delta_unit?!')
518 unit = self.context['delta_unit']
519 dt = n_timeunits_from_base(
522 date_to_datetime(self.date)
524 self.date = datetime_to_date(dt)
526 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
528 self.time = datetime.time(
529 self.context['hour'],
530 self.context['minute'],
531 self.context['seconds'],
532 self.context['micros'],
533 tzinfo=self.context.get('tz', None),
535 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
538 # If we get here there (should be) a relative adjustment to
540 if 'nth' in self.context:
541 count = self.context['nth']
542 elif 'time_delta_int' in self.context:
543 count = self.context['time_delta_int']
545 raise ParseException('Missing delta in relative time.')
549 # Adjust count's sign based on the presence of 'before' or 'after'.
550 if 'time_delta_before_after' in self.context:
551 before_after = self.context['time_delta_before_after'].lower()
553 before_after == 'before' or
554 before_after == 'until' or
555 before_after == 'til' or
560 # What are we counting units of... assume minutes.
561 if 'time_delta_unit' not in self.context:
562 self.timedelta += datetime.timedelta(minutes=count)
564 unit = self.context['time_delta_unit']
565 if unit == TimeUnit.SECONDS:
566 self.timedelta += datetime.timedelta(seconds=count)
567 elif unit == TimeUnit.MINUTES:
568 self.timedelta = datetime.timedelta(minutes=count)
569 elif unit == TimeUnit.HOURS:
570 self.timedelta = datetime.timedelta(hours=count)
572 raise ParseException()
574 def exitDeltaPlusMinusExpr(
575 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
580 raise ParseException(
581 f'Bad N in Delta +/- Expr: {ctx.getText()}'
585 unit = self._figure_out_date_unit(
586 ctx.deltaUnit().getText().lower()
589 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
591 self.context['delta_int'] = n
592 self.context['delta_unit'] = unit
594 def exitNextLastUnit(
595 self, ctx: dateparse_utilsParser.DeltaUnitContext
598 unit = self._figure_out_date_unit(ctx.getText().lower())
600 raise ParseException(f'Bad delta unit: {ctx.getText()}')
602 self.context['delta_unit'] = unit
604 def exitDeltaNextLast(
605 self, ctx: dateparse_utilsParser.DeltaNextLastContext
608 txt = ctx.getText().lower()
610 raise ParseException(f'Bad next/last: {ctx.getText()}')
612 'month' in self.context or
613 'day' in self.context or
614 'year' in self.context
616 raise ParseException(
617 'Next/last expression expected to be relative to today.'
619 if txt[:4] == 'next':
620 self.context['delta_int'] = +1
621 self.context['day'] = self.now_datetime.day
622 self.context['month'] = self.now_datetime.month
623 self.context['year'] = self.now_datetime.year
624 self.saw_overt_year = True
625 elif txt[:4] == 'last':
626 self.context['delta_int'] = -1
627 self.context['day'] = self.now_datetime.day
628 self.context['month'] = self.now_datetime.month
629 self.context['year'] = self.now_datetime.year
630 self.saw_overt_year = True
632 raise ParseException(f'Bad next/last: {ctx.getText()}')
634 def exitCountUnitsBeforeAfterTimeExpr(
635 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
637 if 'nth' not in self.context:
638 raise ParseException(
639 f'Bad count expression: {ctx.getText()}'
642 unit = self._figure_out_time_unit(
643 ctx.deltaTimeUnit().getText().lower()
645 self.context['time_delta_unit'] = unit
647 raise ParseException(f'Bad delta unit: {ctx.getText()}')
648 if 'time_delta_before_after' not in self.context:
649 raise ParseException(
650 f'Bad Before/After: {ctx.getText()}'
653 def exitDeltaTimeFraction(
654 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
657 txt = ctx.getText().lower()[:4]
659 self.context['time_delta_int'] = 15
664 self.context['time_delta_int'] = 30
669 raise ParseException(f'Bad time fraction {ctx.getText()}')
671 raise ParseException(f'Bad time fraction {ctx.getText()}')
673 def exitDeltaBeforeAfter(
674 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
677 txt = ctx.getText().lower()
679 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
681 self.context['delta_before_after'] = txt
683 def exitDeltaTimeBeforeAfter(
684 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
687 txt = ctx.getText().lower()
689 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
691 self.context['time_delta_before_after'] = txt
693 def exitNthWeekdayInMonthMaybeYearExpr(
694 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
696 """Do a bunch of work to convert expressions like...
698 'the 2nd Friday of June' -and-
699 'the last Wednesday in October'
701 ...into base + offset expressions instead.
704 if 'nth' not in self.context:
705 raise ParseException(f'Missing nth number: {ctx.getText()}')
706 n = self.context['nth']
707 if n < 1 or n > 5: # months never have more than 5 Foodays
709 raise ParseException(f'Invalid nth number: {ctx.getText()}')
710 del self.context['nth']
711 self.context['delta_int'] = n
713 year = self.context.get('year', self.today.year)
714 if 'month' not in self.context:
715 raise ParseException(
716 f'Missing month expression: {ctx.getText()}'
718 month = self.context['month']
720 dow = self.context['dow']
721 del self.context['dow']
722 self.context['delta_unit'] = dow
724 # For the nth Fooday in Month, start at the 1st of the
725 # month and count ahead N Foodays. For the last Fooday in
726 # Month, start at the last of the month and count back one
733 tmp_date = datetime.date(year=year, month=month, day=1)
734 tmp_date = tmp_date - datetime.timedelta(days=1)
736 self.context['year'] = tmp_date.year
737 self.context['month'] = tmp_date.month
738 self.context['day'] = tmp_date.day
740 # The delta adjustment code can handle the case where
741 # the last day of the month is the day we're looking
744 self.context['year'] = year
745 self.context['month'] = month
746 self.context['day'] = 1
747 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
749 raise ParseException(
750 f'Invalid nthWeekday expression: {ctx.getText()}'
753 def exitFirstLastWeekdayInMonthMaybeYearExpr(
755 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
757 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
759 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
761 i = self._get_int(ctx.getText())
763 raise ParseException(f'Bad nth expression: {ctx.getText()}')
765 self.context['nth'] = i
768 self, ctx: dateparse_utilsParser.FirstOrLastContext
777 raise ParseException(
778 f'Bad first|last expression: {ctx.getText()}'
781 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
783 self.context['nth'] = txt
785 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
787 dow = ctx.getText().lower()[:3]
788 dow = self.day_name_to_number.get(dow, None)
790 raise ParseException('Bad day of week')
792 self.context['dow'] = dow
795 self, ctx: dateparse_utilsParser.DayOfMonthContext
798 day = ctx.getText().lower()
800 self.context['day'] = 'ide'
803 self.context['day'] = 'non'
806 self.context['day'] = 1
808 day = self._get_int(day)
809 if day < 1 or day > 31:
810 raise ParseException(
811 f'Bad dayOfMonth expression: {ctx.getText()}'
814 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
815 self.context['day'] = day
818 self, ctx: dateparse_utilsParser.MonthNameContext
821 month = ctx.getText()
822 while month[0] == '/' or month[0] == '-':
824 month = month[:3].lower()
825 month = self.month_name_to_number.get(month, None)
827 raise ParseException(
828 f'Bad monthName expression: {ctx.getText()}'
831 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
833 self.context['month'] = month
836 self, ctx: dateparse_utilsParser.MonthNumberContext
839 month = self._get_int(ctx.getText())
840 if month < 1 or month > 12:
841 raise ParseException(
842 f'Bad monthNumber expression: {ctx.getText()}'
845 raise ParseException(
846 f'Bad monthNumber expression: {ctx.getText()}'
849 self.context['month'] = month
851 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
853 year = self._get_int(ctx.getText())
855 raise ParseException(f'Bad year expression: {ctx.getText()}')
857 raise ParseException(f'Bad year expression: {ctx.getText()}')
859 self.saw_overt_year = True
860 self.context['year'] = year
862 def exitSpecialDateMaybeYearExpr(
863 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
866 special = ctx.specialDate().getText().lower()
867 self.context['special'] = special
869 raise ParseException(
870 f'Bad specialDate expression: {ctx.specialDate().getText()}'
873 mod = ctx.thisNextLast()
875 if mod.THIS() is not None:
876 self.context['special_next_last'] = 'this'
877 elif mod.NEXT() is not None:
878 self.context['special_next_last'] = 'next'
879 elif mod.LAST() is not None:
880 self.context['special_next_last'] = 'last'
882 raise ParseException(
883 f'Bad specialDateNextLast expression: {ctx.getText()}'
886 def exitNFoosFromTodayAgoExpr(
887 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
889 d = self.now_datetime
891 count = self._get_int(ctx.unsignedInt().getText())
892 unit = ctx.deltaUnit().getText().lower()
893 ago_from_now = ctx.AGO_FROM_NOW().getText()
895 raise ParseException(
896 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
899 if "ago" in ago_from_now or "back" in ago_from_now:
902 unit = self._figure_out_date_unit(unit)
903 d = n_timeunits_from_base(
907 self.context['year'] = d.year
908 self.context['month'] = d.month
909 self.context['day'] = d.day
911 def exitDeltaRelativeToTodayExpr(
912 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
914 d = self.now_datetime
916 mod = ctx.thisNextLast()
924 raise ParseException(
925 f'Bad This/Next/Last modifier: {mod}'
927 unit = ctx.deltaUnit().getText().lower()
929 raise ParseException(
930 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
932 unit = self._figure_out_date_unit(unit)
933 d = n_timeunits_from_base(
937 self.context['year'] = d.year
938 self.context['month'] = d.month
939 self.context['day'] = d.day
941 def exitSpecialTimeExpr(
942 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
945 txt = ctx.specialTime().getText().lower()
947 raise ParseException(
948 f'Bad special time expression: {ctx.getText()}'
951 if txt == 'noon' or txt == 'midday':
952 self.context['hour'] = 12
953 self.context['minute'] = 0
954 self.context['seconds'] = 0
955 self.context['micros'] = 0
956 elif txt == 'midnight':
957 self.context['hour'] = 0
958 self.context['minute'] = 0
959 self.context['seconds'] = 0
960 self.context['micros'] = 0
962 raise ParseException(f'Bad special time expression: {txt}')
965 tz = ctx.tzExpr().getText()
966 self.context['tz'] = self._parse_tz(tz)
970 def exitTwelveHourTimeExpr(
971 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
974 hour = ctx.hour().getText()
975 while not hour[-1].isdigit():
977 hour = self._get_int(hour)
979 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
980 if hour <= 0 or hour > 12:
981 raise ParseException(f'Bad hour (out of range): {hour}')
984 minute = self._get_int(ctx.minute().getText())
987 if minute < 0 or minute > 59:
988 raise ParseException(f'Bad minute (out of range): {minute}')
989 self.context['minute'] = minute
992 seconds = self._get_int(ctx.second().getText())
995 if seconds < 0 or seconds > 59:
996 raise ParseException(f'Bad second (out of range): {seconds}')
997 self.context['seconds'] = seconds
1000 micros = self._get_int(ctx.micros().getText())
1003 if micros < 0 or micros > 1000000:
1004 raise ParseException(f'Bad micros (out of range): {micros}')
1005 self.context['micros'] = micros
1008 ampm = ctx.ampm().getText()
1010 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
1015 self.context['hour'] = hour
1018 tz = ctx.tzExpr().getText()
1019 self.context['tz'] = self._parse_tz(tz)
1023 def exitTwentyFourHourTimeExpr(
1024 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1027 hour = ctx.hour().getText()
1028 while not hour[-1].isdigit():
1030 hour = self._get_int(hour)
1032 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
1033 if hour < 0 or hour > 23:
1034 raise ParseException(f'Bad hour (out of range): {hour}')
1035 self.context['hour'] = hour
1038 minute = self._get_int(ctx.minute().getText())
1041 if minute < 0 or minute > 59:
1042 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1043 self.context['minute'] = minute
1046 seconds = self._get_int(ctx.second().getText())
1049 if seconds < 0 or seconds > 59:
1050 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1051 self.context['seconds'] = seconds
1054 micros = self._get_int(ctx.micros().getText())
1057 if micros < 0 or micros >= 1000000:
1058 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1059 self.context['micros'] = micros
1062 tz = ctx.tzExpr().getText()
1063 self.context['tz'] = self._parse_tz(tz)
1068 @bootstrap.initialize
1070 parser = DateParser()
1071 for line in sys.stdin:
1073 line = re.sub(r"#.*$", "", line)
1074 if re.match(r"^ *$", line) is not None:
1077 dt = parser.parse(line)
1078 except Exception as e:
1079 print("Unrecognized.")
1081 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1085 if __name__ == "__main__":