【问题标题】:Cannot reconstitute end date with dateutil relativedelta无法使用 dateutil relativedelta 重构结束日期
【发布时间】:2019-04-30 14:28:38
【问题描述】:

我正在使用 dateutil.relativedelta 来计算年龄。当结果出现在某些情况下不正确时,我正在验证结果。仅当开始日期为 1 日且月末为 31 日时才会出现此问题,但并非所有月份都出现此问题!奇怪的是,1 月和 8 月的结果是正确的。我确实遗漏了一些东西,但无法确定是什么以及在哪里。

我在 Win32 和 dateutil 2.7.5 上使用 python 2.7.10(但与 2.6.1 相同的问题)

要运行的代码:

from __future__ import print_function
import datetime
from dateutil.relativedelta import relativedelta

# Main function -- later on, its result is used in dateutil.rrule
# (all irrelevant code expurged)
def age(start, end, includeFirstDay=False):
    # includeFirstDay: whether to include the start date in the result
    if includeFirstDay:
        start += relativedelta(days=-1)
    # Documentation tells nothing about the order of arguments when the first
    # two are dates. The following seems correct.
    return relativedelta(end, start)

# Utility in external module
def eomonth(dt):
    # from the beginning of month, add one month and substract one day
    eom = dt + relativedelta(days=-dt.day+1) + relativedelta(months=+1, days=-1)
    return eom

test_values = [
    # the 3 following lines fail
    (datetime.date(2018, 10, 1), datetime.date(2018, 10, 31)),
    (datetime.date(2018, 10, 1), datetime.date(2018, 11, 1)+relativedelta(days=-1)),
    (datetime.date(2018, 10, 1), eomonth(datetime.date(2018, 10, 1))),
    # the following lines pass
    (datetime.date(2018, 10, 5), eomonth(datetime.date(2018, 10, 5))),
    (datetime.date(2016, 2, 1), datetime.date(2016, 2, 29)),
    (datetime.date(2016, 2, 1), datetime.date(2016, 3, 1)+relativedelta(days=-1)),
    (datetime.date(2016, 2, 1), eomonth(datetime.date(2016, 2, 1))),
    ]

def test(start, end, includeFirstDay=False):
    rd = age(start, end, includeFirstDay=includeFirstDay)
    # calculate end date from age
    d = rd.days + (-1 if includeFirstDay else 0)
    fin = start + relativedelta(years=rd.years, months=rd.months, days=d)
    if fin != end:  # i.e. AssertionError
        print('expected %s, got %s (includeFirstDay=%s)' %(end, fin, includeFirstDay))

for start, end in test_values:
    test(start, end, includeFirstDay=False)
    test(start, end, includeFirstDay=True)

# trying with all months in a year

def make_test(year):
    t = []
    for month in range(1, 13):
        start = datetime.date(year, month, 1)
        # Three ways to find the end of the month
        t.append((start, eomonth(start)))
        t.append((start, start + relativedelta(months=+1, days=-1)))  # only if start day == 1
        for eom in [31, 30, 29, 28]:  # brute force
            try:
                t.append((start, datetime.date(year, month, eom)))
                break
            except:
                pass
    return sorted(list(set(t)))

from pprint import pprint

print('\n*** testing 2016 (leap year)')
test_values = make_test(2016)
pprint(test_values)  # verify ends of months are correct
for start, end in test_values:
    test(start, end, includeFirstDay=False)
    test(start, end, includeFirstDay=True)

print('\n*** testing 2017')
test_values = make_test(2017)
pprint(test_values)  # verify ends of months are correct
for start, end in test_values:
    test(start, end, includeFirstDay=False)
    test(start, end, includeFirstDay=True)

输出:

expected 2018-10-31, got 2018-11-01 (includeFirstDay=True)
expected 2018-10-31, got 2018-11-01 (includeFirstDay=True)
expected 2018-10-31, got 2018-11-01 (includeFirstDay=True)

*** testing 2016 (leap year)
[(datetime.date(2016, 1, 1), datetime.date(2016, 1, 31)),
 (datetime.date(2016, 2, 1), datetime.date(2016, 2, 29)),
 (datetime.date(2016, 3, 1), datetime.date(2016, 3, 31)),
 (datetime.date(2016, 4, 1), datetime.date(2016, 4, 30)),
 (datetime.date(2016, 5, 1), datetime.date(2016, 5, 31)),
 (datetime.date(2016, 6, 1), datetime.date(2016, 6, 30)),
 (datetime.date(2016, 7, 1), datetime.date(2016, 7, 31)),
 (datetime.date(2016, 8, 1), datetime.date(2016, 8, 31)),
 (datetime.date(2016, 9, 1), datetime.date(2016, 9, 30)),
 (datetime.date(2016, 10, 1), datetime.date(2016, 10, 31)),
 (datetime.date(2016, 11, 1), datetime.date(2016, 11, 30)),
 (datetime.date(2016, 12, 1), datetime.date(2016, 12, 31))]
expected 2016-03-31, got 2016-04-02 (includeFirstDay=True)
expected 2016-05-31, got 2016-06-01 (includeFirstDay=True)
expected 2016-07-31, got 2016-08-01 (includeFirstDay=True)
expected 2016-10-31, got 2016-11-01 (includeFirstDay=True)
expected 2016-12-31, got 2017-01-01 (includeFirstDay=True)

*** testing 2017
[(datetime.date(2017, 1, 1), datetime.date(2017, 1, 31)),
 (datetime.date(2017, 2, 1), datetime.date(2017, 2, 28)),
 (datetime.date(2017, 3, 1), datetime.date(2017, 3, 31)),
 (datetime.date(2017, 4, 1), datetime.date(2017, 4, 30)),
 (datetime.date(2017, 5, 1), datetime.date(2017, 5, 31)),
 (datetime.date(2017, 6, 1), datetime.date(2017, 6, 30)),
 (datetime.date(2017, 7, 1), datetime.date(2017, 7, 31)),
 (datetime.date(2017, 8, 1), datetime.date(2017, 8, 31)),
 (datetime.date(2017, 9, 1), datetime.date(2017, 9, 30)),
 (datetime.date(2017, 10, 1), datetime.date(2017, 10, 31)),
 (datetime.date(2017, 11, 1), datetime.date(2017, 11, 30)),
 (datetime.date(2017, 12, 1), datetime.date(2017, 12, 31))]
expected 2017-03-31, got 2017-04-03 (includeFirstDay=True)
expected 2017-05-31, got 2017-06-01 (includeFirstDay=True)
expected 2017-07-31, got 2017-08-01 (includeFirstDay=True)
expected 2017-10-31, got 2017-11-01 (includeFirstDay=True)
expected 2017-12-31, got 2018-01-01 (includeFirstDay=True)

此外,如果可以(并且这个词存在),3 月的结束日期会更加错误。

如果有人能启发我,那将不胜感激。提前致谢。

【问题讨论】:

    标签: python relativedelta


    【解决方案1】:

    我的函数 age() 写得不好。这是正确的:

    def age(start, end, includeFirstDay=False):
        r = relativedelta(end, start)
        if includeFirstDay:
            r += relativedelta(days=1)
        return r
    

    解释:将 start 偏移 -1 天将日期移至上个月,并愚弄了我的意图混淆的 relativedelta。

    【讨论】:

      【解决方案2】:
      from __future__ import print_function
      import datetime
      from datetime import date
      from dateutil.relativedelta import relativedelta
      
      today= date.today()
      end_day=today.replace(day=1)
      end_day += relativedelta(months=1)
      end_day -= relativedelta(days=1)
      
      print(end_day)
      

      【讨论】:

      • 口头解释,而不是单独的代码,通常会很有帮助
      【解决方案3】:

      实际上,我做错了。 一月和八月表现“正常”的原因:对于这几个月,前几个月也有 31 天。 三月对我来说看起来很奇怪的原因:二月比 31 天少 2 或 3 天

      例如:

      start = datetime.date(2018, 3, 1)
      end = start + relativedelta(months=+1, days=-1)
      a = age(start, end, True)
      print 'start:', start
      print 'end  :', end
      print 'age  :', a
      

      输出:

      start: 2018-03-01
      end  : 2018-03-31
      age  : relativedelta(months=+1, days=+3)
      

      我期待像 relativedelta(days=+31) 这样的东西。

      根据开始和结束日期的提供方式,relativedelta 返回不同的结果。示例:

      relativedelta(datetime.date(2018, 4, 1), datetime.date(2018, 3, 1))
      

      给予

      relativedelta(months=+1)
      

      relativedelta(datetime.date(2018, 3, 31), datetime.date(2018, 2, 28))
      

      给予

      relativedelta(months=+1, days=+3)
      

      无论如何,开始日期和结束日期之间的差应始终等于 31 天。

      我的结论是,在计算其中一个日期时,不要使用 relativedelta 来计算第一天和最后一天(包括所有天)之间的单个月的年龄。

      或者修改函数age(),我可能会这样做。

      编辑

      在不久的将来,我将删除这个问题,因为我认为它提出的观点应该包含在 dateutil 文档中的“极端案例”类别中。

      【讨论】:

        猜你喜欢
        • 2014-11-03
        • 1970-01-01
        • 2019-07-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多