两个日期之间找到月份的最佳方法

143

我需要在Python中准确地找到两个日期之间的月份。 我有一个解决方案,但它不是非常好(即优雅)或快速。

dateRange = [datetime.strptime(dateRanges[0], "%Y-%m-%d"), datetime.strptime(dateRanges[1], "%Y-%m-%d")]
months = [] 

tmpTime = dateRange[0]
oneWeek = timedelta(weeks=1)
tmpTime = tmpTime.replace(day=1)
dateRange[0] = tmpTime
dateRange[1] = dateRange[1].replace(day=1)
lastMonth = tmpTime.month
months.append(tmpTime)
while tmpTime < dateRange[1]:
    if lastMonth != 12:
        while tmpTime.month <= lastMonth:
            tmpTime += oneWeek
        tmpTime = tmpTime.replace(day=1)
        months.append(tmpTime)
        lastMonth = tmpTime.month

    else:
        while tmpTime.month >= lastMonth:
            tmpTime += oneWeek
        tmpTime = tmpTime.replace(day=1)
        months.append(tmpTime)
        lastMonth = tmpTime.month

所以,我在这里的做法是将两个日期从ISO格式转换为Python datetime对象。然后我循环添加一个星期到开始日期对象,并检查月份的数值是否更大(除非月份是12月,则检查日期是否更小)。如果值更大,则将其附加到月份列表中,并继续循环直到达到结束日期。

它完美地工作,只是似乎不是一种好的方法...


你是在询问两个日期之间的月份数量,还是实际的月份是什么? - Charles Hooper
在我的解决方案中:我并没有按“一个月的秒数”递增。我只是将数字1递增到2,然后再从2递增到3。 - nonopolarity
我只是想让你知道,即使你不喜欢我的答案,因为它“有一个循环”,但你选择了一个有两个循环的答案。列表推导仍然是循环。 - Charles Hooper
42个回答

271

先定义一些测试用例,然后你会发现这个函数非常简单,不需要循环。

from datetime import datetime

def diff_month(d1, d2):
    return (d1.year - d2.year) * 12 + d1.month - d2.month

assert diff_month(datetime(2010,10,1), datetime(2010,9,1)) == 1
assert diff_month(datetime(2010,10,1), datetime(2009,10,1)) == 12
assert diff_month(datetime(2010,10,1), datetime(2009,11,1)) == 11
assert diff_month(datetime(2010,10,1), datetime(2009,8,1)) == 14

你应该在问题中添加一些测试用例,因为有很多潜在的边界情况需要考虑——定义两个日期之间的月份数不止一种方式。


6
结果不正确。在“2015-04-30”和“2015-05-01”之间,它给出了一个月的结果,实际上只是1天。 - Rao
29
@Rao,这就是为什么我说“定义两个日期之间月份的方法不止一种”。问题仍然缺乏正式定义。这也是我建议在定义的同时提供测试用例的原因。 - John La Rooy
4
我建议在两个日期相减的周围添加abs()函数,以便允许d1小于d2:返回abs(d1.year - d2.year)*12 + abs(d1.month - d2.month) - lszrh
你确定吗,@LukasSchulze?如果d1小于d2,那你无论如何都需要从第一个数中减去这个数字,对吧? - Lilith-Elina
2
可能更容易使用pandas:pandas.date_range(start_date, end_date,freq='MS').strftime('%Y-%m').tolist() - Ankhnesmerira

61

一行代码:找到两个日期之间,按月递增的日期时间列表。

import datetime
from dateutil.rrule import rrule, MONTHLY

strt_dt = datetime.date(2001,1,1)
end_dt = datetime.date(2005,6,1)

dates = [dt for dt in rrule(MONTHLY, dtstart=strt_dt, until=end_dt)]

这个!这个通过使用rrule来适应许多花哨的特殊情况。请注意,输出值是日期时间,因此您可以将它们转换为任何您喜欢的格式(包括字符串),以匹配其他人显示的内容。 - Ezekiel Kruglick
由于2001年没有闰年,因此当strt_dt为2001-1-29时会失败。 - C S
4
OP 请求列出开始日期和结束日期之间的月份列表。在您提供的方法中,二月在我的例子里被遗漏了。当然,您可以通过将开始日期调整为该月的第一天来挽救您的解决方案。 - C S
2
这是一个不错的解决方案,它为我节省了很多时间。我们可以通过给出开始日期[dt for dt in rrule(MONTHLY, bymonthday=10,dtstart=strt_dt, until=end_dt)]来使它更好。 - Pengju Zhao
2
请注意。他们实际上在文档中有一条注释,说当开始日期出现在月底时,rrule可能会有“令人惊讶的行为”(请参见https://dateutil.readthedocs.io/en/stable/rrule.html中的注释)。避免这种情况的一种方法是将日期替换为该月的第一天: start_date_first = start_date.replace(day=1), end_date_first = end_date.replace(day=1) 然后rrule可以正确计算月份。 - Alla Sorokina
显示剩余3条评论

50

这对我有用 -

from datetime import datetime
from dateutil import relativedelta
date1 = datetime.strptime('2011-08-15 12:00:00', '%Y-%m-%d %H:%M:%S')
date2 = datetime.strptime('2012-02-15', '%Y-%m-%d')
r = relativedelta.relativedelta(date2, date1)
r.months + (12*r.years)

删除字符串字面量周围不必要的 str() - jfs
8
需要注意的是,如果时间间隔超过1年,r.months将从0开始计算。 - jbkkd
2
通常我会使用 r.months * (r.years+1) 进行计算,因为这样可以调整 @jbkkd 所说的问题。 - triunenature
11
@triunenature 这看起来有些不对,应该是 r.months + (r.years*12) 才对。 - Moataz Elmasry
2
是的,如果日期相隔超过一年,这个方法就不起作用了。 - HansG600
买家要小心 - 如果您想在11月和12月之间的某些任意日期之间显示1的差异,则无法实现。也就是说,当date1=date(2019,11,30)和date2=date(2019,12,29)时,您会得到0,而您可能想要1(就像我一样)。这在我的看来是一个重要的边缘案例。 - Sibs

19
from dateutil import relativedelta

r = relativedelta.relativedelta(date1, date2)

months_difference = (r.years * 12) + r.months

15
你可以使用dateutil模块中的rrule轻松计算此内容:
from dateutil import rrule
from datetime import date

print(list(rrule.rrule(rrule.MONTHLY, dtstart=date(2013, 11, 1), until=date(2014, 2, 1))))

会给你:

 [datetime.datetime(2013, 11, 1, 0, 0),
 datetime.datetime(2013, 12, 1, 0, 0),
 datetime.datetime(2014, 1, 1, 0, 0),
 datetime.datetime(2014, 2, 1, 0, 0)]

10

获取结束月份(相对于开始月份的年份和月份,例如:如果您的开始日期为2010年10月,则2011年1月= 13),然后生成从开始月份到该结束月份的日期时间,如下所示:

dt1, dt2 = dateRange
start_month=dt1.month
end_months=(dt2.year-dt1.year)*12 + dt2.month+1
dates=[datetime.datetime(year=yr, month=mn, day=1) for (yr, mn) in (
          ((m - 1) / 12 + dt1.year, (m - 1) % 12 + 1) for m in range(start_month, end_months)
      )]
如果两个日期在同一年,也可以简单地写成:
dates=[datetime.datetime(year=dt1.year, month=mn, day=1) for mn in range(dt1.month, dt2.month + 1)]

9

我的简单解决方案:

import datetime

def months(d1, d2):
    return d1.month - d2.month + 12*(d1.year - d2.year)

d1 = datetime.datetime(2009, 9, 26)  
d2 = datetime.datetime(2019, 9, 26) 

print(months(d1, d2))

被低估的解决方案 - reabow
2
尽管操作顺序不同,但它与被接受的答案完全相同。 - rnevius

9

这篇文章讲得很好!使用dateutil.relativedelta即可。

from datetime import datetime
from dateutil import relativedelta
date1 = datetime.strptime(str('2011-08-15 12:00:00'), '%Y-%m-%d %H:%M:%S')
date2 = datetime.strptime(str('2012-02-15'), '%Y-%m-%d')
r = relativedelta.relativedelta(date2, date1)
r.months

1
@edouard 如您所见,在我提供的示例中,日期处于不同的年份。尽管如此,我进行的 str() 转换是完全没有必要的。 - srodriguex
8
如果日期跨越一年以上,这个方法就无效了,你需要添加relativedelta.relativedelta(date2, date1).years * 12 - muon
1
delta.years*12 + delta.months - user2682863

6
更新于2018年4月20日:似乎OP @Joshkunz正在寻找两个日期之间的“哪些月份”,而不是两个日期之间的“多少个月”。因此,我不确定为什么@JohnLaRooy获得了100多次赞。@Joshkunz在原始问题下的评论中指出,他想要实际日期[或月份],而不是找到“总月数”。

因此,问题似乎想要找到从2018-04-112018-06-01之间的日期。

Apr 2018, May 2018, June 2018 

如果日期在 2014-04-112018-06-01 之间呢?那么答案就是:
Apr 2014, May 2014, ..., Dec 2014, Jan 2015, ..., Jan 2018, ..., June 2018

因此,多年前我有以下伪代码。它仅建议使用两个月作为端点并循环遍历它们,每次增加一个月。@Joshkunz提到他想要“月份”,他还提到他想要“日期”,但不知道确切的内容,很难编写准确的代码,但这个想法是使用一个简单的循环来遍历终点,并每次增加一个月。
8年前2010年的答案:
如果按周添加,则大约需要4.35倍的工作量。为什么不只是:
1. get start date in array of integer, set it to i: [2008, 3, 12], 
       and change it to [2008, 3, 1]
2. get end date in array: [2010, 10, 26]
3. add the date to your result by parsing i
       increment the month in i
       if month is >= 13, then set it to 1, and increment the year by 1
   until either the year in i is > year in end_date, 
           or (year in i == year in end_date and month in i > month in end_date)

目前只有伪代码,尚未测试,但我认为相同思路的想法会起作用。


1
好的,我发现如果按月增加而不是按周增加,像二月份这样的月份会出现问题。 - Joshkunz
我并不是按照“一个月的秒数”来递增。我只是将数字1递增到2,然后再从2递增到3 - nonopolarity

6

将“月”定义为1/12年,然后执行以下操作:

def month_diff(d1, d2): 
    """Return the number of months between d1 and d2, 
    such that d2 + month_diff(d1, d2) == d1
    """
    diff = (12 * d1.year + d1.month) - (12 * d2.year + d2.month)
    return diff

您可以将一个月定义为“29、28、30或31天的时间段(取决于年份)”。但是,这样做会带来另一个问题。通常情况下,6月15日 + 1个月应该是7月15日,这是很清楚的。但通常不清楚1月30日 + 1个月是在2月还是3月。在后一种情况下,您可能需要将日期计算为2月30日,然后“更正”为3月2日。但是,当您这样做时,您将发现3月2日 - 1个月显然是2月2日。因此,归谬法表明这种操作是没有被明确定义的。

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