Search code examples
pythonpython-dateutilrrule

Very weird rruleset behavior


Using the latest dateutil available on pip, I'm getting strange time and ordering-dependent behavior when calling the count method using a recurring DAILY rrule.

>>> import dateutil
>>> dateutil.__version__
'2.4.2'
>>> from dateutil import rrule
>>> import datetime

>>> rules = rrule.rruleset()
>>> rules.rrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.count()
8179
>>> rules.exrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.count()
8179  # ??? Expected 0

>>> rules = rrule.rruleset()
>>> rules.exrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.rrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.count()
8179  # ??? Expected 0

>>> rules = rrule.rruleset()
>>> rules.exrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.count()
0
>>> rules.rrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.count()
0  # Now its working???

>>> rules = rrule.rruleset()
>>> rules.exrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.rrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.count()
8179  # ??? Expected 0

>>> rules = rrule.rruleset()
>>> rules.count()
0
>>> rules.rrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.count()
0  # WHAT???
>>> rules.count()
0

>>> rules = rrule.rruleset()
>>> rules.rrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
>>> rules.count()
8179  # IM DONE... WTF

Solution

  • The answer is simple, its because you have not included the dtstart parameter when creating the ruleset, when that is not included it defaults to datetime.datetime.now() , which is the current time, and it contains components upto the current microsecond.

    Hence, when you first create the ruleset using -

    >>> rules = rrule.rruleset()
    >>> rules.rrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
    >>> rules.count()
    8179
    

    You got entries by starting at the current time , upto microsecond.

    After some time, when you again try -

    rules.exrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0)))
    

    You are again creating a rrule.rrule object, by starting at current time , so its not the same as the previous one that you have created in rules.

    To fix the issue, you can specify the dtstart attribute to make sure it starts at the same time.

    Example -

    >>> rules = rrule.rruleset()
    >>> rules.rrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0), dtstart=datetime.datetime(now.year,now.month,now.day,0,0,0)))
    >>> rules.count()
    8179
    >>> rules.exrule(rrule.rrule(rrule.DAILY, until=datetime.datetime(2038,1,1,0,0,0), dtstart=datetime.datetime(now.year,now.month,now.day,0,0,0)))
    >>> l3 = list(rules)
    >>> len(l3)
    0
    >>> rules.count()
    0
    

    Similar issue occurs throughout your other examples.


    Given the above, I think there is an issue in the dateutil code, where they are actually caching the count (length) of ruleset when you first time call count() , and then its correct length is only recalculated when you iterate over it, etc.

    The issue occurs in rrulebase class, which is the base class for ruleset . The code from that is (source - https://github.com/dateutil/dateutil/blob/master/dateutil/rrule.py) -

    def count(self):
        """ Returns the number of recurrences in this set. It will have go
            trough the whole recurrence, if this hasn't been done before. """
        if self._len is None:
            for x in self:
                pass
        return self._len
    

    So, even after applying exrule() if you had previously called .count(), it would keep giving back the same count.

    I am not 100% sure if its a bug or if its intended to behave like that , most probably it is a bug.

    I have openned issue for this.