# © 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; for example,
+when using :file:`argparse_utils.py` to pass an argument of type
+datetime it allows the user to use free form English 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
"""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
),
)
class DateParser(dateparse_utilsListener):
- """A class to parse dates expressed in human language. Example usage::
+ """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: string_utils.parse_date.
+ See also :meth:`pyutils.string_utils.to_date`.
+
"""
PARSE_TYPE_SINGLE_DATE_EXPR = 1
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._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.
+ 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.
- 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)
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 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 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: