Search code examples
pythondatetimedate-parsingpython-dateutil

dateutil parser for month/year strings


Somewhat related to this post: dateutil parser for month/year format: return beginning of month

Given a date string of the form 'Sep-2020', dateutil.parser.parse correctly identifies the month and the year but adds the day as well. If a default is provided, it takes the day from it. Else, it will just use today's day. Is there anyway to tell if the parser used any of the default terms?

For example, how can I tell from the three options below that the input date string in the first case did not include day and that the default value was used?

>>> from datetime import datetime
>>> from dateutil import parser
>>> d = datetime(1978, 1, 1, 0, 0)
>>> parser.parse('Sep-2020', default=d)
datetime.datetime(2020, 9, 1, 0, 0)
>>> parser.parse('1-Sep-2020', default=d)
datetime.datetime(2020, 9, 1, 0, 0)
>>> parser.parse('Sep-1-2020', default=d)
datetime.datetime(2020, 9, 1, 0, 0)
``

Solution

  • I did something a little mad to solve this. It's mad since it's not guaranteed to work with future versions of dateutil (since it's relying on some dateutil internals).

    Currently I'm using: python-dateutil 2.8.1.

    I wrote my own class and passed it as default to the parser:

    from datetime import datetime
    
    
    class SentinelDateTime:
    
        def __init__(self, year=0, month=0, day=0, default=None):
            self._year = year
            self._month = month
            self._day = day
    
            if default is None:
                default = datetime.now().replace(
                    hour=0, minute=0,
                    second=0, microsecond=0
                )
    
            self.year = default.year
            self.month = default.month
            self.day = default.day
            self.default = default
    
        @property
        def has_year(self):
            return self._year != 0
    
        @property
        def has_month(self):
            return self._month != 0
    
        @property
        def has_day(self):
            return self._day != 0
    
        def todatetime(self):
            res = {
                attr: value
                for attr, value in [
                    ("year", self._year),
                    ("month", self._month),
                    ("day", self._day),
                ] if value
            }
            return self.default.replace(**res)
    
        def replace(self, **result):
            return SentinelDateTime(**result, default=self.default)
    
        def __repr__(self):
            return "%s(%d, %d, %d)" % (
                self.__class__.__qualname__,
                self._year,
                self._month,
                self._day
            )
    

    The dateutils method now returns this SentinelDateTime class:

    
    >>> from dateutil import parser
    >>> from datetime import datetime
    >>> from snippet1 import SentinelDateTime
    >>>
    >>> sentinel = SentinelDateTime()
    >>> s = parser.parse('Sep-2020', default=sentinel)
    >>> s
    SentinelDateTime(2020, 9, 0)
    >>> s.has_day
    False
    >>> s.todatetime()
    datetime.datetime(2020, 9, 9, 0, 0)
    
    
    >>> d = datetime(1978, 1, 1)
    >>> sentinel = SentinelDateTime(default=d)
    >>> s = parser.parse('Sep-2020', default=sentinel)
    >>> s
    SentinelDateTime(2020, 9, 0)
    >>> s.has_day
    False
    >>> s.todatetime()
    datetime.datetime(2020, 9, 1, 0, 0)
    
    

    I wrote this answer into a little package: https://github.com/foxyblue/sentinel-datetime