dateutil
dateutil copied to clipboard
Make it possible to separate `dtstart` and `basis` in constructing `rrule` (cannot generate `rrule` backwards)
I have a case where I need to know whether or not the start date of a recurrence (given a period, an interval, and a dtstart) is far enough in the future to make the next occurrence further away than it would be if the recurrence extended back to today. (Or to simplify the problem statement, I need to take an rrule determined by an interval, a period, and a dtstart, and extend that rrule backwards in time before that dtstart).
It seems the most robust way to answer this question would be to construct an rrule whose start date is today, but which uses the real start date as the basis for determining the by* defaults. It seems to me that dateutil doesn't currently provide any public API that I could use to do this, though.
I could, of course, provide today as the original dtstart if I explicitly calculate and provide the necessary by* arguments, so that dtstart isn't used as the basis for defaulting them. But this involves duplicating a bunch of knowledge that's already encoded in rrule.__init__, which seems silly.
There would be a few possible APIs for this, but the one that seems most natural to me (and doesn't break the immutability of rrule objects, which would be problematic for caching) would be to provide a new init argument, basis, which defaults to dtstart if not provided, and is used to provide the defaults for all the by* arguments. This would allow one to separate "when does this rrule start?" from "what key datetime should be used to lock down this recurrence?"
What do you think? Is this a reasonable use case? Is there an alternative technique that I've missed? If I submitted a PR for this, would it have a shot of getting in?
I think the actual code changes would be quite simple: add the basis arg, default it to dtstart if it is None, and then replace dtstart with basis throughout __init__ wherever dtstart is currently used to determine default values for by* properties.
@carljm If I'm understanding you correctly, I believe you can just construct your rrule using today (or any day before the desired start date) as the dtstart, then call rrule.after() using the desired date to start generating your dates. Is there a reason this would not work for you?
I think the downside to what I'm suggesting is that you'd have to predict the count between the current date and the "basis" date, but I think that would be the case in your proposal as well, since you seem to want to be able to retrieve dates specified by the rrule between the start date and the "basis" date anyway, so the "count" would be ambiguous anyway. If we're just worried about the count parameter, maybe a better solution would be adding a "count" parameter to the after() method, which would preserve all functionality.
Actually, looking at this more carefully, I realize that after() retrieves exactly one date, so the way to do what you want would be:
def dates_after(rr, basis_date, count=None):
"""
Generator to generate `count` dates after basis_date from rrule rr
:param rr:
Instance of `dateutil.rrule` to iterate over.
:param basis_date:
The date from which to start generating dates (inclusive)
:param count:
The maximum number of dates to return - if None is passed this will yield dates until the
rrule is exhausted.
"""
# Needs to execute at least once
cdate = rr.after(basis_date, inc=True) # Include the basis date on the first run
n = 0
while cdate is not None:
yield cdate
cdate = rr.after(cdate)
n += 1
if cdate is None or (count is not None and n >= count):
break
raise StopIteration
I think adding something like the above (though not necessarily as a wrapper for after()/before()) is a reasonable enhancement to rrule, so I may take a crack at it today.
Another valuable enhancement that could also help in this situation (though I think there's still room for a modified after()) would be to add a replace() method, analogous to datetime's replace(), or to namedtuple's _replace(). Then the same rrule could be easily regenerated with a different dtstart.
@pganssle Actually for my specific needs, just getting one date is fine, that's not an issue. The problem is something completely different: using today as dtstart doesn't generate the same recurrence as using the "real" start date. For instance, say my real start date is Feb 20, and I have interval 3 and period DAILY, and today is Feb 15. What I want to know is that the first actual occurrence will be Feb 20, but the first recurrence extended back to today would have been Feb 17. Creating an rrule with a dtstart of today gives me a totally different recurrence, one that occurs on Feb 15/18/21..., instead of 17/20/23/.... This is why what I'm asking for is to separate basis (the date used to determine the nature of the recurrence) from dtstart - that way I could create an rrule with a basis of Feb 20 but a dtstart of Feb 15, and (if it had a period of DAILY and an interval of 3) its first occurrence would be on Feb 17. I can't figure out any other way to achieve this with rrule, without re-implementing conditional logic for all the different periods that's in rrule.__init__.
Ah, I see what you mean. The problem is that rrules cannot iterate backwards, only forwards. I think that this is a feature that should be added when rrule2 is developed (Issue #15). There's definitely a kludge way to do it by modifying the core rrule and making all the logic go backwards, but I think there's probably a more elegant design pattern for this that is reversible starting at a given date.
It's worth thinking about how to implement that and whether we need a separate input or whether we can simply remove the restriction that all rrule dates are generated starting with dtstart.
Any updates on this? Any way to generate recurring dates going backward from the start date, without building something from scratch?