5 import holidays # type: ignore
9 from typing import Any, Callable, Dict, Optional
11 import antlr4 # type: ignore
12 import dateutil.easter
19 import decorator_utils
20 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
21 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
22 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
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(
83 acl=acl.StringWildcardBasedACL(
89 order_to_check_allow_deny=acl.ACL_ORDER_DENY_ALLOW,
93 class DateParser(dateparse_utilsListener):
94 PARSE_TYPE_SINGLE_DATE_EXPR = 1
95 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
96 PARSE_TYPE_SINGLE_TIME_EXPR = 3
97 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
102 override_now_for_test_purposes = None
104 """C'tor. Passing a value to override_now_for_test_purposes can be
105 used to force this instance to use a custom date/time for its
106 idea of "now" so that the code can be more easily unittested.
107 Leave as None for real use cases.
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': datetime_utils.TimeUnit.HOURS,
156 'min': datetime_utils.TimeUnit.MINUTES,
157 'sec': datetime_utils.TimeUnit.SECONDS,
159 self.delta_unit_to_constant = {
160 'day': datetime_utils.TimeUnit.DAYS,
161 'wor': datetime_utils.TimeUnit.WORKDAYS,
162 'wee': datetime_utils.TimeUnit.WEEKS,
163 'mon': datetime_utils.TimeUnit.MONTHS,
164 'yea': datetime_utils.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 if self.override_now_for_test_purposes is None:
235 self.now_datetime = datetime.datetime.now()
236 self.today = datetime.date.today()
238 self.now_datetime = self.override_now_for_test_purposes
239 self.today = datetime_utils.datetime_to_date(
240 self.override_now_for_test_purposes
242 self.date: Optional[datetime.date] = None
243 self.time: Optional[datetime.time] = None
244 self.datetime: Optional[datetime.datetime] = None
245 self.context: Dict[str, Any] = {}
246 self.timedelta = datetime.timedelta(seconds=0)
249 def _normalize_special_day_name(name: str) -> str:
250 """String normalization / canonicalization for date expressions."""
252 name = name.replace("'", '')
253 name = name.replace('xmas', 'christmas')
254 name = name.replace('mlk', 'martin luther king')
255 name = name.replace(' ', '')
256 eve = 'eve' if name[-3:] == 'eve' else ''
257 name = name[:5] + eve
258 name = name.replace('washi', 'presi')
261 def _figure_out_date_unit(self, orig: str) -> int:
262 """Figure out what unit a date expression piece is talking about."""
264 return datetime_utils.TimeUnit.MONTHS
265 txt = orig.lower()[:3]
266 if txt in self.day_name_to_number:
267 return(self.day_name_to_number[txt])
268 elif txt in self.delta_unit_to_constant:
269 return(self.delta_unit_to_constant[txt])
270 raise ParseException(f'Invalid date unit: {orig}')
272 def _figure_out_time_unit(self, orig: str) -> int:
273 """Figure out what unit a time expression piece is talking about."""
274 txt = orig.lower()[:3]
275 if txt in self.time_delta_unit_to_constant:
276 return(self.time_delta_unit_to_constant[txt])
277 raise ParseException(f'Invalid time unit: {orig}')
279 def _parse_special_date(self, name: str) -> Optional[datetime.date]:
280 """Parse what we think is a special date name and return its datetime
281 (or None if it can't be parsed).
284 year = self.context.get('year', today.year)
285 name = DateParser._normalize_special_day_name(self.context['special'])
287 # Yesterday, today, tomorrow -- ignore any next/last
288 if name == 'today' or name == 'now':
291 return today + datetime.timedelta(days=-1)
293 return today + datetime.timedelta(days=+1)
295 next_last = self.context.get('special_next_last', '')
296 if next_last == 'next':
298 elif next_last == 'last':
303 return dateutil.easter.easter(year=year)
304 elif name == 'hallo':
305 return datetime.date(year=year, month=10, day=31)
307 for holiday_date, holiday_name in sorted(
308 holidays.US(years=year).items()
310 if 'Observed' not in holiday_name:
311 holiday_name = DateParser._normalize_special_day_name(
314 if name == holiday_name:
316 if name == 'chriseve':
317 return datetime.date(year=year, month=12, day=24)
318 elif name == 'newyeeve':
319 return datetime.date(year=year, month=12, day=31)
322 def _resolve_ides_nones(self, day: str, month_number: int) -> int:
323 """Handle date expressions like "the ides of March" which require
324 both the "ides" and the month since the definition of the "ides"
325 changes based on the length of the month.
327 assert 'ide' in day or 'non' in day
328 assert month_number in self.typical_days_per_month
329 typical_days_per_month = self.typical_days_per_month[month_number]
332 if typical_days_per_month == 31:
333 if self.context['day'] == 'ide':
340 if self.context['day'] == 'ide':
345 def _parse_normal_date(self) -> datetime.date:
346 if 'dow' in self.context:
348 while d.weekday() != self.context['dow']:
349 d += datetime.timedelta(days=1)
352 if 'month' not in self.context:
353 raise ParseException('Missing month')
354 if 'day' not in self.context:
355 raise ParseException('Missing day')
356 if 'year' not in self.context:
357 self.context['year'] = self.today.year
359 # Handling "ides" and "nones" requires both the day and month.
361 self.context['day'] == 'ide' or
362 self.context['day'] == 'non'
364 self.context['day'] = self._resolve_ides_nones(
365 self.context['day'], self.context['month']
368 return datetime.date(
369 year=self.context['year'],
370 month=self.context['month'],
371 day=self.context['day'],
374 def _parse_tz(self, txt: str) -> Any:
380 tz = pytz.timezone(txt)
388 tz = dateutil.tz.gettz(txt)
394 # Try constructing an offset in seconds
397 if sign == '-' or sign == '+':
398 sign = +1 if sign == '+' else -1
400 minute = int(txt[-2:])
401 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
402 tzoffset = dateutil.tz.tzoffset(txt, offset)
408 def _get_int(self, txt: str) -> int:
409 while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
411 while not txt[-1].isdigit():
415 # -- overridden methods invoked by parse walk --
417 def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
420 def visitTerminal(self, node: antlr4.TerminalNode) -> None:
423 def exitParse(self, ctx: dateparse_utilsParser.ParseContext) -> None:
424 """Populate self.datetime."""
425 if self.date is None:
426 self.date = self.today
427 year = self.date.year
428 month = self.date.month
431 if self.time is None:
432 self.time = datetime.time(0, 0, 0)
433 hour = self.time.hour
434 minute = self.time.minute
435 second = self.time.second
436 micros = self.time.microsecond
438 self.datetime = datetime.datetime(
439 year, month, day, hour, minute, second, micros,
440 tzinfo=self.time.tzinfo
443 # Apply resudual adjustments to times here when we have a
445 self.datetime = self.datetime + self.timedelta
446 self.time = datetime.time(
448 self.datetime.minute,
449 self.datetime.second,
450 self.datetime.microsecond,
454 def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
456 if ctx.singleDateExpr() is not None:
457 self.main_type = DateParser.PARSE_TYPE_SINGLE_DATE_EXPR
458 elif ctx.baseAndOffsetDateExpr() is not None:
459 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
461 def enterTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext):
463 if ctx.singleTimeExpr() is not None:
464 self.time_type = DateParser.PARSE_TYPE_SINGLE_TIME_EXPR
465 elif ctx.baseAndOffsetTimeExpr() is not None:
466 self.time_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR
468 def exitDateExpr(self, ctx: dateparse_utilsParser.DateExprContext) -> None:
469 """When we leave the date expression, populate self.date."""
470 if 'special' in self.context:
471 self.date = self._parse_special_date(self.context['special'])
473 self.date = self._parse_normal_date()
474 assert self.date is not None
476 # For a single date, just return the date we pulled out.
477 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
480 # Otherwise treat self.date as a base date that we're modifying
482 if 'delta_int' not in self.context:
483 raise ParseException('Missing delta_int?!')
484 count = self.context['delta_int']
488 # Adjust count's sign based on the presence of 'before' or 'after'.
489 if 'delta_before_after' in self.context:
490 before_after = self.context['delta_before_after'].lower()
492 before_after == 'before' or
493 before_after == 'until' or
494 before_after == 'til' or
499 # What are we counting units of?
500 if 'delta_unit' not in self.context:
501 raise ParseException('Missing delta_unit?!')
502 unit = self.context['delta_unit']
503 dt = datetime_utils.n_timeunits_from_base(
506 datetime_utils.date_to_datetime(self.date)
508 self.date = datetime_utils.datetime_to_date(dt)
510 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
512 self.time = datetime.time(
513 self.context['hour'],
514 self.context['minute'],
515 self.context['seconds'],
516 self.context['micros'],
517 tzinfo=self.context.get('tz', None),
519 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
522 # If we get here there (should be) a relative adjustment to
524 if 'nth' in self.context:
525 count = self.context['nth']
526 elif 'time_delta_int' in self.context:
527 count = self.context['time_delta_int']
529 raise ParseException('Missing delta in relative time.')
533 # Adjust count's sign based on the presence of 'before' or 'after'.
534 if 'time_delta_before_after' in self.context:
535 before_after = self.context['time_delta_before_after'].lower()
537 before_after == 'before' or
538 before_after == 'until' or
539 before_after == 'til' or
544 # What are we counting units of... assume minutes.
545 if 'time_delta_unit' not in self.context:
546 self.timedelta += datetime.timedelta(minutes=count)
548 unit = self.context['time_delta_unit']
549 if unit == datetime_utils.TimeUnit.SECONDS:
550 self.timedelta += datetime.timedelta(seconds=count)
551 elif unit == datetime_utils.TimeUnit.MINUTES:
552 self.timedelta = datetime.timedelta(minutes=count)
553 elif unit == datetime_utils.TimeUnit.HOURS:
554 self.timedelta = datetime.timedelta(hours=count)
556 raise ParseException()
558 def exitDeltaPlusMinusExpr(
559 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
564 raise ParseException(
565 f'Bad N in Delta +/- Expr: {ctx.getText()}'
569 unit = self._figure_out_date_unit(
570 ctx.deltaUnit().getText().lower()
573 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
575 self.context['delta_int'] = n
576 self.context['delta_unit'] = unit
578 def exitNextLastUnit(
579 self, ctx: dateparse_utilsParser.DeltaUnitContext
582 unit = self._figure_out_date_unit(ctx.getText().lower())
584 raise ParseException(f'Bad delta unit: {ctx.getText()}')
586 self.context['delta_unit'] = unit
588 def exitDeltaNextLast(
589 self, ctx: dateparse_utilsParser.DeltaNextLastContext
592 txt = ctx.getText().lower()
594 raise ParseException(f'Bad next/last: {ctx.getText()}')
596 'month' in self.context or
597 'day' in self.context or
598 'year' in self.context
600 raise ParseException(
601 'Next/last expression expected to be relative to today.'
603 if txt[:4] == 'next':
604 self.context['delta_int'] = +1
605 self.context['day'] = self.now_datetime.day
606 self.context['month'] = self.now_datetime.month
607 self.context['year'] = self.now_datetime.year
608 elif txt[:4] == 'last':
609 self.context['delta_int'] = -1
610 self.context['day'] = self.now_datetime.day
611 self.context['month'] = self.now_datetime.month
612 self.context['year'] = self.now_datetime.year
614 raise ParseException(f'Bad next/last: {ctx.getText()}')
616 def exitCountUnitsBeforeAfterTimeExpr(
617 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
619 if 'nth' not in self.context:
620 raise ParseException(
621 f'Bad count expression: {ctx.getText()}'
624 unit = self._figure_out_time_unit(
625 ctx.deltaTimeUnit().getText().lower()
627 self.context['time_delta_unit'] = unit
629 raise ParseException(f'Bad delta unit: {ctx.getText()}')
630 if 'time_delta_before_after' not in self.context:
631 raise ParseException(
632 f'Bad Before/After: {ctx.getText()}'
635 def exitDeltaTimeFraction(
636 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
639 txt = ctx.getText().lower()[:4]
641 self.context['time_delta_int'] = 15
644 ] = datetime_utils.TimeUnit.MINUTES
646 self.context['time_delta_int'] = 30
649 ] = datetime_utils.TimeUnit.MINUTES
651 raise ParseException(f'Bad time fraction {ctx.getText()}')
653 raise ParseException(f'Bad time fraction {ctx.getText()}')
655 def exitDeltaBeforeAfter(
656 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
659 txt = ctx.getText().lower()
661 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
663 self.context['delta_before_after'] = txt
665 def exitDeltaTimeBeforeAfter(
666 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
669 txt = ctx.getText().lower()
671 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
673 self.context['time_delta_before_after'] = txt
675 def exitNthWeekdayInMonthMaybeYearExpr(
676 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
678 """Do a bunch of work to convert expressions like...
680 'the 2nd Friday of June' -and-
681 'the last Wednesday in October'
683 ...into base + offset expressions instead.
686 if 'nth' not in self.context:
687 raise ParseException(f'Missing nth number: {ctx.getText()}')
688 n = self.context['nth']
689 if n < 1 or n > 5: # months never have more than 5 Foodays
691 raise ParseException(f'Invalid nth number: {ctx.getText()}')
692 del self.context['nth']
693 self.context['delta_int'] = n
695 year = self.context.get('year', self.today.year)
696 if 'month' not in self.context:
697 raise ParseException(
698 f'Missing month expression: {ctx.getText()}'
700 month = self.context['month']
702 dow = self.context['dow']
703 del self.context['dow']
704 self.context['delta_unit'] = dow
706 # For the nth Fooday in Month, start at the 1st of the
707 # month and count ahead N Foodays. For the last Fooday in
708 # Month, start at the last of the month and count back one
715 tmp_date = datetime.date(year=year, month=month, day=1)
716 tmp_date = tmp_date - datetime.timedelta(days=1)
718 self.context['year'] = tmp_date.year
719 self.context['month'] = tmp_date.month
720 self.context['day'] = tmp_date.day
722 # The delta adjustment code can handle the case where
723 # the last day of the month is the day we're looking
726 self.context['year'] = year
727 self.context['month'] = month
728 self.context['day'] = 1
729 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
731 raise ParseException(
732 f'Invalid nthWeekday expression: {ctx.getText()}'
735 def exitFirstLastWeekdayInMonthMaybeYearExpr(
737 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
739 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
741 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
743 i = self._get_int(ctx.getText())
745 raise ParseException(f'Bad nth expression: {ctx.getText()}')
747 self.context['nth'] = i
750 self, ctx: dateparse_utilsParser.FirstOrLastContext
759 raise ParseException(
760 f'Bad first|last expression: {ctx.getText()}'
763 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
765 self.context['nth'] = txt
767 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
769 dow = ctx.getText().lower()[:3]
770 dow = self.day_name_to_number.get(dow, None)
772 raise ParseException('Bad day of week')
774 self.context['dow'] = dow
777 self, ctx: dateparse_utilsParser.DayOfMonthContext
780 day = ctx.getText().lower()
782 self.context['day'] = 'ide'
785 self.context['day'] = 'non'
788 self.context['day'] = 1
790 day = self._get_int(day)
791 if day < 1 or day > 31:
792 raise ParseException(
793 f'Bad dayOfMonth expression: {ctx.getText()}'
796 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
797 self.context['day'] = day
800 self, ctx: dateparse_utilsParser.MonthNameContext
803 month = ctx.getText()
804 while month[0] == '/' or month[0] == '-':
806 month = month[:3].lower()
807 month = self.month_name_to_number.get(month, None)
809 raise ParseException(
810 f'Bad monthName expression: {ctx.getText()}'
813 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
815 self.context['month'] = month
818 self, ctx: dateparse_utilsParser.MonthNumberContext
821 month = self._get_int(ctx.getText())
822 if month < 1 or month > 12:
823 raise ParseException(
824 f'Bad monthNumber expression: {ctx.getText()}'
827 raise ParseException(
828 f'Bad monthNumber expression: {ctx.getText()}'
831 self.context['month'] = month
833 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
835 year = self._get_int(ctx.getText())
837 raise ParseException(f'Bad year expression: {ctx.getText()}')
839 raise ParseException(f'Bad year expression: {ctx.getText()}')
841 self.context['year'] = year
843 def exitSpecialDateMaybeYearExpr(
844 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
847 special = ctx.specialDate().getText().lower()
848 self.context['special'] = special
850 raise ParseException(
851 f'Bad specialDate expression: {ctx.specialDate().getText()}'
854 mod = ctx.thisNextLast()
856 if mod.THIS() is not None:
857 self.context['special_next_last'] = 'this'
858 elif mod.NEXT() is not None:
859 self.context['special_next_last'] = 'next'
860 elif mod.LAST() is not None:
861 self.context['special_next_last'] = 'last'
863 raise ParseException(
864 f'Bad specialDateNextLast expression: {ctx.getText()}'
867 def exitNFoosFromTodayAgoExpr(
868 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
870 d = self.now_datetime
872 count = self._get_int(ctx.unsignedInt().getText())
873 unit = ctx.deltaUnit().getText().lower()
874 ago_from_now = ctx.AGO_FROM_NOW().getText()
876 raise ParseException(
877 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
880 if "ago" in ago_from_now or "back" in ago_from_now:
883 unit = self._figure_out_date_unit(unit)
884 d = datetime_utils.n_timeunits_from_base(
888 self.context['year'] = d.year
889 self.context['month'] = d.month
890 self.context['day'] = d.day
892 def exitDeltaRelativeToTodayExpr(
893 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
895 d = self.now_datetime
897 mod = ctx.thisNextLast()
905 raise ParseException(
906 f'Bad This/Next/Last modifier: {mod}'
908 unit = ctx.deltaUnit().getText().lower()
910 raise ParseException(
911 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
913 unit = self._figure_out_date_unit(unit)
914 d = datetime_utils.n_timeunits_from_base(
918 self.context['year'] = d.year
919 self.context['month'] = d.month
920 self.context['day'] = d.day
922 def exitSpecialTimeExpr(
923 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
926 txt = ctx.specialTime().getText().lower()
928 raise ParseException(
929 f'Bad special time expression: {ctx.getText()}'
932 if txt == 'noon' or txt == 'midday':
933 self.context['hour'] = 12
934 self.context['minute'] = 0
935 self.context['seconds'] = 0
936 self.context['micros'] = 0
937 elif txt == 'midnight':
938 self.context['hour'] = 0
939 self.context['minute'] = 0
940 self.context['seconds'] = 0
941 self.context['micros'] = 0
943 raise ParseException(f'Bad special time expression: {txt}')
946 tz = ctx.tzExpr().getText()
947 self.context['tz'] = self._parse_tz(tz)
951 def exitTwelveHourTimeExpr(
952 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
955 hour = ctx.hour().getText()
956 while not hour[-1].isdigit():
958 hour = self._get_int(hour)
960 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
961 if hour <= 0 or hour > 12:
962 raise ParseException(f'Bad hour (out of range): {hour}')
965 minute = self._get_int(ctx.minute().getText())
968 if minute < 0 or minute > 59:
969 raise ParseException(f'Bad minute (out of range): {minute}')
970 self.context['minute'] = minute
973 seconds = self._get_int(ctx.second().getText())
976 if seconds < 0 or seconds > 59:
977 raise ParseException(f'Bad second (out of range): {seconds}')
978 self.context['seconds'] = seconds
981 micros = self._get_int(ctx.micros().getText())
984 if micros < 0 or micros > 1000000:
985 raise ParseException(f'Bad micros (out of range): {micros}')
986 self.context['micros'] = micros
989 ampm = ctx.ampm().getText()
991 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
996 self.context['hour'] = hour
999 tz = ctx.tzExpr().getText()
1000 self.context['tz'] = self._parse_tz(tz)
1004 def exitTwentyFourHourTimeExpr(
1005 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1008 hour = ctx.hour().getText()
1009 while not hour[-1].isdigit():
1011 hour = self._get_int(hour)
1013 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
1014 if hour < 0 or hour > 23:
1015 raise ParseException(f'Bad hour (out of range): {hour}')
1016 self.context['hour'] = hour
1019 minute = self._get_int(ctx.minute().getText())
1022 if minute < 0 or minute > 59:
1023 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1024 self.context['minute'] = minute
1027 seconds = self._get_int(ctx.second().getText())
1030 if seconds < 0 or seconds > 59:
1031 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1032 self.context['seconds'] = seconds
1035 micros = self._get_int(ctx.micros().getText())
1038 if micros < 0 or micros >= 1000000:
1039 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1040 self.context['micros'] = micros
1043 tz = ctx.tzExpr().getText()
1044 self.context['tz'] = self._parse_tz(tz)
1050 parser = DateParser()
1051 for line in sys.stdin:
1053 line = re.sub(r"#.*$", "", line)
1054 if re.match(r"^ *$", line) is not None:
1057 dt = parser.parse(line)
1058 except Exception as e:
1059 print("Unrecognized.")
1061 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1065 if __name__ == "__main__":
1066 main = bootstrap.initialize(main)