如何在Python中将“3个月”添加到datetime.date对象?

43

Python如何进行日期计算?

我有一个Python应用程序需要每隔三个月绘制出几年的日期。重要的是,日期每年恰好出现四次,并尽可能在同一天出现,日期在尽可能多的情况下在同一天出现,并且日期尽可能接近“3个月”(这是一个移动目标,特别是在闰年中)。不幸的是,datetime.timedelta 不支持以月为单位的计算!

在Python中是否有“标准”的方法来进行这种计算?

SQL方式?

最坏的情况下,如果没有其他办法,我将让我的应用程序询问PostgreSQL,他们在日期计算方面具有良好的内置支持,如下所示:

# select ('2010-11-29'::date + interval '3 months')::date;
    date    
------------
 2011-02-28
(1 row)

取决于你认为一个月是多少天。它总是90天吗? - campos.ddc
为什么不直接修正日期呢?例如,使用1月1日、4月1日、7月1日、10月1日。这样就满足了“每年恰好4次”的要求,而且你不必选第一天,可以选择第5天、第10天或者其他任何你想要的日期。唯一的缺点是第一季度有90或91天,第二季度有91天,第三季度和第四季度都有92天,但对于政府工作来说,这已经足够了。 - jgritty
顺便说一下,最终我选择了PostgresSQL来进行所有日期计算,因为它是以最干净、最合理的方式进行计算的。不过,我在下面标记了最好的Python答案。 - Nathan Stocks
6个回答

83
如果您正在寻找确切或“更精确”的日期,最好查看dateutil
快速示例:
>>> from dateutil.relativedelta import relativedelta
>>> import datetime
>>> TODAY = datetime.date.today()
>>> TODAY
datetime.date(2012, 3, 6)

现在将3个月加到TODAY,观察它是否完全匹配(注意relativedelta(months=3)relativedelta(month=3)的行为不同。确保在这些示例中使用months!)。
>>> three_mon_rel = relativedelta(months=3)
>>> TODAY + three_mon_rel
datetime.date(2012, 6, 6)

这个值在一年中保持不变。每三个月的那一天,它都是一样的(必须要加上这句话,因为由于某种原因,在将一个relativedelta乘以一个datetime.date对象并将其添加时会抛出一个TypeError):

>>> TODAY + three_mon_rel + three_mon_rel
datetime.date(2012, 9, 6)
>>> TODAY + three_mon_rel + three_mon_rel + three_mon_rel
datetime.date(2012, 12, 6)
>>> TODAY + three_mon_rel + three_mon_rel + three_mon_rel + three_mon_rel
datetime.date(2013, 3, 6)

mVChr的建议解决方案相比,虽然绝对“足够好”,但随着时间的推移略微偏离:

>>> three_mon_timedelta = datetime.timedelta(days=3 * 365/12)
>>> TODAY + three_mon_timedelta
datetime.date(2012, 6, 5)

在一年的时间里,每个月的日期都在滑动:

>>> TODAY + three_mon_timedelta * 2
datetime.date(2012, 9, 4)
>>> TODAY + three_mon_timedelta * 3
datetime.date(2012, 12, 4)
>>> TODAY + three_mon_timedelta * 4
datetime.date(2013, 3, 5)

10
relativedelta(months=3)relativedelta(month=3)的行为非常不同,我曾经很费劲才发现我在“month”上错拼了一个字母's',也许在你的很好的回答中提到这点是有帮助的 :) - dulaccc
1
好的,谢谢你的提醒!我已经更新了响应并且顺便修正了一个错别字。 :) - jathanism
对于 dateutil 模块,给予大力支持 +1。 - xliiv
这个问题与月份中天数较少的情况有关。不要使用“TODAY + delta + delta ...”,而应该使用“delta + delta + TODAY”。这样可以确保即使在途中遇到二月份,你也会落在所结束的月份最好的一天上。 - Adam Barnes

10
import datetime

some_date = datetime.date.today()
three_months = datetime.timedelta(3*365/12)
print (some_date + three_months).isoformat()
# => '2012-06-01'

然后将每个新年“归一化”为原始日期的那一天(除非是2月29日)


1
这是一个“足够好”的解决方案。我喜欢使用 3 * 365 / 12 而不是 3 * 30 - jathanism
你设置的日期是2012年6月5日吗?因为今天是三月六号。 - jgritty
3
这里的“three_months”指的是确切的91天,除非你使用“3*365.0/12”,这样就是91天6小时了。 - jgritty
哎呀,是的,在我的虚拟机上测试过了。谢谢。 - mVChr
你能解释一下为什么我们需要做 month*days-in-a-year/months-in-a-year 吗? - dataviews

2
使用Python标准库,即不使用dateutil或其他库,并解决“2月31日”问题:
import datetime
import calendar

def add_months(date, months):
    months_count = date.month + months

    # Calculate the year
    year = date.year + int(months_count / 12)

    # Calculate the month
    month = (months_count % 12)
    if month == 0:
        month = 12

    # Calculate the day
    day = date.day
    last_day_of_month = calendar.monthrange(year, month)[1]
    if day > last_day_of_month:
        day = last_day_of_month

    new_date = datetime.date(year, month, day)
    return new_date

测试:

>>>date = datetime.date(2018, 11, 30)

>>>print(date, add_months(date, 3))
(datetime.date(2018, 11, 30), datetime.date(2019, 2, 28))

>>>print(date, add_months(date, 14))
(datetime.date(2018, 12, 31), datetime.date(2020, 2, 29))

如果 months 小于等于 -date.month,则无法处理负值。例如,add_months(datetime.date(2020,7,30), -7) = datetime.date(2020, 12, 30)。 - AlexM
这段代码存在一个错误,当月份为11且尝试增加一个月时,add_months(datetime.date(2020, 11, 1), 1) 的输出结果为 datetime.date(2021, 12, 1)。修复方法是将 int(months_count / 12) 更改为 int(months_count / 13) 或者更好的方法是使用 months_count // 13 - Ron Serruya

2

@david-ragazzi和@mikedugas77的答案适用于正值months,但是如果months <= -date.month则不适用。这里有一个修改版,即使对于负月偏移量也应该有效:

import calendar
import datetime

def add_months(date, months):
    months_count = date.year * 12 + date.month + months - 1

    # Calculate the year
    year = months_count // 12

    # Calculate the month
    month = months_count % 12 + 1

    # Calculate the day
    day = date.day
    last_day_of_month = calendar.monthrange(year, month)[1]
    if day > last_day_of_month:
        day = last_day_of_month

    new_date = datetime.date(year, month, day)
    return new_date

使用今天的日期进行一些测试:

date = datetime.date(2020, 7, 30)
assert add_months(date, -12) == datetime.date(2019, 7, 30)
assert add_months(date, -11) == datetime.date(2019, 8, 30)
assert add_months(date, -10) == datetime.date(2019, 9, 30)
assert add_months(date, -9) == datetime.date(2019, 10, 30)
assert add_months(date, -8) == datetime.date(2019, 11, 30)
assert add_months(date, -7) == datetime.date(2019, 12, 30)
assert add_months(date, -6) == datetime.date(2020, 1, 30)
assert add_months(date, -5) == datetime.date(2020, 2, 29)
assert add_months(date, -4) == datetime.date(2020, 3, 30)
assert add_months(date, -3) == datetime.date(2020, 4, 30)
assert add_months(date, -2) == datetime.date(2020, 5, 30)
assert add_months(date, -1) == datetime.date(2020, 6, 30)
assert add_months(date, 0) == datetime.date(2020, 7, 30)
assert add_months(date, 1) == datetime.date(2020, 8, 30)
assert add_months(date, 2) == datetime.date(2020, 9, 30)
assert add_months(date, 3) == datetime.date(2020, 10, 30)
assert add_months(date, 4) == datetime.date(2020, 11, 30)
assert add_months(date, 5) == datetime.date(2020, 12, 30)
assert add_months(date, 6) == datetime.date(2021, 1, 30)
assert add_months(date, 7) == datetime.date(2021, 2, 28)
assert add_months(date, 8) == datetime.date(2021, 3, 30)
assert add_months(date, 9) == datetime.date(2021, 4, 30)
assert add_months(date, 10) == datetime.date(2021, 5, 30)
assert add_months(date, 11) == datetime.date(2021, 6, 30)
assert add_months(date, 12) == datetime.date(2021, 7, 30)

1
我写了这个函数,可能能帮到你:
import datetime
import calendar

def add_months(date, months):

    # Determine the month and year of the new date
    month, year = (date + relativedelta(months=months)).month, (date + relativedelta(months=months)).year

    # Determine the day of the new date
    # If the day of the current date is at the end of the month,
    # the day of the new date should also be at the end of the month
    if(date.day == calendar.monthrange(date.year, date.month)[1]):
        day = calendar.monthrange(year, month)[1]
    else:
        day = date.day

    new_date = datetime.datetime(year, month, day)
    return new_date

它也支持添加负数月份(即减去月份)。
以下是一些示例用法,说明如何按照您的规格获取2021年和2022年的日期:
import datetime

a = datetime.datetime(2020, 1, 1)

# Initialse a list to hold the dates
dates = [0]*8

# Obtain the dates
for i in range(0, len(dates)):
    dates[i] = add_months(a, 3*i)

dates

你能否同时提供一个示例代码来解决原始问题? - William Baker Morrison
现在请看一下。添加了2021年和2022年的样本。 - slater2809

0

我没有足够的声望来评论。所以,我只是写了一个解决方案,修复了David Ragazzi发布的解决方案中的一个错误。

当您添加足够的月份以达到12月日期时,会出现错误。年份多了1年。

例如,add_months(date.fromisoformat('2020-01-29'), 11)返回2021而不是2020。我通过更改以year =开头的行来解决了这个问题。

    import datetime
    import calendar
    def add_months(dateInput, months):
        months_count = dateInput.month + months

        # Calculate the year
        year = dateInput.year + int((months_count-1) / 12)

        # Calculate the month
        month = (months_count % 12)
        if month == 0:
            month = 12

        # Calculate the day
        day = dateInput.day
        last_day_of_month = calendar.monthrange(year, month)[1]
        if day > last_day_of_month:
            day = last_day_of_month

        new_date = date(year, month, day)
        return new_date

如果 months 小于等于 -date.month,则无法处理负值。例如,add_months(datetime.date(2020,7,30), -7) = datetime.date(2020, 12, 30)。 - AlexM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接