#!/usr/bin/env python3
+# type: ignore
+# pylint: disable=W0201
+# pylint: disable=R0904
+
+# © Copyright 2021-2022, Scott Gasch
+
+"""Parse dates in a variety of formats."""
import datetime
import functools
-import holidays # type: ignore
import logging
import re
import sys
import antlr4 # type: ignore
import dateutil.easter
import dateutil.tz
+import holidays # type: ignore
import pytz
import acl
import bootstrap
-from datetime_utils import (
- TimeUnit, n_timeunits_from_base, datetime_to_date, date_to_datetime
-)
+import decorator_utils
from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
-import decorator_utils
-
+from datetime_utils import (
+ TimeUnit,
+ date_to_datetime,
+ datetime_to_date,
+ n_timeunits_from_base,
+)
logger = logging.getLogger(__name__)
def debug_parse(enter_or_exit_f: Callable[[Any, Any], None]):
@functools.wraps(enter_or_exit_f)
def debug_parse_wrapper(*args, **kwargs):
- slf = args[0]
+ # slf = args[0]
ctx = args[1]
depth = ctx.depth()
logger.debug(
- ' ' * (depth-1) +
- f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
+ ' ' * (depth - 1)
+ + f'Entering {enter_or_exit_f.__name__} ({ctx.invokingState} / {ctx.exception})'
)
for c in ctx.getChildren():
- logger.debug(
- ' ' * (depth-1) +
- f'{c} {type(c)}'
- )
+ logger.debug(' ' * (depth - 1) + f'{c} {type(c)}')
retval = enter_or_exit_f(*args, **kwargs)
return retval
+
return debug_parse_wrapper
class ParseException(Exception):
"""An exception thrown during parsing because of unrecognized input."""
+
def __init__(self, message: str) -> None:
- logger.error(message)
+ super().__init__()
self.message = message
class RaisingErrorListener(antlr4.DiagnosticErrorListener):
"""An error listener that raises ParseExceptions."""
- def syntaxError(
- self, recognizer, offendingSymbol, line, column, msg, e
- ):
- logger.error(msg)
+
+ def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
raise ParseException(msg)
- def reportAmbiguity(
- self, recognizer, dfa, startIndex, stopIndex, exact,
- ambigAlts, configs
- ):
+ def reportAmbiguity(self, recognizer, dfa, startIndex, stopIndex, exact, ambigAlts, configs):
pass
def reportAttemptingFullContext(
- self, recognizer, dfa, startIndex, stopIndex, conflictingAlts,
- configs
+ self, recognizer, dfa, startIndex, stopIndex, conflictingAlts, configs
):
pass
- def reportContextSensitivity(
- self, recognizer, dfa, startIndex, stopIndex, prediction, configs
- ):
+ def reportContextSensitivity(self, recognizer, dfa, startIndex, stopIndex, prediction, configs):
pass
'enter*',
'exit*',
],
- denied_patterns=[
- 'enterEveryRule',
- 'exitEveryRule'
- ],
+ denied_patterns=['enterEveryRule', 'exitEveryRule'],
order_to_check_allow_deny=acl.Order.DENY_ALLOW,
- default_answer=False
- )
+ default_answer=False,
+ ),
)
class DateParser(dateparse_utilsListener):
+ """A class to parse dates expressed in human language."""
+
PARSE_TYPE_SINGLE_DATE_EXPR = 1
PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
PARSE_TYPE_SINGLE_TIME_EXPR = 3
PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
- def __init__(
- self,
- *,
- override_now_for_test_purposes = None
- ) -> None:
+ def __init__(self, *, override_now_for_test_purposes=None) -> None:
"""C'tor. Passing a value to override_now_for_test_purposes can be
used to force this instance to use a custom date/time for its
idea of "now" so that the code can be more easily unittested.
9: 30,
10: 31,
11: 30,
- 12: 31
+ 12: 31,
}
# N.B. day number is also synched with datetime_utils.TimeUnit values
'yea': TimeUnit.YEARS,
}
self.override_now_for_test_purposes = override_now_for_test_purposes
+
+ # Note: _reset defines several class fields. It is used both here
+ # in the c'tor but also in between parse operations to restore the
+ # class' state and allow it to be reused.
+ #
self._reset()
def parse(self, date_string: str) -> Optional[datetime.datetime]:
This is the main entrypoint to this class for caller code.
"""
date_string = date_string.strip()
- date_string = re.sub('\s+', ' ', date_string)
+ date_string = re.sub(r'\s+', ' ', date_string)
self._reset()
listener = RaisingErrorListener()
input_stream = antlr4.InputStream(date_string)
to timezone naive (i.e. tzinfo = None).
"""
dt = self.datetime
- if tz is not None:
- dt = dt.replace(tzinfo=None).astimezone(tz=tz)
+ if dt is not None:
+ if tz is not None:
+ dt = dt.replace(tzinfo=None).astimezone(tz=tz)
return dt
# -- helpers --
self.today = datetime.date.today()
else:
self.now_datetime = self.override_now_for_test_purposes
- self.today = datetime_to_date(
- self.override_now_for_test_purposes
- )
+ self.today = datetime_to_date(self.override_now_for_test_purposes)
self.date: Optional[datetime.date] = None
self.time: Optional[datetime.time] = None
self.datetime: Optional[datetime.datetime] = None
self.context: Dict[str, Any] = {}
self.timedelta = datetime.timedelta(seconds=0)
+ self.saw_overt_year = False
@staticmethod
def _normalize_special_day_name(name: str) -> str:
return TimeUnit.MONTHS
txt = orig.lower()[:3]
if txt in self.day_name_to_number:
- return(TimeUnit(self.day_name_to_number[txt]))
+ return TimeUnit(self.day_name_to_number[txt])
elif txt in self.delta_unit_to_constant:
- return(TimeUnit(self.delta_unit_to_constant[txt]))
+ return TimeUnit(self.delta_unit_to_constant[txt])
raise ParseException(f'Invalid date unit: {orig}')
def _figure_out_time_unit(self, orig: str) -> int:
"""Figure out what unit a time expression piece is talking about."""
txt = orig.lower()[:3]
if txt in self.time_delta_unit_to_constant:
- return(self.time_delta_unit_to_constant[txt])
+ return self.time_delta_unit_to_constant[txt]
raise ParseException(f'Invalid time unit: {orig}')
def _parse_special_date(self, name: str) -> Optional[datetime.date]:
name = DateParser._normalize_special_day_name(self.context['special'])
# Yesterday, today, tomorrow -- ignore any next/last
- if name == 'today' or name == 'now':
+ if name in ('today', 'now'):
return today
if name == 'yeste':
return today + datetime.timedelta(days=-1)
next_last = self.context.get('special_next_last', '')
if next_last == 'next':
year += 1
+ self.saw_overt_year = True
elif next_last == 'last':
year -= 1
+ self.saw_overt_year = True
# Holiday names
if name == 'easte':
elif name == 'hallo':
return datetime.date(year=year, month=10, day=31)
- for holiday_date, holiday_name in sorted(
- holidays.US(years=year).items()
- ):
+ for holiday_date, holiday_name in sorted(holidays.US(years=year).items()):
if 'Observed' not in holiday_name:
- holiday_name = DateParser._normalize_special_day_name(
- holiday_name
- )
+ holiday_name = DateParser._normalize_special_day_name(holiday_name)
if name == holiday_name:
return holiday_date
if name == 'chriseve':
raise ParseException('Missing day')
if 'year' not in self.context:
self.context['year'] = self.today.year
+ self.saw_overt_year = False
+ else:
+ self.saw_overt_year = True
# Handling "ides" and "nones" requires both the day and month.
- if (
- self.context['day'] == 'ide' or
- self.context['day'] == 'non'
- ):
+ if self.context['day'] == 'ide' or self.context['day'] == 'non':
self.context['day'] = self._resolve_ides_nones(
self.context['day'], self.context['month']
)
day=self.context['day'],
)
- def _parse_tz(self, txt: str) -> Any:
+ @staticmethod
+ def _parse_tz(txt: str) -> Any:
if txt == 'Z':
txt = 'UTC'
# Try pytz
try:
- tz = pytz.timezone(txt)
- if tz is not None:
- return tz
+ tz1 = pytz.timezone(txt)
+ if tz1 is not None:
+ return tz1
except Exception:
pass
# Try dateutil
try:
- tz = dateutil.tz.gettz(txt)
- if tz is not None:
- return tz
+ tz2 = dateutil.tz.gettz(txt)
+ if tz2 is not None:
+ return tz2
except Exception:
pass
# Try constructing an offset in seconds
try:
- sign = txt[0]
- if sign == '-' or sign == '+':
- sign = +1 if sign == '+' else -1
+ txt_sign = txt[0]
+ if txt_sign in ('-', '+'):
+ sign = +1 if txt_sign == '+' else -1
hour = int(txt[1:3])
minute = int(txt[-2:])
offset = sign * (hour * 60 * 60) + sign * (minute * 60)
pass
return None
- def _get_int(self, txt: str) -> int:
+ @staticmethod
+ def _get_int(txt: str) -> int:
while not txt[0].isdigit() and txt[0] != '-' and txt[0] != '+':
txt = txt[1:]
while not txt[-1].isdigit():
txt = txt[:-1]
return int(txt)
- # -- overridden methods invoked by parse walk --
+ # -- overridden methods invoked by parse walk. Note: not part of the class'
+ # public API(!!) --
def visitErrorNode(self, node: antlr4.ErrorNode) -> None:
pass
micros = self.time.microsecond
self.datetime = datetime.datetime(
- year, month, day, hour, minute, second, micros,
- tzinfo=self.time.tzinfo
+ year,
+ month,
+ day,
+ hour,
+ minute,
+ second,
+ micros,
+ tzinfo=self.time.tzinfo,
)
# Apply resudual adjustments to times here when we have a
# datetime.
self.datetime = self.datetime + self.timedelta
+ assert self.datetime is not None
self.time = datetime.time(
self.datetime.hour,
self.datetime.minute,
self.datetime.second,
self.datetime.microsecond,
- self.datetime.tzinfo
+ self.datetime.tzinfo,
)
def enterDateExpr(self, ctx: dateparse_utilsParser.DateExprContext):
# Adjust count's sign based on the presence of 'before' or 'after'.
if 'delta_before_after' in self.context:
before_after = self.context['delta_before_after'].lower()
- if (
- before_after == 'before' or
- before_after == 'until' or
- before_after == 'til' or
- before_after == 'to'
- ):
+ if before_after in ('before', 'until', 'til', 'to'):
count = -count
# What are we counting units of?
if 'delta_unit' not in self.context:
raise ParseException('Missing delta_unit?!')
unit = self.context['delta_unit']
- dt = n_timeunits_from_base(
- count,
- TimeUnit(unit),
- date_to_datetime(self.date)
- )
+ dt = n_timeunits_from_base(count, TimeUnit(unit), date_to_datetime(self.date))
self.date = datetime_to_date(dt)
def exitTimeExpr(self, ctx: dateparse_utilsParser.TimeExprContext) -> None:
# Adjust count's sign based on the presence of 'before' or 'after'.
if 'time_delta_before_after' in self.context:
before_after = self.context['time_delta_before_after'].lower()
- if (
- before_after == 'before' or
- before_after == 'until' or
- before_after == 'til' or
- before_after == 'to'
- ):
+ if before_after in ('before', 'until', 'til', 'to'):
count = -count
# What are we counting units of... assume minutes.
elif unit == TimeUnit.HOURS:
self.timedelta = datetime.timedelta(hours=count)
else:
- raise ParseException()
+ raise ParseException(f'Invalid Unit: "{unit}"')
- def exitDeltaPlusMinusExpr(
- self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
- ) -> None:
+ def exitDeltaPlusMinusExpr(self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext) -> None:
try:
n = ctx.nth()
if n is None:
- raise ParseException(
- f'Bad N in Delta +/- Expr: {ctx.getText()}'
- )
+ raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
n = n.getText()
- n = self._get_int(n)
- unit = self._figure_out_date_unit(
- ctx.deltaUnit().getText().lower()
- )
- except Exception:
- raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
+ n = DateParser._get_int(n)
+ unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
+ except Exception as e:
+ raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
else:
self.context['delta_int'] = n
self.context['delta_unit'] = unit
- def exitNextLastUnit(
- self, ctx: dateparse_utilsParser.DeltaUnitContext
- ) -> None:
+ def exitNextLastUnit(self, ctx: dateparse_utilsParser.DeltaUnitContext) -> None:
try:
unit = self._figure_out_date_unit(ctx.getText().lower())
- except Exception:
- raise ParseException(f'Bad delta unit: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
else:
self.context['delta_unit'] = unit
- def exitDeltaNextLast(
- self, ctx: dateparse_utilsParser.DeltaNextLastContext
- ) -> None:
+ def exitDeltaNextLast(self, ctx: dateparse_utilsParser.DeltaNextLastContext) -> None:
try:
txt = ctx.getText().lower()
- except Exception:
- raise ParseException(f'Bad next/last: {ctx.getText()}')
- if (
- 'month' in self.context or
- 'day' in self.context or
- 'year' in self.context
- ):
- raise ParseException(
- 'Next/last expression expected to be relative to today.'
- )
+ except Exception as e:
+ raise ParseException(f'Bad next/last: {ctx.getText()}') from e
+ if 'month' in self.context or 'day' in self.context or 'year' in self.context:
+ raise ParseException('Next/last expression expected to be relative to today.')
if txt[:4] == 'next':
self.context['delta_int'] = +1
self.context['day'] = self.now_datetime.day
self.context['month'] = self.now_datetime.month
self.context['year'] = self.now_datetime.year
+ self.saw_overt_year = True
elif txt[:4] == 'last':
self.context['delta_int'] = -1
self.context['day'] = self.now_datetime.day
self.context['month'] = self.now_datetime.month
self.context['year'] = self.now_datetime.year
+ self.saw_overt_year = True
else:
raise ParseException(f'Bad next/last: {ctx.getText()}')
self, ctx: dateparse_utilsParser.CountUnitsBeforeAfterTimeExprContext
) -> None:
if 'nth' not in self.context:
- raise ParseException(
- f'Bad count expression: {ctx.getText()}'
- )
+ raise ParseException(f'Bad count expression: {ctx.getText()}')
try:
- unit = self._figure_out_time_unit(
- ctx.deltaTimeUnit().getText().lower()
- )
+ unit = self._figure_out_time_unit(ctx.deltaTimeUnit().getText().lower())
self.context['time_delta_unit'] = unit
- except Exception:
- raise ParseException(f'Bad delta unit: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad delta unit: {ctx.getText()}') from e
if 'time_delta_before_after' not in self.context:
- raise ParseException(
- f'Bad Before/After: {ctx.getText()}'
- )
+ raise ParseException(f'Bad Before/After: {ctx.getText()}')
- def exitDeltaTimeFraction(
- self, ctx: dateparse_utilsParser.DeltaTimeFractionContext
- ) -> None:
+ def exitDeltaTimeFraction(self, ctx: dateparse_utilsParser.DeltaTimeFractionContext) -> None:
try:
txt = ctx.getText().lower()[:4]
if txt == 'quar':
self.context['time_delta_int'] = 15
- self.context[
- 'time_delta_unit'
- ] = TimeUnit.MINUTES
+ self.context['time_delta_unit'] = TimeUnit.MINUTES
elif txt == 'half':
self.context['time_delta_int'] = 30
- self.context[
- 'time_delta_unit'
- ] = TimeUnit.MINUTES
+ self.context['time_delta_unit'] = TimeUnit.MINUTES
else:
raise ParseException(f'Bad time fraction {ctx.getText()}')
- except Exception:
- raise ParseException(f'Bad time fraction {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad time fraction {ctx.getText()}') from e
- def exitDeltaBeforeAfter(
- self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
- ) -> None:
+ def exitDeltaBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
try:
txt = ctx.getText().lower()
- except Exception:
- raise ParseException(f'Bad delta before|after: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
else:
self.context['delta_before_after'] = txt
- def exitDeltaTimeBeforeAfter(
- self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext
- ) -> None:
+ def exitDeltaTimeBeforeAfter(self, ctx: dateparse_utilsParser.DeltaBeforeAfterContext) -> None:
try:
txt = ctx.getText().lower()
- except Exception:
- raise ParseException(f'Bad delta before|after: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad delta before|after: {ctx.getText()}') from e
else:
self.context['time_delta_before_after'] = txt
year = self.context.get('year', self.today.year)
if 'month' not in self.context:
- raise ParseException(
- f'Missing month expression: {ctx.getText()}'
- )
+ raise ParseException(f'Missing month expression: {ctx.getText()}')
month = self.context['month']
dow = self.context['dow']
self.context['month'] = month
self.context['day'] = 1
self.main_type = DateParser.PARSE_TYPE_BASE_AND_OFFSET_EXPR
- except Exception:
- raise ParseException(
- f'Invalid nthWeekday expression: {ctx.getText()}'
- )
+ except Exception as e:
+ raise ParseException(f'Invalid nthWeekday expression: {ctx.getText()}') from e
def exitFirstLastWeekdayInMonthMaybeYearExpr(
self,
def exitNth(self, ctx: dateparse_utilsParser.NthContext) -> None:
try:
- i = self._get_int(ctx.getText())
- except Exception:
- raise ParseException(f'Bad nth expression: {ctx.getText()}')
+ i = DateParser._get_int(ctx.getText())
+ except Exception as e:
+ raise ParseException(f'Bad nth expression: {ctx.getText()}') from e
else:
self.context['nth'] = i
- def exitFirstOrLast(
- self, ctx: dateparse_utilsParser.FirstOrLastContext
- ) -> None:
+ def exitFirstOrLast(self, ctx: dateparse_utilsParser.FirstOrLastContext) -> None:
try:
txt = ctx.getText()
if txt == 'first':
elif txt == 'last':
txt = -1
else:
- raise ParseException(
- f'Bad first|last expression: {ctx.getText()}'
- )
- except Exception:
- raise ParseException(f'Bad first|last expression: {ctx.getText()}')
+ raise ParseException(f'Bad first|last expression: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad first|last expression: {ctx.getText()}') from e
else:
self.context['nth'] = txt
try:
dow = ctx.getText().lower()[:3]
dow = self.day_name_to_number.get(dow, None)
- except Exception:
- raise ParseException('Bad day of week')
+ except Exception as e:
+ raise ParseException('Bad day of week') from e
else:
self.context['dow'] = dow
- def exitDayOfMonth(
- self, ctx: dateparse_utilsParser.DayOfMonthContext
- ) -> None:
+ def exitDayOfMonth(self, ctx: dateparse_utilsParser.DayOfMonthContext) -> None:
try:
day = ctx.getText().lower()
if day[:3] == 'ide':
if day[:3] == 'kal':
self.context['day'] = 1
return
- day = self._get_int(day)
+ day = DateParser._get_int(day)
if day < 1 or day > 31:
- raise ParseException(
- f'Bad dayOfMonth expression: {ctx.getText()}'
- )
- except Exception:
- raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
+ raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad dayOfMonth expression: {ctx.getText()}') from e
self.context['day'] = day
- def exitMonthName(
- self, ctx: dateparse_utilsParser.MonthNameContext
- ) -> None:
+ def exitMonthName(self, ctx: dateparse_utilsParser.MonthNameContext) -> None:
try:
month = ctx.getText()
while month[0] == '/' or month[0] == '-':
month = month[:3].lower()
month = self.month_name_to_number.get(month, None)
if month is None:
- raise ParseException(
- f'Bad monthName expression: {ctx.getText()}'
- )
- except Exception:
- raise ParseException(f'Bad monthName expression: {ctx.getText()}')
+ raise ParseException(f'Bad monthName expression: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad monthName expression: {ctx.getText()}') from e
else:
self.context['month'] = month
- def exitMonthNumber(
- self, ctx: dateparse_utilsParser.MonthNumberContext
- ) -> None:
+ def exitMonthNumber(self, ctx: dateparse_utilsParser.MonthNumberContext) -> None:
try:
- month = self._get_int(ctx.getText())
+ month = DateParser._get_int(ctx.getText())
if month < 1 or month > 12:
- raise ParseException(
- f'Bad monthNumber expression: {ctx.getText()}'
- )
- except Exception:
- raise ParseException(
- f'Bad monthNumber expression: {ctx.getText()}'
- )
+ raise ParseException(f'Bad monthNumber expression: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad monthNumber expression: {ctx.getText()}') from e
else:
self.context['month'] = month
def exitYear(self, ctx: dateparse_utilsParser.YearContext) -> None:
try:
- year = self._get_int(ctx.getText())
+ year = DateParser._get_int(ctx.getText())
if year < 1:
raise ParseException(f'Bad year expression: {ctx.getText()}')
- except Exception:
- raise ParseException(f'Bad year expression: {ctx.getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad year expression: {ctx.getText()}') from e
else:
+ self.saw_overt_year = True
self.context['year'] = year
def exitSpecialDateMaybeYearExpr(
try:
special = ctx.specialDate().getText().lower()
self.context['special'] = special
- except Exception:
+ except Exception as e:
raise ParseException(
f'Bad specialDate expression: {ctx.specialDate().getText()}'
- )
+ ) from e
try:
mod = ctx.thisNextLast()
if mod is not None:
self.context['special_next_last'] = 'next'
elif mod.LAST() is not None:
self.context['special_next_last'] = 'last'
- except Exception:
- raise ParseException(
- f'Bad specialDateNextLast expression: {ctx.getText()}'
- )
+ except Exception as e:
+ raise ParseException(f'Bad specialDateNextLast expression: {ctx.getText()}') from e
def exitNFoosFromTodayAgoExpr(
self, ctx: dateparse_utilsParser.NFoosFromTodayAgoExprContext
) -> None:
d = self.now_datetime
try:
- count = self._get_int(ctx.unsignedInt().getText())
+ count = DateParser._get_int(ctx.unsignedInt().getText())
unit = ctx.deltaUnit().getText().lower()
ago_from_now = ctx.AGO_FROM_NOW().getText()
- except Exception:
- raise ParseException(
- f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}'
- )
+ except Exception as e:
+ raise ParseException(f'Bad NFoosFromTodayAgoExpr: {ctx.getText()}') from e
if "ago" in ago_from_now or "back" in ago_from_now:
count = -count
unit = self._figure_out_date_unit(unit)
- d = n_timeunits_from_base(
- count,
- TimeUnit(unit),
- d)
+ d = n_timeunits_from_base(count, TimeUnit(unit), d)
self.context['year'] = d.year
self.context['month'] = d.month
self.context['day'] = d.day
def exitDeltaRelativeToTodayExpr(
self, ctx: dateparse_utilsParser.DeltaRelativeToTodayExprContext
) -> None:
+ # When someone says "next week" they mean a week from now.
+ # Likewise next month or last year. These expressions are now
+ # +/- delta.
+ #
+ # But when someone says "this Friday" they mean "this coming
+ # Friday". It would be weird to say "this Friday" if today
+ # was already Friday but I'm parsing it to mean: the next day
+ # that is a Friday. So when you say "next Friday" you mean
+ # the Friday after this coming Friday, or 2 Fridays from now.
+ #
+ # This set handles this weirdness.
+ weekdays = set(
+ [
+ TimeUnit.MONDAYS,
+ TimeUnit.TUESDAYS,
+ TimeUnit.WEDNESDAYS,
+ TimeUnit.THURSDAYS,
+ TimeUnit.FRIDAYS,
+ TimeUnit.SATURDAYS,
+ TimeUnit.SUNDAYS,
+ ]
+ )
d = self.now_datetime
try:
mod = ctx.thisNextLast()
+ unit = ctx.deltaUnit().getText().lower()
+ unit = self._figure_out_date_unit(unit)
if mod.LAST():
count = -1
elif mod.THIS():
- count = +1
+ if unit in weekdays:
+ count = +1
+ else:
+ count = 0
elif mod.NEXT():
- count = +2
+ if unit in weekdays:
+ count = +2
+ else:
+ count = +1
else:
- raise ParseException(
- f'Bad This/Next/Last modifier: {mod}'
- )
- unit = ctx.deltaUnit().getText().lower()
- except Exception:
- raise ParseException(
- f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}'
- )
- unit = self._figure_out_date_unit(unit)
- d = n_timeunits_from_base(
- count,
- TimeUnit(unit),
- d)
+ raise ParseException(f'Bad This/Next/Last modifier: {mod}')
+ except Exception as e:
+ raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}') from e
+ d = n_timeunits_from_base(count, TimeUnit(unit), d)
self.context['year'] = d.year
self.context['month'] = d.month
self.context['day'] = d.day
- def exitSpecialTimeExpr(
- self, ctx: dateparse_utilsParser.SpecialTimeExprContext
- ) -> None:
+ def exitSpecialTimeExpr(self, ctx: dateparse_utilsParser.SpecialTimeExprContext) -> None:
try:
txt = ctx.specialTime().getText().lower()
- except Exception:
- raise ParseException(
- f'Bad special time expression: {ctx.getText()}'
- )
+ except Exception as e:
+ raise ParseException(f'Bad special time expression: {ctx.getText()}') from e
else:
- if txt == 'noon' or txt == 'midday':
+ if txt in ('noon', 'midday'):
self.context['hour'] = 12
self.context['minute'] = 0
self.context['seconds'] = 0
try:
tz = ctx.tzExpr().getText()
- self.context['tz'] = self._parse_tz(tz)
+ self.context['tz'] = DateParser._parse_tz(tz)
except Exception:
pass
- def exitTwelveHourTimeExpr(
- self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext
- ) -> None:
+ def exitTwelveHourTimeExpr(self, ctx: dateparse_utilsParser.TwelveHourTimeExprContext) -> None:
try:
hour = ctx.hour().getText()
while not hour[-1].isdigit():
hour = hour[:-1]
- hour = self._get_int(hour)
- except Exception:
- raise ParseException(f'Bad hour: {ctx.hour().getText()}')
+ hour = DateParser._get_int(hour)
+ except Exception as e:
+ raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
if hour <= 0 or hour > 12:
raise ParseException(f'Bad hour (out of range): {hour}')
try:
- minute = self._get_int(ctx.minute().getText())
+ minute = DateParser._get_int(ctx.minute().getText())
except Exception:
minute = 0
if minute < 0 or minute > 59:
self.context['minute'] = minute
try:
- seconds = self._get_int(ctx.second().getText())
+ seconds = DateParser._get_int(ctx.second().getText())
except Exception:
seconds = 0
if seconds < 0 or seconds > 59:
self.context['seconds'] = seconds
try:
- micros = self._get_int(ctx.micros().getText())
+ micros = DateParser._get_int(ctx.micros().getText())
except Exception:
micros = 0
if micros < 0 or micros > 1000000:
try:
ampm = ctx.ampm().getText()
- except Exception:
- raise ParseException(f'Bad ampm: {ctx.ampm().getText()}')
+ except Exception as e:
+ raise ParseException(f'Bad ampm: {ctx.ampm().getText()}') from e
if hour == 12:
hour = 0
if ampm[0] == 'p':
try:
tz = ctx.tzExpr().getText()
- self.context['tz'] = self._parse_tz(tz)
+ self.context['tz'] = DateParser._parse_tz(tz)
except Exception:
pass
hour = ctx.hour().getText()
while not hour[-1].isdigit():
hour = hour[:-1]
- hour = self._get_int(hour)
- except Exception:
- raise ParseException(f'Bad hour: {ctx.hour().getText()}')
+ hour = DateParser._get_int(hour)
+ except Exception as e:
+ raise ParseException(f'Bad hour: {ctx.hour().getText()}') from e
if hour < 0 or hour > 23:
raise ParseException(f'Bad hour (out of range): {hour}')
self.context['hour'] = hour
try:
- minute = self._get_int(ctx.minute().getText())
+ minute = DateParser._get_int(ctx.minute().getText())
except Exception:
minute = 0
if minute < 0 or minute > 59:
self.context['minute'] = minute
try:
- seconds = self._get_int(ctx.second().getText())
+ seconds = DateParser._get_int(ctx.second().getText())
except Exception:
seconds = 0
if seconds < 0 or seconds > 59:
self.context['seconds'] = seconds
try:
- micros = self._get_int(ctx.micros().getText())
+ micros = DateParser._get_int(ctx.micros().getText())
except Exception:
micros = 0
if micros < 0 or micros >= 1000000:
try:
tz = ctx.tzExpr().getText()
- self.context['tz'] = self._parse_tz(tz)
+ self.context['tz'] = DateParser._parse_tz(tz)
except Exception:
pass
try:
dt = parser.parse(line)
except Exception as e:
+ logger.exception(e)
print("Unrecognized.")
else:
+ assert dt is not None
print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
sys.exit(0)