Since this thing is on the innerwebs I suppose it should have a
[python_utils.git] / dateparse / dateparse_utils.py
index 54a47366b6f6493ca7766d6ff72e52a0ac87cf54..bd0d491f17e55fc4eef1894cfbe8a544b88787f2 100755 (executable)
@@ -1,9 +1,11 @@
 #!/usr/bin/env python3
+# type: ignore
+# pylint: disable=W0201
+# pylint: disable=R0904
 
-"""
-Parse dates in a variety of formats.
+# © Copyright 2021-2022, Scott Gasch
 
-"""
+"""Parse dates in a variety of formats."""
 
 import datetime
 import functools
@@ -24,7 +26,12 @@ 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
-from datetime_utils import TimeUnit, date_to_datetime, datetime_to_date, n_timeunits_from_base
+from datetime_utils import (
+    TimeUnit,
+    date_to_datetime,
+    datetime_to_date,
+    n_timeunits_from_base,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -51,6 +58,7 @@ class ParseException(Exception):
     """An exception thrown during parsing because of unrecognized input."""
 
     def __init__(self, message: str) -> None:
+        super().__init__()
         self.message = message
 
 
@@ -85,6 +93,8 @@ class RaisingErrorListener(antlr4.DiagnosticErrorListener):
     ),
 )
 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
@@ -154,6 +164,11 @@ class DateParser(dateparse_utilsListener):
             '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]:
@@ -277,7 +292,7 @@ class DateParser(dateparse_utilsListener):
         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)
@@ -361,7 +376,8 @@ class DateParser(dateparse_utilsListener):
             day=self.context['day'],
         )
 
-    def _parse_tz(self, txt: str) -> Any:
+    @staticmethod
+    def _parse_tz(txt: str) -> Any:
         if txt == 'Z':
             txt = 'UTC'
 
@@ -384,7 +400,7 @@ class DateParser(dateparse_utilsListener):
         # Try constructing an offset in seconds
         try:
             txt_sign = txt[0]
-            if txt_sign == '-' or txt_sign == '+':
+            if txt_sign in ('-', '+'):
                 sign = +1 if txt_sign == '+' else -1
                 hour = int(txt[1:3])
                 minute = int(txt[-2:])
@@ -395,14 +411,16 @@ class DateParser(dateparse_utilsListener):
             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
@@ -485,12 +503,7 @@ class DateParser(dateparse_utilsListener):
         # 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?
@@ -526,12 +539,7 @@ class DateParser(dateparse_utilsListener):
         # 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.
@@ -554,10 +562,10 @@ class DateParser(dateparse_utilsListener):
             if n is None:
                 raise ParseException(f'Bad N in Delta +/- Expr: {ctx.getText()}')
             n = n.getText()
-            n = self._get_int(n)
+            n = DateParser._get_int(n)
             unit = self._figure_out_date_unit(ctx.deltaUnit().getText().lower())
-        except Exception:
-            raise ParseException(f'Invalid Delta +/-: {ctx.getText()}')
+        except Exception as e:
+            raise ParseException(f'Invalid Delta +/-: {ctx.getText()}') from e
         else:
             self.context['delta_int'] = n
             self.context['delta_unit'] = unit
@@ -565,16 +573,16 @@ class DateParser(dateparse_utilsListener):
     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:
         try:
             txt = ctx.getText().lower()
-        except Exception:
-            raise ParseException(f'Bad next/last: {ctx.getText()}')
+        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':
@@ -600,8 +608,8 @@ class DateParser(dateparse_utilsListener):
         try:
             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()}')
 
@@ -616,22 +624,22 @@ class DateParser(dateparse_utilsListener):
                 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:
         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:
         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
 
@@ -688,8 +696,8 @@ class DateParser(dateparse_utilsListener):
                 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,
@@ -699,9 +707,9 @@ class DateParser(dateparse_utilsListener):
 
     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
 
@@ -714,8 +722,8 @@ class DateParser(dateparse_utilsListener):
                 txt = -1
             else:
                 raise ParseException(f'Bad first|last expression: {ctx.getText()}')
-        except Exception:
-            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
 
@@ -723,8 +731,8 @@ class DateParser(dateparse_utilsListener):
         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
 
@@ -740,11 +748,11 @@ class DateParser(dateparse_utilsListener):
             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()}')
+        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:
@@ -756,28 +764,28 @@ class DateParser(dateparse_utilsListener):
             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()}')
+        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:
         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()}')
+        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
@@ -788,8 +796,10 @@ class DateParser(dateparse_utilsListener):
         try:
             special = ctx.specialDate().getText().lower()
             self.context['special'] = special
-        except Exception:
-            raise ParseException(f'Bad specialDate expression: {ctx.specialDate().getText()}')
+        except Exception as e:
+            raise ParseException(
+                f'Bad specialDate expression: {ctx.specialDate().getText()}'
+            ) from e
         try:
             mod = ctx.thisNextLast()
             if mod is not None:
@@ -799,19 +809,19 @@ class DateParser(dateparse_utilsListener):
                     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
@@ -866,8 +876,8 @@ class DateParser(dateparse_utilsListener):
                     count = +1
             else:
                 raise ParseException(f'Bad This/Next/Last modifier: {mod}')
-        except Exception:
-            raise ParseException(f'Bad DeltaRelativeToTodayExpr: {ctx.getText()}')
+        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
@@ -876,10 +886,10 @@ class DateParser(dateparse_utilsListener):
     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
@@ -894,7 +904,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             tz = ctx.tzExpr().getText()
-            self.context['tz'] = self._parse_tz(tz)
+            self.context['tz'] = DateParser._parse_tz(tz)
         except Exception:
             pass
 
@@ -903,14 +913,14 @@ class DateParser(dateparse_utilsListener):
             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:
@@ -918,7 +928,7 @@ class DateParser(dateparse_utilsListener):
         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:
@@ -926,7 +936,7 @@ class DateParser(dateparse_utilsListener):
         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:
@@ -935,8 +945,8 @@ class DateParser(dateparse_utilsListener):
 
         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':
@@ -945,7 +955,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             tz = ctx.tzExpr().getText()
-            self.context['tz'] = self._parse_tz(tz)
+            self.context['tz'] = DateParser._parse_tz(tz)
         except Exception:
             pass
 
@@ -956,15 +966,15 @@ class DateParser(dateparse_utilsListener):
             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:
@@ -972,7 +982,7 @@ class DateParser(dateparse_utilsListener):
         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:
@@ -980,7 +990,7 @@ class DateParser(dateparse_utilsListener):
         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:
@@ -989,7 +999,7 @@ class DateParser(dateparse_utilsListener):
 
         try:
             tz = ctx.tzExpr().getText()
-            self.context['tz'] = self._parse_tz(tz)
+            self.context['tz'] = DateParser._parse_tz(tz)
         except Exception:
             pass