More documentation improvements.
[pyutils.git] / src / pyutils / datetimez / dateparse_utils.py
index 3ae2e1f3f7d38cc481f7bff7ac144096ad962cc1..47cca3cdb81a5156ee32d5c34bb730c3f7c9af40 100755 (executable)
@@ -5,7 +5,92 @@
 
 # © Copyright 2021-2022, Scott Gasch
 
 
 # © Copyright 2021-2022, Scott Gasch
 
-"""Parse dates in a variety of formats."""
+"""
+Parse dates / datetimes in a variety of formats.  Some examples:
+
+    |    today
+    |    tomorrow
+    |    yesterday
+    |    21:30
+    |    12:01am
+    |    12:01pm
+    |    last Wednesday
+    |    this Wednesday
+    |    next Wed
+    |    this coming Tues
+    |    this past Mon
+    |    4 days ago
+    |    4 Mondays ago
+    |    4 months ago
+    |    3 days back
+    |    13 weeks from now
+    |    1 year from now
+    |    4 weeks from now
+    |    3 saturdays ago
+    |    4 months from today
+    |    5 years from yesterday
+    |    6 weeks from tomorrow
+    |    april 15, 2005
+    |    april 21
+    |    9:30am on last Wednesday
+    |    2005/apr/15
+    |    2005 apr 15
+    |    the 1st wednesday in may
+    |    the last sun of june
+    |    this easter
+    |    last xmas
+    |    Christmas, 1999
+    |    next MLK day
+    |    Halloween, 2020
+    |    5 work days after independence day
+    |    50 working days from last wed
+    |    25 working days before xmas
+    |    today +1 week
+    |    sunday -3 weeks
+    |    3 weeks before xmas, 1999
+    |    3 days before new years eve, 2000
+    |    july 4th
+    |    the ides of march
+    |    the nones of april
+    |    the kalends of may
+    |    4 sundays before veterans' day
+    |    xmas eve
+    |    this friday at 5pm
+    |    presidents day
+    |    memorial day, 1921
+    |    thanksgiving
+    |    2 sun in jun
+    |    easter -40 days
+    |    2 days before last xmas at 3:14:15.92a
+    |    3 weeks after xmas, 1995 at midday
+    |    4 months before easter, 1992 at midnight
+    |    5 months before halloween, 1995 at noon
+    |    4 days before last wednesday
+    |    44 months after today
+    |    44 years before today
+    |    44 weeks ago
+    |    15 minutes to 3am
+    |    quarter past 4pm
+    |    half past 9
+    |    4 seconds to midnight
+    |    4 seconds to midnight, tomorrow
+    |    2021/apr/15T21:30:44.55
+    |    2021/apr/15 at 21:30:44.55
+    |    2021/4/15 at 21:30:44.55
+    |    2021/04/15 at 21:30:44.55Z
+    |    2021/04/15 at 21:30:44.55EST
+    |    13 days after last memorial day at 12 seconds before 4pm
+
+This code is used by other code in the pyutils library such as
+:meth:`pyutils.argparse_utils.valid_datetime`,
+:meth:`pyutils.argparse_utils.valid_date`,
+:meth:`pyutils.string_utils.to_datetime`
+and
+:meth:`pyutils.string_utils.to_date`.  This means any of these are
+also able to accept and recognize this larger set of date expressions.
+
+See the `unittest <https://wannabe.guru.org/gitweb/?p=pyutils.git;a=blob_plain;f=tests/datetimez/dateparse_utils_test.py;h=93c7b96e4c19af217fbafcf1ed5dbde13ec599c5;hb=HEAD>`_ for more examples and the `grammar <https://wannabe.guru.org/gitweb/?p=pyutils.git;a=blob_plain;f=src/pyutils/datetimez/dateparse_utils.g4;hb=HEAD>`_ for more details.
+"""
 
 import datetime
 import functools
 
 import datetime
 import functools
@@ -61,6 +146,10 @@ class ParseException(Exception):
     """An exception thrown during parsing because of unrecognized input."""
 
     def __init__(self, message: str) -> None:
     """An exception thrown during parsing because of unrecognized input."""
 
     def __init__(self, message: str) -> None:
+        """
+        Args:
+            message: parse error message description.
+        """
         super().__init__()
         self.message = message
 
         super().__init__()
         self.message = message
 
@@ -100,7 +189,20 @@ class RaisingErrorListener(antlr4.DiagnosticErrorListener):
     ),
 )
 class DateParser(dateparse_utilsListener):
     ),
 )
 class DateParser(dateparse_utilsListener):
-    """A class to parse dates expressed in human language."""
+    """A class to parse dates expressed in human language (English).
+    Example usage::
+
+        d = DateParser()
+        d.parse('next wednesday')
+        dt = d.get_datetime()
+        print(dt)
+        Wednesday 2022/10/26 00:00:00.000000
+
+    Note that the interface is somewhat klunky here because this class is
+    conforming to interfaces auto-generated by ANTLR as it parses the grammar.
+    See also :meth:`pyutils.string_utils.to_date`.
+
+    """
 
     PARSE_TYPE_SINGLE_DATE_EXPR = 1
     PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
 
     PARSE_TYPE_SINGLE_DATE_EXPR = 1
     PARSE_TYPE_BASE_AND_OFFSET_EXPR = 2
@@ -108,10 +210,14 @@ class DateParser(dateparse_utilsListener):
     PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
 
     def __init__(self, *, override_now_for_test_purposes=None) -> None:
     PARSE_TYPE_BASE_AND_OFFSET_TIME_EXPR = 4
 
     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.
-        Leave as None for real use cases.
+        """Construct a parser.
+
+        Args:
+            override_now_for_test_purposes: passing a value to
+                override_now_for_test_purposes can be used to force
+                this parser instance to use a custom date/time for its
+                idea of "now" so that the code can be more easily
+                unittested.  Leave as None for real use cases.
         """
         self.month_name_to_number = {
             'jan': 1,
         """
         self.month_name_to_number = {
             'jan': 1,
@@ -179,27 +285,44 @@ class DateParser(dateparse_utilsListener):
         self._reset()
 
     def parse(self, date_string: str) -> Optional[datetime.datetime]:
         self._reset()
 
     def parse(self, date_string: str) -> Optional[datetime.datetime]:
-        """Parse a date/time expression and return a timezone agnostic
-        datetime on success.  Also sets self.datetime, self.date and
-        self.time which can each be accessed other methods on the
-        class: get_datetime(), get_date() and get_time().  Raises a
-        ParseException with a helpful(?) message on parse error or
+        """
+        Parse a ~free form date/time expression and return a
+        timezone agnostic datetime on success.  Also sets
+        `self.datetime`, `self.date` and `self.time` which can each be
+        accessed other methods on the class: :meth:`get_datetime`,
+        :meth:`get_date` and :meth:`get_time`.  Raises a
+        `ParseException` with a helpful(?) message on parse error or
         confusion.
 
         confusion.
 
+        This is the main entrypoint to this class for caller code.
+
         To get an idea of what expressions can be parsed, check out
         the unittest and the grammar.
 
         To get an idea of what expressions can be parsed, check out
         the unittest and the grammar.
 
-        Usage:
+        Args:
+            date_string: the string to parse
 
 
-        txt = '3 weeks before last tues at 9:15am'
-        dp = DateParser()
-        dt1 = dp.parse(txt)
-        dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
+        Returns:
+            A datetime.datetime representing the parsed date/time or
+            None on error.
 
 
-        # dt1 and dt2 will be identical other than the fact that
-        # the latter's tzinfo will be set to PST/PDT.
+        .. note::
+
+            Parsed date expressions without any time part return
+            hours = minutes = seconds = microseconds = 0 (i.e. at
+            midnight that day).  Parsed time expressions without any
+            date part default to date = today.
+
+        Example usage::
+
+            txt = '3 weeks before last tues at 9:15am'
+            dp = DateParser()
+            dt1 = dp.parse(txt)
+            dt2 = dp.get_datetime(tz=pytz.timezone('US/Pacific'))
+
+            # dt1 and dt2 will be identical other than the fact that
+            # the latter's tzinfo will be set to PST/PDT.
 
 
-        This is the main entrypoint to this class for caller code.
         """
         date_string = date_string.strip()
         date_string = re.sub(r'\s+', ' ', date_string)
         """
         date_string = date_string.strip()
         date_string = re.sub(r'\s+', ' ', date_string)
@@ -219,22 +342,40 @@ class DateParser(dateparse_utilsListener):
         return self.datetime
 
     def get_date(self) -> Optional[datetime.date]:
         return self.datetime
 
     def get_date(self) -> Optional[datetime.date]:
-        """Return the date part or None."""
+        """
+        Returns:
+            The date part of the last :meth:`parse` operation again
+            or None.
+        """
         return self.date
 
     def get_time(self) -> Optional[datetime.time]:
         return self.date
 
     def get_time(self) -> Optional[datetime.time]:
-        """Return the time part or None."""
+        """
+        Returns:
+            The time part of the last :meth:`parse` operation again
+            or None.
+        """
         return self.time
 
     def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
         return self.time
 
     def get_datetime(self, *, tz=None) -> Optional[datetime.datetime]:
-        """Return as a datetime.  Parsed date expressions without any time
-        part return hours = minutes = seconds = microseconds = 0 (i.e. at
-        midnight that day).  Parsed time expressions without any date part
-        default to date = today.
-
-        The optional tz param allows the caller to request the datetime be
-        timezone aware and sets the tzinfo to the indicated zone.  Defaults
-        to timezone naive (i.e. tzinfo = None).
+        """Get the datetime of the last :meth:`parse` operation again
+        ot None.
+
+        Args:
+            tz: the timezone to set on output times.  By default we
+                return timezone-naive datetime objects.
+
+        Returns:
+            the same datetime that :meth:`parse` last did, optionally
+            overriding the timezone.  Returns None of no calls to
+            :meth:`parse` have yet been made.
+
+        .. note::
+
+            Parsed date expressions without any time part return
+            hours = minutes = seconds = microseconds = 0 (i.e. at
+            midnight that day).  Parsed time expressions without any
+            date part default to date = today.
         """
         dt = self.datetime
         if dt is not None:
         """
         dt = self.datetime
         if dt is not None: