X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=src%2Fpyutils%2Fdatetimez%2Fdateparse_utils.py;h=7e8b6d6d01f71fd8f2178b3a9c0d549371551807;hb=993b0992473c12294ed659e52b532e1c8cf9cd1e;hp=89112b4e29fb91a235f13c8f6dbdd74952068fe8;hpb=b38920f24d1ac948958480c540bc4b8436186765;p=pyutils.git diff --git a/src/pyutils/datetimez/dateparse_utils.py b/src/pyutils/datetimez/dateparse_utils.py index 89112b4..7e8b6d6 100755 --- a/src/pyutils/datetimez/dateparse_utils.py +++ b/src/pyutils/datetimez/dateparse_utils.py @@ -5,7 +5,88 @@ # © 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 `_ for more examples and the `grammar `_ for more details. +""" import datetime import functools @@ -61,6 +142,10 @@ class ParseException(Exception): """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 @@ -100,15 +185,19 @@ class RaisingErrorListener(antlr4.DiagnosticErrorListener): ), ) 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 @@ -117,10 +206,14 @@ class DateParser(dateparse_utilsListener): 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, @@ -188,27 +281,44 @@ class DateParser(dateparse_utilsListener): 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) @@ -228,22 +338,40 @@ class DateParser(dateparse_utilsListener): 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: