Как справиться с суммированием и умножением временных интервалов?

Мне нужно вычислить deadline (datetime) после добавления N (int) intervals (представленного relativedelta, потому что это могут быть месяцы или годы, а также секунды или даты). Я могу сделать это, просто умножив interval на N и суммируя его с start_date (datetime). В то же время мне нужно сделать это в несколько этапов, например, рассчитать 5-й крайний срок, 6-й ..., поэтому я просто добавляю interval к start_date N раз. В некоторых случаях эти два метода дают разные результаты.

Предположим, что start_date = datetime (год = 2019, месяц = ​​1, день = 2), interval = relativedelta (месяцы = 1, дни = 2) и N = 16. С одной точки зрения, оба метода верны, потому что interval*16 = relativedelta (годы = + 1, месяцы = + 4, дни = + 32), start_date+16*interval = 2019-01-01 + 1 год + 4 месяца + 32 дня = 2020/05/1 + 32 дня = 2020-06-02 (потому что в мае 31 день). В то же время, когда мы добавляем их по одному, получается 2020/05/1 + 1 месяц + 2 дня = 2020/06/02.

Проблема связана с "переполнением месяца-дней", но я не могу понять, как с этим справиться. Всегда используйте сумму вместо умножения? Но небезопасно для калькуляции (представьте 9999999-й крайний срок с интервалом = 1 день и 1 секунда).

Действия по воспроизведению:

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

След:

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)

Версии: Python 3.6 python-dateutil == 2.8.0


person Anton Pererva    schedule 12.04.2019    source источник
comment
Что такое relativedelta() функция?   -  person martineau    schedule 12.04.2019
comment
@martineau Это из пакета dateutil, dateutil.readthedocs.io/en/stable, это может обрабатывать относительные интервалы по секундам, дням, месяцам и годам.   -  person Anton Pererva    schedule 12.04.2019


Ответы (1)


Позвольте мне проиллюстрировать ваш пример более простым образом:

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)

Так что я даже не думаю, что это ошибка библиотеки: просто мысленно проследите за теми же вычислениями и убедитесь, что они имеют какой-то смысл.

person brevno    schedule 15.04.2019
comment
Да, этот пример намного короче)) и лучше освещает основную проблему. - person Anton Pererva; 16.04.2019