Reputation: 329
I need to calculate deadline
(datetime) after adding N
(int) interval
s (represented by relativedelta, because it can be months or years and also in seconds or dates). I can do it simply by multiplying interval
by N
and summing it to start_date
(datetime). At the same time, I need to do it in multiple steps, like calculate 5th deadline, 6th..., so I just add interval
to start_date
for N
times.
In some cases, these two methods provide different results.
Assume start_date
= datetime(year=2019, month=1, day=2), interval
= relativedelta(months=1, days=2), and N
= 16.
From one point of view, both of mothods are correct, because interval*16
= relativedelta(years=+1, months=+4, days=+32), start_date+16*interval
= 2019-01-01 + 1 year + 4 months + 32 days = 2020/05/1 + 32 days = 2020-06-02(because May has 31 day).
At the sametime, when we add them one by one it results into 2020/05/1 + 1 month + 2 days = 2020/06/02
The problem is related to "month-days overflow", but I can't figure out how to handle it. Always use sum instead of multiplication? But isn't calc-safe (imagine 9999999th deadline with interval=1 day and 1 sec)
Steps to reproduce:
def test_relative_sum_mult_with_date():
start = datetime(year=2019, month=1, day=1)
interval = relativedelta(months=1, days=2)
check_up_to = 100
for i in range(check_up_to):
multiplied = start + i*interval
summed = start
for j in range(i):
summed += interval
print('i=%s, i*interval=%s, diff(multiplied-summed)=%s, multiplied=%s, summed=%s' %
(i, i*interval, multiplied-summed, multiplied, summed))
assert multiplied == summed
Trace:
i*interval=relativedelta(), diff(multiplied-summed)=0:00:00, multiplied=2019-01-01 00:00:00, summed=2019-01-01 00:00:00
i=1, i*interval=relativedelta(months=+1, days=+2), diff(multiplied-summed)=0:00:00, multiplied=2019-02-03 00:00:00, summed=2019-02-03 00:00:00
i=2, i*interval=relativedelta(months=+2, days=+4), diff(multiplied-summed)=0:00:00, multiplied=2019-03-05 00:00:00, summed=2019-03-05 00:00:00
i=3, i*interval=relativedelta(months=+3, days=+6), diff(multiplied-summed)=0:00:00, multiplied=2019-04-07 00:00:00, summed=2019-04-07 00:00:00
i=4, i*interval=relativedelta(months=+4, days=+8), diff(multiplied-summed)=0:00:00, multiplied=2019-05-09 00:00:00, summed=2019-05-09 00:00:00
i=5, i*interval=relativedelta(months=+5, days=+10), diff(multiplied-summed)=0:00:00, multiplied=2019-06-11 00:00:00, summed=2019-06-11 00:00:00
i=6, i*interval=relativedelta(months=+6, days=+12), diff(multiplied-summed)=0:00:00, multiplied=2019-07-13 00:00:00, summed=2019-07-13 00:00:00
i=7, i*interval=relativedelta(months=+7, days=+14), diff(multiplied-summed)=0:00:00, multiplied=2019-08-15 00:00:00, summed=2019-08-15 00:00:00
i=8, i*interval=relativedelta(months=+8, days=+16), diff(multiplied-summed)=0:00:00, multiplied=2019-09-17 00:00:00, summed=2019-09-17 00:00:00
i=9, i*interval=relativedelta(months=+9, days=+18), diff(multiplied-summed)=0:00:00, multiplied=2019-10-19 00:00:00, summed=2019-10-19 00:00:00
i=10, i*interval=relativedelta(months=+10, days=+20), diff(multiplied-summed)=0:00:00, multiplied=2019-11-21 00:00:00, summed=2019-11-21 00:00:00
i=11, i*interval=relativedelta(months=+11, days=+22), diff(multiplied-summed)=0:00:00, multiplied=2019-12-23 00:00:00, summed=2019-12-23 00:00:00
i=12, i*interval=relativedelta(years=+1, days=+24), diff(multiplied-summed)=0:00:00, multiplied=2020-01-25 00:00:00, summed=2020-01-25 00:00:00
i=13, i*interval=relativedelta(years=+1, months=+1, days=+26), diff(multiplied-summed)=0:00:00, multiplied=2020-02-27 00:00:00, summed=2020-02-27 00:00:00
i=14, i*interval=relativedelta(years=+1, months=+2, days=+28), diff(multiplied-summed)=0:00:00, multiplied=2020-03-29 00:00:00, summed=2020-03-29 00:00:00
i=15, i*interval=relativedelta(years=+1, months=+3, days=+30), diff(multiplied-summed)=0:00:00, multiplied=2020-05-01 00:00:00, summed=2020-05-01 00:00:00
i=16, i*interval=relativedelta(years=+1, months=+4, days=+32), diff(multiplied-summed)=-1 day, 0:00:00, multiplied=2020-06-02 00:00:00, summed=2020-06-03 00:00:00
datetime.datetime(2020, 6, 2, 0, 0, 0) != datetime.datetime(2020, 6, 3, 0, 0, 0)
Expected :datetime.datetime(2020, 6, 3, 0, 0, 0)
Actual :datetime.datetime(2020, 6, 2, 0, 0, 0)
Versions: Python 3.6 python-dateutil==2.8.0
Upvotes: 0
Views: 928
Reputation: 202
Let me put your example in a more simple manner:
start = datetime(year=2018, month=3, day=29)
interval = relativedelta(months=1, days=2)
d1 = start + interval * 2 # 2018-06-02
d2 = start + interval + interval # 2018-06-03
print(d1, d2)
So I don't even think it's a library bug: just follow the same calculations mentally and see they make some sense.
Upvotes: 1