5 import holidays # type: ignore
9 from typing import Any, Callable, Dict, Optional
11 import antlr4 # type: ignore
12 import dateutil.easter
18 from decorator_utils import decorate_matching_methods_with
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
24 logger = logging.getLogger(__name__)
27 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
28 @functools.wraps(enter_or_exit_f)
29 def debug_parse_wrapper(*args, **kwargs):
35 f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
37 for c in ctx.getChildren():
42 retval = enter_or_exit_f(*args, **kwargs)
44 return debug_parse_wrapper
47 class ParseException(Exception):
48 """An exception thrown during parsing because of unrecognized input."""
49 def __init__(self, message: str) -> None:
51 self.message = message
54 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
55 """An error listener that raises ParseExceptions."""
57 self, recognizer, offendingSymbol, line, column, msg, e
60 raise ParseException(msg)
63 self, recognizer, dfa, startIndex, stopIndex, exact,
68 def reportAttemptingFullContext(
69 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts,
74 def reportContextSensitivity(
75 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
80 @decorate_matching_methods_with(
82 acl=acl.StringWildcardBasedACL(
88 order_to_check_allow_deny=acl.ACL_ORDER_DENY_ALLOW,
92 class DateParser(dateparse_utilsListener):
93 PARSE_TYPE_SINGLE_DATE_EXPR = 1
94 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
95 PARSE_TYPE_SINGLE_TIME_EXPR = 3
96 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
101 override_now_for_test_purposes = None
103 """C'tor. Passing a value to override_now_for_test_purposes can be
104 used to force this instance to use a custom date/time for its
105 idea of "now" so that the code can be more easily unittested.
106 Leave as None for real use cases.
108 from datetime_utils import TimeUnit
109 self.month_name_to_number = {
124 # Used only for ides/nones. Month length on a non-leap year.
125 self.typical_days_per_month = {
140 # N.B. day number is also synched with datetime_utils.TimeUnit values
141 # which allows expressions like "3 wednesdays from now" to work.
142 self.day_name_to_number = {
152 # These TimeUnits are defined in datetime_utils and are used as params
153 # to datetime_utils.n_timeunits_from_base.
154 self.time_delta_unit_to_constant = {
155 'hou': TimeUnit.HOURS,
156 'min': TimeUnit.MINUTES,
157 'sec': TimeUnit.SECONDS,
159 self.delta_unit_to_constant = {
160 'day': TimeUnit.DAYS,
161 'wor': TimeUnit.WORKDAYS,
162 'wee': TimeUnit.WEEKS,
163 'mon': TimeUnit.MONTHS,
164 'yea': TimeUnit.YEARS,
166 self.override_now_for_test_purposes = override_now_for_test_purposes
169 def parse(self, date_string: str) -> Optional[datetime.datetime]:
170 """Parse a date/time expression and return a timezone agnostic
171 datetime on success. Also sets self.datetime, self.date and
172 self.time which can each be accessed other methods on the
173 class: get_datetime(), get_date() and get_time(). Raises a
174 ParseException with a helpful(?) message on parse error or
177 To get an idea of what expressions can be parsed, check out
178 the unittest and the grammar.
182 txt = '3 weeks before last tues at 9:15am'
185 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
187 # dt1 and dt2 will be identical other than the fact that
188 # the latter's tzinfo will be set to PST/PDT.
190 This is the main entrypoint to this class for caller code.
193 listener = RaisingErrorListener()
194 input_stream = antlr4.InputStream(date_string)
195 lexer = dateparse_utilsLexer(input_stream)
196 lexer.removeErrorListeners()
197 lexer.addErrorListener(listener)
198 stream = antlr4.CommonTokenStream(lexer)
199 parser = dateparse_utilsParser(stream)
200 parser.removeErrorListeners()
201 parser.addErrorListener(listener)
202 tree = parser.parse()
203 walker = antlr4.ParseTreeWalker()
204 walker.walk(self, tree)
207 def get_date(self) -> Optional[datetime.date]:
208 """Return the date part or None."""
211 def get_time(self) -> Optional[datetime.time]:
212 """Return the time part or None."""
215 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
216 """Return as a datetime. Parsed date expressions without any time
217 part return hours = minutes = seconds = microseconds = 0 (i.e. at
218 midnight that day). Parsed time expressions without any date part
219 default to date = today.
221 The optional tz param allows the caller to request the datetime be
222 timezone aware and sets the tzinfo to the indicated zone. Defaults
223 to timezone naive (i.e. tzinfo = None).
227 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
233 """Reset at init and between parses."""
234 from datetime_utils import datetime_to_date
235 if self.override_now_for_test_purposes is None:
236 self.now_datetime = datetime.datetime.now()
237 self.today = datetime.date.today()
239 self.now_datetime = self.override_now_for_test_purposes
240 self.today = datetime_to_date(
241 self.override_now_for_test_purposes
243 self.date: Optional[datetime.date] = None
244 self.time: Optional[datetime.time] = None
245 self.datetime: Optional[datetime.datetime] = None
246 self.context: Dict[str, Any] = {}
247 self.timedelta = datetime.timedelta(seconds=0)
250 def _normalize_special_day_name(name: str) -> str:
251 """String normalization / canonicalization for date expressions."""
253 name = name.replace("'", '')
254 name = name.replace('xmas', 'christmas')
255 name = name.replace('mlk', 'martin luther king')
256 name = name.replace(' ', '')
257 eve = 'eve' if name[-3:] == 'eve' else ''
258 name = name[:5] + eve
259 name = name.replace('washi', 'presi')
262 def _figure_out_date_unit(self, orig: str) -> int:
263 """Figure out what unit a date expression piece is talking about."""
264 from datetime_utils import TimeUnit
266 return TimeUnit.MONTHS
267 txt = orig.lower()[:3]
268 if txt in self.day_name_to_number:
269 return(self.day_name_to_number[txt])
270 elif txt in self.delta_unit_to_constant:
271 return(self.delta_unit_to_constant[txt])
272 raise ParseException(f'Invalid date unit: {orig}')
274 def _figure_out_time_unit(self, orig: str) -> int:
275 """Figure out what unit a time expression piece is talking about."""
276 txt = orig.lower()[:3]
277 if txt in self.time_delta_unit_to_constant:
278 return(self.time_delta_unit_to_constant[txt])
279 raise ParseException(f'Invalid time unit: {orig}')
281 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
282 """Parse what we think is a special date name and return its datetime
283 (or None if it can't be parsed).
286 year = self.context.get('year', today.year)
287 name = DateParser._normalize_special_day_name(self.context['special'])
289 # Yesterday, today, tomorrow -- ignore any next/last
290 if name == 'today' or name == 'now':
293 return today + datetime.timedelta(days=-1)
295 return today + datetime.timedelta(days=+1)
297 next_last = self.context.get('special_next_last', '')
298 if next_last == 'next':
300 elif next_last == 'last':
305 return dateutil.easter.easter(year=year)
306 elif name == 'hallo':
307 return datetime.date(year=year, month=10, day=31)
309 for holiday_date, holiday_name in sorted(
310 holidays.US(years=year).items()
312 if 'Observed' not in holiday_name:
313 holiday_name = DateParser._normalize_special_day_name(
316 if name == holiday_name:
318 if name == 'chriseve':
319 return datetime.date(year=year, month=12, day=24)
320 elif name == 'newyeeve':
321 return datetime.date(year=year, month=12, day=31)
324 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
325 """Handle date expressions like "the ides of March" which require
326 both the "ides" and the month since the definition of the "ides"
327 changes based on the length of the month.
329 assert 'ide' in day or 'non' in day
330 assert month_number in self.typical_days_per_month
331 typical_days_per_month = self.typical_days_per_month[month_number]
334 if typical_days_per_month == 31:
335 if self.context['day'] == 'ide':
342 if self.context['day'] == 'ide':
347 def _parse_normal_date(self) -> datetime.date:
348 if 'dow' in self.context:
350 while d.weekday() != self.context['dow']:
351 d += datetime.timedelta(days=1)
354 if 'month' not in self.context:
355 raise ParseException('Missing month')
356 if 'day' not in self.context:
357 raise ParseException('Missing day')
358 if 'year' not in self.context:
359 self.context['year'] = self.today.year
361 # Handling "ides" and "nones" requires both the day and month.
363 self.context['day'] == 'ide' or
364 self.context['day'] == 'non'
366 self.context['day'] = self._resolve_ides_nones(
367 self.context['day'], self.context['month']
370 return datetime.date(
371 year=self.context['year'],
372 month=self.context['month'],
373 day=self.context['day'],
376 def _parse_tz(self, txt: str) -> Any:
382 tz = pytz.timezone(txt)
390 tz = dateutil.tz.gettz(txt)
396 # Try constructing an offset in seconds
399 if sign == '-' or sign == '+':
400 sign = +1 if sign == '+' else -1
402 minute = int(txt[-2:])
403 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
404 tzoffset = dateutil.tz.tzoffset(txt, offset)
410 def _get_int(self, txt: str) -> int:
411 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
413 while not txt[-1].isdigit():
417 # -- overridden methods invoked by parse walk --
419 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
422 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
425 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
426 """Populate self.datetime."""
427 if self.date is None:
428 self.date = self.today
429 year = self.date.year
430 month = self.date.month
433 if self.time is None:
434 self.time = datetime.time(0, 0, 0)
435 hour = self.time.hour
436 minute = self.time.minute
437 second = self.time.second
438 micros = self.time.microsecond
440 self.datetime = datetime.datetime(
441 year, month, day, hour, minute, second, micros,
442 tzinfo=self.time.tzinfo
445 # Apply resudual adjustments to times here when we have a
447 self.datetime = self.datetime + self.timedelta
448 self.time = datetime.time(
450 self.datetime.minute,
451 self.datetime.second,
452 self.datetime.microsecond,
456 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
458 if ctx.singleDateExpr() is not None:
459 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
460 elif ctx.baseAndOffsetDateExpr() is not None:
461 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
463 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
465 if ctx.singleTimeExpr() is not None:
466 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
467 elif ctx.baseAndOffsetTimeExpr() is not None:
468 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
470 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
471 """When we leave the date expression, populate self.date."""
472 from datetime_utils import (
473 n_timeunits_from_base, datetime_to_date, date_to_datetime
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 from datetime_utils import TimeUnit
518 self.time = datetime.time(
519 self.context['hour'],
520 self.context['minute'],
521 self.context['seconds'],
522 self.context['micros'],
523 tzinfo=self.context.get('tz', None),
525 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
528 # If we get here there (should be) a relative adjustment to
530 if 'nth' in self.context:
531 count = self.context['nth']
532 elif 'time_delta_int' in self.context:
533 count = self.context['time_delta_int']
535 raise ParseException('Missing delta in relative time.')
539 # Adjust count's sign based on the presence of 'before' or 'after'.
540 if 'time_delta_before_after' in self.context:
541 before_after = self.context['time_delta_before_after'].lower()
543 before_after == 'before' or
544 before_after == 'until' or
545 before_after == 'til' or
550 # What are we counting units of... assume minutes.
551 if 'time_delta_unit' not in self.context:
552 self.timedelta += datetime.timedelta(minutes=count)
554 unit = self.context['time_delta_unit']
555 if unit == TimeUnit.SECONDS:
556 self.timedelta += datetime.timedelta(seconds=count)
557 elif unit == TimeUnit.MINUTES:
558 self.timedelta = datetime.timedelta(minutes=count)
559 elif unit == TimeUnit.HOURS:
560 self.timedelta = datetime.timedelta(hours=count)
562 raise ParseException()
564 def exitDeltaPlusMinusExpr(
565 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
570 raise ParseException(
571 f'Bad N in Delta +/- Expr: {ctx.getText()}'
575 unit = self._figure_out_date_unit(
576 ctx.deltaUnit().getText().lower()
579 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
581 self.context['delta_int'] = n
582 self.context['delta_unit'] = unit
584 def exitNextLastUnit(
585 self, ctx: dateparse_utilsParser.DeltaUnitContext
588 unit = self._figure_out_date_unit(ctx.getText().lower())
590 raise ParseException(f'Bad delta unit: {ctx.getText()}')
592 self.context['delta_unit'] = unit
594 def exitDeltaNextLast(
595 self, ctx: dateparse_utilsParser.DeltaNextLastContext
598 txt = ctx.getText().lower()
600 raise ParseException(f'Bad next/last: {ctx.getText()}')
602 'month' in self.context or
603 'day' in self.context or
604 'year' in self.context
606 raise ParseException(
607 'Next/last expression expected to be relative to today.'
609 if txt[:4] == 'next':
610 self.context['delta_int'] = +1
611 self.context['day'] = self.now_datetime.day
612 self.context['month'] = self.now_datetime.month
613 self.context['year'] = self.now_datetime.year
614 elif txt[:4] == 'last':
615 self.context['delta_int'] = -1
616 self.context['day'] = self.now_datetime.day
617 self.context['month'] = self.now_datetime.month
618 self.context['year'] = self.now_datetime.year
620 raise ParseException(f'Bad next/last: {ctx.getText()}')
622 def exitCountUnitsBeforeAfterTimeExpr(
623 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
625 if 'nth' not in self.context:
626 raise ParseException(
627 f'Bad count expression: {ctx.getText()}'
630 unit = self._figure_out_time_unit(
631 ctx.deltaTimeUnit().getText().lower()
633 self.context['time_delta_unit'] = unit
635 raise ParseException(f'Bad delta unit: {ctx.getText()}')
636 if 'time_delta_before_after' not in self.context:
637 raise ParseException(
638 f'Bad Before/After: {ctx.getText()}'
641 def exitDeltaTimeFraction(
642 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
644 from datetime_utils import TimeUnit
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 from datetime_utils import n_timeunits_from_base
878 d = self.now_datetime
880 count = self._get_int(ctx.unsignedInt().getText())
881 unit = ctx.deltaUnit().getText().lower()
882 ago_from_now = ctx.AGO_FROM_NOW().getText()
884 raise ParseException(
885 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
888 if "ago" in ago_from_now or "back" in ago_from_now:
891 unit = self._figure_out_date_unit(unit)
892 d = n_timeunits_from_base(
896 self.context['year'] = d.year
897 self.context['month'] = d.month
898 self.context['day'] = d.day
900 def exitDeltaRelativeToTodayExpr(
901 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
903 from datetime_utils import n_timeunits_from_base
904 d = self.now_datetime
906 mod = ctx.thisNextLast()
914 raise ParseException(
915 f'Bad This/Next/Last modifier: {mod}'
917 unit = ctx.deltaUnit().getText().lower()
919 raise ParseException(
920 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
922 unit = self._figure_out_date_unit(unit)
923 d = n_timeunits_from_base(
927 self.context['year'] = d.year
928 self.context['month'] = d.month
929 self.context['day'] = d.day
931 def exitSpecialTimeExpr(
932 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
935 txt = ctx.specialTime().getText().lower()
937 raise ParseException(
938 f'Bad special time expression: {ctx.getText()}'
941 if txt == 'noon' or txt == 'midday':
942 self.context['hour'] = 12
943 self.context['minute'] = 0
944 self.context['seconds'] = 0
945 self.context['micros'] = 0
946 elif txt == 'midnight':
947 self.context['hour'] = 0
948 self.context['minute'] = 0
949 self.context['seconds'] = 0
950 self.context['micros'] = 0
952 raise ParseException(f'Bad special time expression: {txt}')
955 tz = ctx.tzExpr().getText()
956 self.context['tz'] = self._parse_tz(tz)
960 def exitTwelveHourTimeExpr(
961 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
964 hour = ctx.hour().getText()
965 while not hour[-1].isdigit():
967 hour = self._get_int(hour)
969 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
970 if hour <= 0 or hour > 12:
971 raise ParseException(f'Bad hour (out of range): {hour}')
974 minute = self._get_int(ctx.minute().getText())
977 if minute < 0 or minute > 59:
978 raise ParseException(f'Bad minute (out of range): {minute}')
979 self.context['minute'] = minute
982 seconds = self._get_int(ctx.second().getText())
985 if seconds < 0 or seconds > 59:
986 raise ParseException(f'Bad second (out of range): {seconds}')
987 self.context['seconds'] = seconds
990 micros = self._get_int(ctx.micros().getText())
993 if micros < 0 or micros > 1000000:
994 raise ParseException(f'Bad micros (out of range): {micros}')
995 self.context['micros'] = micros
998 ampm = ctx.ampm().getText()
1000 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
1005 self.context['hour'] = hour
1008 tz = ctx.tzExpr().getText()
1009 self.context['tz'] = self._parse_tz(tz)
1013 def exitTwentyFourHourTimeExpr(
1014 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1017 hour = ctx.hour().getText()
1018 while not hour[-1].isdigit():
1020 hour = self._get_int(hour)
1022 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
1023 if hour < 0 or hour > 23:
1024 raise ParseException(f'Bad hour (out of range): {hour}')
1025 self.context['hour'] = hour
1028 minute = self._get_int(ctx.minute().getText())
1031 if minute < 0 or minute > 59:
1032 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1033 self.context['minute'] = minute
1036 seconds = self._get_int(ctx.second().getText())
1039 if seconds < 0 or seconds > 59:
1040 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1041 self.context['seconds'] = seconds
1044 micros = self._get_int(ctx.micros().getText())
1047 if micros < 0 or micros >= 1000000:
1048 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1049 self.context['micros'] = micros
1052 tz = ctx.tzExpr().getText()
1053 self.context['tz'] = self._parse_tz(tz)
1059 parser = DateParser()
1060 for line in sys.stdin:
1062 line = re.sub(r"#.*$", "", line)
1063 if re.match(r"^ *$", line) is not None:
1066 dt = parser.parse(line)
1067 except Exception as e:
1068 print("Unrecognized.")
1070 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1074 if __name__ == "__main__":
1075 main = bootstrap.initialize(main)