Pergunta

please consider this problem:

I have a date in the past from where I start adding periods. When adding these periods results in a date greater than today, I want to stop, and check what the last date is.

This is a functionality for calculating debit dates in a membership. A member joins, say, 2007-01-31. He is debited every month. Let's say today is 2013-03-29 (it actually is atm). So I need to start counting months since 2007-01-31 and when I get past today's date, I need to stop. I can then see that the next debit date is 2013-03-31.

I am using the dateutil library to implement this, adding relativedelta's in a while loop until I surpass the current date. (I know it's probably not the best way, but I'm still quite new at Python and this is a proof-of-concept). The problem is that when I add a month to 2007-01-31, the next date is 2007-02-28, which is correct. But the next iteration the date is 2007-03-28, because dateutil doesn't recognize the 28th as the last day of the month to keep it intact and iterate to the last day of march. Ofcourse, that's a perfectly valid implementation. I then experimented with dateutils rrule object, but it has the same principles. It outputs a list of dates, but it simply skips the months that don't have enough days.

period = rrule(MONTHLY, interval=1, dtstart=datetime.date(2012, 5, 31), until=datetime.date(2013, 3, 29))
print(list(period))

Then I thought of a different approach:

If I could count the number of periods in the timespan between 2007-01-31 and 2013-03-29, I can add those number of periods to the startdate, and dateutil would return the right date.
The problem there is that the period isn't always one month. It can also be four weeks, a quarter or a year, for example.

I couldn't find a way to take a relativedelta and divide it with another relativedelta, to get a number of times the latter goes in the first.

If anyone can point me in the right direction I would appreciate it. Is there, for example, a library that can do this (divide timespans by each other, and output the result in a given timeblock, like months or weeks)? Is there perhaps a datediff function that accepts a period as an input (I know for example in vbscript you can get the difference between two dates in whatever period you want, be it weeks, months, days, whatever). Is there perhaps a totally different solution?

For completeness, I will include the code, but I think the text explains it all already:

def calculate(self, testdate=datetime.date.today()):
    self._testdate = testdate


    while self.next < self._testdate:
        self.next += self._ddinterval
    self.previous = self.next - self._ddinterval
    return self.next

Thanks,

Erik

edit: I now have a solution that does what it's supposed to, but it's hardly Pythonic, elegant or speedy. So the question remains the same, if anyone can come up with a better solution, please do. Here's what I came up with:

def calculate(self, testdate=datetime.date.today()):
    self._testdate = testdate

    start = self.next
    count = 0
    while self.next < self._testdate:
        count += 1
        self.next = start + (count * self._ddinterval)
    self.previous = self.next - self._ddinterval
    return self.next
Foi útil?

Solução

Instead of using the new value after adding your period as the starting point for the next loop, create an ever-increasing delta instead:

today = datetime.date.today()
start = datetime.date(2007, 1, 31)
period = relativedelta(months=1)

delta = period
while start + delta < today:
    delta += period

next = start + delta

This results in:

>>> import datetime
>>> from dateutil.relativedelta import relativedelta
>>> today = datetime.date.today()
>>> start = datetime.date(2007, 1, 31)
>>> period = relativedelta(months=1)
>>> delta = period
>>> while start + delta < today:
...     delta += period
... 
>>> delta
relativedelta(years=+6, months=+3)
>>> start + delta
datetime.date(2013, 4, 30)

This works for any variable period lengths including quarters and years. For periods measured in exact weeks or days you can use a timedelta() object too, but this is a generic solution.

You cannot 'divide' time periods when using variable-width periods such as months and quarters. For time periods measured in whole days you can simply convert the difference between two dates to days (from the timedelta) and then get the modulus:

period = datetime.timedelta(days=28)  # 4 weeks
delta = today - start
remainder = delta.days % period.days
end = today + datetime.timedelta(days=remainder)

which gives:

>>> period = datetime.timedelta(days=28)  # 4 weeks
>>> delta = today - start
>>> remainder = delta.days % period.days
>>> today + datetime.timedelta(days=remainder)
datetime.date(2013, 4, 17)

Outras dicas

If your delta is variable with respect to base time (i.e., 1 month can mean any of 28-31 days depending), then you're stuck with a loop.

If delta is a constant day count, however, you can sidestep iteration by converting to integers and doing a modulo operation.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top