5 import holidays # type: ignore
9 from typing import Any, Callable, Dict, Optional
11 import antlr4 # type: ignore
12 import dateutil.easter
18 from datetime_utils import (
19 TimeUnit, n_timeunits_from_base, datetime_to_date, date_to_datetime
21 from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
22 from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
23 from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
24 from decorator_utils import decorate_matching_methods_with
27 logger = logging.getLogger(__name__)
30 def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
31 @functools.wraps(enter_or_exit_f)
32 def debug_parse_wrapper(*args, **kwargs):
38 f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
40 for c in ctx.getChildren():
45 retval = enter_or_exit_f(*args, **kwargs)
47 return debug_parse_wrapper
50 class ParseException(Exception):
51 """An exception thrown during parsing because of unrecognized input."""
52 def __init__(self, message: str) -> None:
54 self.message = message
57 class RaisingErrorListener(antlr4.DiagnosticErrorListener):
58 """An error listener that raises ParseExceptions."""
60 self, recognizer, offendingSymbol, line, column, msg, e
63 raise ParseException(msg)
66 self, recognizer, dfa, startIndex, stopIndex, exact,
71 def reportAttemptingFullContext(
72 self, recognizer, dfa, startIndex, stopIndex, conflictingAlts,
77 def reportContextSensitivity(
78 self, recognizer, dfa, startIndex, stopIndex, prediction, configs
83 @decorate_matching_methods_with(
85 acl=acl.StringWildcardBasedACL(
91 order_to_check_allow_deny=acl.Order.DENY_ALLOW,
95 class DateParser(dateparse_utilsListener):
96 PARSE_TYPE_SINGLE_DATE_EXPR = 1
97 PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
98 PARSE_TYPE_SINGLE_TIME_EXPR = 3
99 PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
104 override_now_for_test_purposes = None
106 """C'tor. Passing a value to override_now_for_test_purposes can be
107 used to force this instance to use a custom date/time for its
108 idea of "now" so that the code can be more easily unittested.
109 Leave as None for real use cases.
111 self.month_name_to_number = {
126 # Used only for ides/nones. Month length on a non-leap year.
127 self.typical_days_per_month = {
142 # N.B. day number is also synched with datetime_utils.TimeUnit values
143 # which allows expressions like "3 wednesdays from now" to work.
144 self.day_name_to_number = {
154 # These TimeUnits are defined in datetime_utils and are used as params
155 # to datetime_utils.n_timeunits_from_base.
156 self.time_delta_unit_to_constant = {
157 'hou': TimeUnit.HOURS,
158 'min': TimeUnit.MINUTES,
159 'sec': TimeUnit.SECONDS,
161 self.delta_unit_to_constant = {
162 'day': TimeUnit.DAYS,
163 'wor': TimeUnit.WORKDAYS,
164 'wee': TimeUnit.WEEKS,
165 'mon': TimeUnit.MONTHS,
166 'yea': TimeUnit.YEARS,
168 self.override_now_for_test_purposes = override_now_for_test_purposes
171 def parse(self, date_string: str) -> Optional[datetime.datetime]:
172 """Parse a date/time expression and return a timezone agnostic
173 datetime on success. Also sets self.datetime, self.date and
174 self.time which can each be accessed other methods on the
175 class: get_datetime(), get_date() and get_time(). Raises a
176 ParseException with a helpful(?) message on parse error or
179 To get an idea of what expressions can be parsed, check out
180 the unittest and the grammar.
184 txt = '3 weeks before last tues at 9:15am'
187 dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
189 # dt1 and dt2 will be identical other than the fact that
190 # the latter's tzinfo will be set to PST/PDT.
192 This is the main entrypoint to this class for caller code.
195 listener = RaisingErrorListener()
196 input_stream = antlr4.InputStream(date_string)
197 lexer = dateparse_utilsLexer(input_stream)
198 lexer.removeErrorListeners()
199 lexer.addErrorListener(listener)
200 stream = antlr4.CommonTokenStream(lexer)
201 parser = dateparse_utilsParser(stream)
202 parser.removeErrorListeners()
203 parser.addErrorListener(listener)
204 tree = parser.parse()
205 walker = antlr4.ParseTreeWalker()
206 walker.walk(self, tree)
209 def get_date(self) -> Optional[datetime.date]:
210 """Return the date part or None."""
213 def get_time(self) -> Optional[datetime.time]:
214 """Return the time part or None."""
217 def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
218 """Return as a datetime. Parsed date expressions without any time
219 part return hours = minutes = seconds = microseconds = 0 (i.e. at
220 midnight that day). Parsed time expressions without any date part
221 default to date = today.
223 The optional tz param allows the caller to request the datetime be
224 timezone aware and sets the tzinfo to the indicated zone. Defaults
225 to timezone naive (i.e. tzinfo = None).
229 dt = dt.replace(tzinfo=None).astimezone(tz=tz)
235 """Reset at init and between parses."""
236 if self.override_now_for_test_purposes is None:
237 self.now_datetime = datetime.datetime.now()
238 self.today = datetime.date.today()
240 self.now_datetime = self.override_now_for_test_purposes
241 self.today = datetime_to_date(
242 self.override_now_for_test_purposes
244 self.date: Optional[datetime.date] = None
245 self.time: Optional[datetime.time] = None
246 self.datetime: Optional[datetime.datetime] = None
247 self.context: Dict[str, Any] = {}
248 self.timedelta = datetime.timedelta(seconds=0)
251 def _normalize_special_day_name(name: str) -> str:
252 """String normalization / canonicalization for date expressions."""
254 name = name.replace("'", '')
255 name = name.replace('xmas', 'christmas')
256 name = name.replace('mlk', 'martin luther king')
257 name = name.replace(' ', '')
258 eve = 'eve' if name[-3:] == 'eve' else ''
259 name = name[:5] + eve
260 name = name.replace('washi', 'presi')
263 def _figure_out_date_unit(self, orig: str) -> TimeUnit:
264 """Figure out what unit a date expression piece is talking about."""
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 if 'special' in self.context:
473 self.date = self._parse_special_date(self.context['special'])
475 self.date = self._parse_normal_date()
476 assert self.date is not None
478 # For a single date, just return the date we pulled out.
479 if self.main_type == DateParser.PARSE_TYPE_SINGLE_DATE_EXPR:
482 # Otherwise treat self.date as a base date that we're modifying
484 if 'delta_int' not in self.context:
485 raise ParseException('Missing delta_int?!')
486 count = self.context['delta_int']
490 # Adjust count's sign based on the presence of 'before' or 'after'.
491 if 'delta_before_after' in self.context:
492 before_after = self.context['delta_before_after'].lower()
494 before_after == 'before' or
495 before_after == 'until' or
496 before_after == 'til' or
501 # What are we counting units of?
502 if 'delta_unit' not in self.context:
503 raise ParseException('Missing delta_unit?!')
504 unit = self.context['delta_unit']
505 dt = n_timeunits_from_base(
508 date_to_datetime(self.date)
510 self.date = datetime_to_date(dt)
512 def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
514 self.time = datetime.time(
515 self.context['hour'],
516 self.context['minute'],
517 self.context['seconds'],
518 self.context['micros'],
519 tzinfo=self.context.get('tz', None),
521 if self.time_type == DateParser.PARSE_TYPE_SINGLE_TIME_EXPR:
524 # If we get here there (should be) a relative adjustment to
526 if 'nth' in self.context:
527 count = self.context['nth']
528 elif 'time_delta_int' in self.context:
529 count = self.context['time_delta_int']
531 raise ParseException('Missing delta in relative time.')
535 # Adjust count's sign based on the presence of 'before' or 'after'.
536 if 'time_delta_before_after' in self.context:
537 before_after = self.context['time_delta_before_after'].lower()
539 before_after == 'before' or
540 before_after == 'until' or
541 before_after == 'til' or
546 # What are we counting units of... assume minutes.
547 if 'time_delta_unit' not in self.context:
548 self.timedelta += datetime.timedelta(minutes=count)
550 unit = self.context['time_delta_unit']
551 if unit == TimeUnit.SECONDS:
552 self.timedelta += datetime.timedelta(seconds=count)
553 elif unit == TimeUnit.MINUTES:
554 self.timedelta = datetime.timedelta(minutes=count)
555 elif unit == TimeUnit.HOURS:
556 self.timedelta = datetime.timedelta(hours=count)
558 raise ParseException()
560 def exitDeltaPlusMinusExpr(
561 self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
566 raise ParseException(
567 f'Bad N in Delta +/- Expr: {ctx.getText()}'
571 unit = self._figure_out_date_unit(
572 ctx.deltaUnit().getText().lower()
575 raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
577 self.context['delta_int'] = n
578 self.context['delta_unit'] = unit
580 def exitNextLastUnit(
581 self, ctx: dateparse_utilsParser.DeltaUnitContext
584 unit = self._figure_out_date_unit(ctx.getText().lower())
586 raise ParseException(f'Bad delta unit: {ctx.getText()}')
588 self.context['delta_unit'] = unit
590 def exitDeltaNextLast(
591 self, ctx: dateparse_utilsParser.DeltaNextLastContext
594 txt = ctx.getText().lower()
596 raise ParseException(f'Bad next/last: {ctx.getText()}')
598 'month' in self.context or
599 'day' in self.context or
600 'year' in self.context
602 raise ParseException(
603 'Next/last expression expected to be relative to today.'
605 if txt[:4] == 'next':
606 self.context['delta_int'] = +1
607 self.context['day'] = self.now_datetime.day
608 self.context['month'] = self.now_datetime.month
609 self.context['year'] = self.now_datetime.year
610 elif txt[:4] == 'last':
611 self.context['delta_int'] = -1
612 self.context['day'] = self.now_datetime.day
613 self.context['month'] = self.now_datetime.month
614 self.context['year'] = self.now_datetime.year
616 raise ParseException(f'Bad next/last: {ctx.getText()}')
618 def exitCountUnitsBeforeAfterTimeExpr(
619 self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
621 if 'nth' not in self.context:
622 raise ParseException(
623 f'Bad count expression: {ctx.getText()}'
626 unit = self._figure_out_time_unit(
627 ctx.deltaTimeUnit().getText().lower()
629 self.context['time_delta_unit'] = unit
631 raise ParseException(f'Bad delta unit: {ctx.getText()}')
632 if 'time_delta_before_after' not in self.context:
633 raise ParseException(
634 f'Bad Before/After: {ctx.getText()}'
637 def exitDeltaTimeFraction(
638 self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
641 txt = ctx.getText().lower()[:4]
643 self.context['time_delta_int'] = 15
648 self.context['time_delta_int'] = 30
653 raise ParseException(f'Bad time fraction {ctx.getText()}')
655 raise ParseException(f'Bad time fraction {ctx.getText()}')
657 def exitDeltaBeforeAfter(
658 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
661 txt = ctx.getText().lower()
663 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
665 self.context['delta_before_after'] = txt
667 def exitDeltaTimeBeforeAfter(
668 self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
671 txt = ctx.getText().lower()
673 raise ParseException(f'Bad delta before|after: {ctx.getText()}')
675 self.context['time_delta_before_after'] = txt
677 def exitNthWeekdayInMonthMaybeYearExpr(
678 self, ctx: dateparse_utilsParser.NthWeekdayInMonthMaybeYearExprContext
680 """Do a bunch of work to convert expressions like...
682 'the 2nd Friday of June' -and-
683 'the last Wednesday in October'
685 ...into base + offset expressions instead.
688 if 'nth' not in self.context:
689 raise ParseException(f'Missing nth number: {ctx.getText()}')
690 n = self.context['nth']
691 if n < 1 or n > 5: # months never have more than 5 Foodays
693 raise ParseException(f'Invalid nth number: {ctx.getText()}')
694 del self.context['nth']
695 self.context['delta_int'] = n
697 year = self.context.get('year', self.today.year)
698 if 'month' not in self.context:
699 raise ParseException(
700 f'Missing month expression: {ctx.getText()}'
702 month = self.context['month']
704 dow = self.context['dow']
705 del self.context['dow']
706 self.context['delta_unit'] = dow
708 # For the nth Fooday in Month, start at the 1st of the
709 # month and count ahead N Foodays. For the last Fooday in
710 # Month, start at the last of the month and count back one
717 tmp_date = datetime.date(year=year, month=month, day=1)
718 tmp_date = tmp_date - datetime.timedelta(days=1)
720 self.context['year'] = tmp_date.year
721 self.context['month'] = tmp_date.month
722 self.context['day'] = tmp_date.day
724 # The delta adjustment code can handle the case where
725 # the last day of the month is the day we're looking
728 self.context['year'] = year
729 self.context['month'] = month
730 self.context['day'] = 1
731 self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
733 raise ParseException(
734 f'Invalid nthWeekday expression: {ctx.getText()}'
737 def exitFirstLastWeekdayInMonthMaybeYearExpr(
739 ctx: dateparse_utilsParser.FirstLastWeekdayInMonthMaybeYearExprContext,
741 self.exitNthWeekdayInMonthMaybeYearExpr(ctx)
743 def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
745 i = self._get_int(ctx.getText())
747 raise ParseException(f'Bad nth expression: {ctx.getText()}')
749 self.context['nth'] = i
752 self, ctx: dateparse_utilsParser.FirstOrLastContext
761 raise ParseException(
762 f'Bad first|last expression: {ctx.getText()}'
765 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
767 self.context['nth'] = txt
769 def exitDayName(self, ctx: dateparse_utilsParser.DayNameContext) -> None:
771 dow = ctx.getText().lower()[:3]
772 dow = self.day_name_to_number.get(dow, None)
774 raise ParseException('Bad day of week')
776 self.context['dow'] = dow
779 self, ctx: dateparse_utilsParser.DayOfMonthContext
782 day = ctx.getText().lower()
784 self.context['day'] = 'ide'
787 self.context['day'] = 'non'
790 self.context['day'] = 1
792 day = self._get_int(day)
793 if day < 1 or day > 31:
794 raise ParseException(
795 f'Bad dayOfMonth expression: {ctx.getText()}'
798 raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
799 self.context['day'] = day
802 self, ctx: dateparse_utilsParser.MonthNameContext
805 month = ctx.getText()
806 while month[0] == '/' or month[0] == '-':
808 month = month[:3].lower()
809 month = self.month_name_to_number.get(month, None)
811 raise ParseException(
812 f'Bad monthName expression: {ctx.getText()}'
815 raise ParseException(f'Bad monthName expression: {ctx.getText()}')
817 self.context['month'] = month
820 self, ctx: dateparse_utilsParser.MonthNumberContext
823 month = self._get_int(ctx.getText())
824 if month < 1 or month > 12:
825 raise ParseException(
826 f'Bad monthNumber expression: {ctx.getText()}'
829 raise ParseException(
830 f'Bad monthNumber expression: {ctx.getText()}'
833 self.context['month'] = month
835 def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
837 year = self._get_int(ctx.getText())
839 raise ParseException(f'Bad year expression: {ctx.getText()}')
841 raise ParseException(f'Bad year expression: {ctx.getText()}')
843 self.context['year'] = year
845 def exitSpecialDateMaybeYearExpr(
846 self, ctx: dateparse_utilsParser.SpecialDateMaybeYearExprContext
849 special = ctx.specialDate().getText().lower()
850 self.context['special'] = special
852 raise ParseException(
853 f'Bad specialDate expression: {ctx.specialDate().getText()}'
856 mod = ctx.thisNextLast()
858 if mod.THIS() is not None:
859 self.context['special_next_last'] = 'this'
860 elif mod.NEXT() is not None:
861 self.context['special_next_last'] = 'next'
862 elif mod.LAST() is not None:
863 self.context['special_next_last'] = 'last'
865 raise ParseException(
866 f'Bad specialDateNextLast expression: {ctx.getText()}'
869 def exitNFoosFromTodayAgoExpr(
870 self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
872 d = self.now_datetime
874 count = self._get_int(ctx.unsignedInt().getText())
875 unit = ctx.deltaUnit().getText().lower()
876 ago_from_now = ctx.AGO_FROM_NOW().getText()
878 raise ParseException(
879 f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
882 if "ago" in ago_from_now or "back" in ago_from_now:
885 unit = self._figure_out_date_unit(unit)
886 d = n_timeunits_from_base(
890 self.context['year'] = d.year
891 self.context['month'] = d.month
892 self.context['day'] = d.day
894 def exitDeltaRelativeToTodayExpr(
895 self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
897 d = self.now_datetime
899 mod = ctx.thisNextLast()
907 raise ParseException(
908 f'Bad This/Next/Last modifier: {mod}'
910 unit = ctx.deltaUnit().getText().lower()
912 raise ParseException(
913 f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
915 unit = self._figure_out_date_unit(unit)
916 d = n_timeunits_from_base(
920 self.context['year'] = d.year
921 self.context['month'] = d.month
922 self.context['day'] = d.day
924 def exitSpecialTimeExpr(
925 self, ctx: dateparse_utilsParser.SpecialTimeExprContext
928 txt = ctx.specialTime().getText().lower()
930 raise ParseException(
931 f'Bad special time expression: {ctx.getText()}'
934 if txt == 'noon' or txt == 'midday':
935 self.context['hour'] = 12
936 self.context['minute'] = 0
937 self.context['seconds'] = 0
938 self.context['micros'] = 0
939 elif txt == 'midnight':
940 self.context['hour'] = 0
941 self.context['minute'] = 0
942 self.context['seconds'] = 0
943 self.context['micros'] = 0
945 raise ParseException(f'Bad special time expression: {txt}')
948 tz = ctx.tzExpr().getText()
949 self.context['tz'] = self._parse_tz(tz)
953 def exitTwelveHourTimeExpr(
954 self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
957 hour = ctx.hour().getText()
958 while not hour[-1].isdigit():
960 hour = self._get_int(hour)
962 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
963 if hour <= 0 or hour > 12:
964 raise ParseException(f'Bad hour (out of range): {hour}')
967 minute = self._get_int(ctx.minute().getText())
970 if minute < 0 or minute > 59:
971 raise ParseException(f'Bad minute (out of range): {minute}')
972 self.context['minute'] = minute
975 seconds = self._get_int(ctx.second().getText())
978 if seconds < 0 or seconds > 59:
979 raise ParseException(f'Bad second (out of range): {seconds}')
980 self.context['seconds'] = seconds
983 micros = self._get_int(ctx.micros().getText())
986 if micros < 0 or micros > 1000000:
987 raise ParseException(f'Bad micros (out of range): {micros}')
988 self.context['micros'] = micros
991 ampm = ctx.ampm().getText()
993 raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
998 self.context['hour'] = hour
1001 tz = ctx.tzExpr().getText()
1002 self.context['tz'] = self._parse_tz(tz)
1006 def exitTwentyFourHourTimeExpr(
1007 self, ctx: dateparse_utilsParser.TwentyFourHourTimeExprContext
1010 hour = ctx.hour().getText()
1011 while not hour[-1].isdigit():
1013 hour = self._get_int(hour)
1015 raise ParseException(f'Bad hour: {ctx.hour().getText()}')
1016 if hour < 0 or hour > 23:
1017 raise ParseException(f'Bad hour (out of range): {hour}')
1018 self.context['hour'] = hour
1021 minute = self._get_int(ctx.minute().getText())
1024 if minute < 0 or minute > 59:
1025 raise ParseException(f'Bad minute (out of range): {ctx.getText()}')
1026 self.context['minute'] = minute
1029 seconds = self._get_int(ctx.second().getText())
1032 if seconds < 0 or seconds > 59:
1033 raise ParseException(f'Bad second (out of range): {ctx.getText()}')
1034 self.context['seconds'] = seconds
1037 micros = self._get_int(ctx.micros().getText())
1040 if micros < 0 or micros >= 1000000:
1041 raise ParseException(f'Bad micros (out of range): {ctx.getText()}')
1042 self.context['micros'] = micros
1045 tz = ctx.tzExpr().getText()
1046 self.context['tz'] = self._parse_tz(tz)
1051 @bootstrap.initialize
1053 parser = DateParser()
1054 for line in sys.stdin:
1056 line = re.sub(r"#.*$", "", line)
1057 if re.match(r"^ *$", line) is not None:
1060 dt = parser.parse(line)
1061 except Exception as e:
1062 print("Unrecognized.")
1064 print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
1068 if __name__ == "__main__":