Лучший способ найти месяцы между двумя датами
У меня есть необходимость быть в состоянии точно найти месяцы между двумя датами в 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. Затем я циклически добавляю неделю к объекту start datetime и проверяю, больше ли числовое значение месяца (если месяц не декабрь, то он проверяет если дата меньше), если значение больше, я добавляю его в список месяцев и продолжаю цикл, пока не доберусь до своей конечной даты.
он отлично работает, это просто не кажется хорошим способом сделать это...
27 ответов:
обновление 2018-04-20: похоже, что OP @Joshkunz просил найти какие месяцы между двумя датами, а не "сколько месяцев" между двумя датами. Поэтому я не уверен, почему @JohnLaRooy поддерживается более чем в 100 раз. @Joshkunz указал в комментарии под исходным вопросом, что он хотел фактические даты [или месяцы], вместо того, чтобы найти общее количество месяцев.
так что, казалось, вопрос хотел, ибо между двумя датами
2018-04-11до2018-06-01Apr 2018, May 2018, June 2018а что если это между
2014-04-11до2018-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)просто код pseduo на данный момент, не тестировался, но я думаю, что идея по той же линии будет работать.
Начнем с определения некоторых тестовых случаев, то вы увидите, что функция очень проста и не нуждается в петли
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вы должны добавить некоторые тестовые случаи в свой вопрос, так как есть много потенциальных угловых случаев для покрытия - существует более одного способа определить количество месяцев между двумя датами.
один лайнер, чтобы найти список дат, увеличенных на месяц, между двумя датами.
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)]
это сработало для меня -
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 * (r.years+1)
получить конечный месяц (относительно года и месяца месяца начала ex: 2011 январь = 13, Если ваша дата начала начинается на 2010 Oct), а затем генерировать даты начала месяца начала и этого конца месяца, как так:
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)]
вы можете легко вычислить это с помощью rrule from dateutil модуль:
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)]
этот пост ногти! Используйте
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
несколько немного приукрашенное решение @Vin-G.
import datetime def monthrange(start, finish): months = (finish.year - start.year) * 12 + finish.month + 1 for i in xrange(start.month, months): year = (i - 1) / 12 + start.year month = (i - 1) % 12 + 1 yield datetime.date(year, month, 1)
попробуйте что-то вроде этого. В настоящее время он включает месяц, если обе даты находятся в одном месяце.
from datetime import datetime,timedelta def months_between(start,end): months = [] cursor = start while cursor <= end: if cursor.month not in months: months.append(cursor.month) cursor += timedelta(weeks=1) return monthsвыход выглядит так:
>>> start = datetime.now() - timedelta(days=120) >>> end = datetime.now() >>> months_between(start,end) [6, 7, 8, 9, 10]
вы могли бы использовать python-dateutil. Смотрите Python: разница в 2 даты в месяцах
вы также можете использовать стрелка библиотека. Это простой пример:
from datetime import datetime import arrow start = datetime(2014, 1, 17) end = datetime(2014, 6, 20) for d in arrow.Arrow.range('month', start, end): print d.month, d.format('MMMM')это будет напечатано:
1 January 2 February 3 March 4 April 5 May 6 Juneнадеюсь, что это помогает!
определить "месяц" как 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 дней (в зависимости от года)". Но вы делаете это, у вас есть дополнительная проблема для решения.
хотя обычно понятно, что 15 июняth + 1 месяц должен быть 15 июляth, это обычно не ясно, если 30 январяth + 1 месяц в феврале или марте. В в последнем случае вы можете быть вынуждены вычислить дату, как 30 февраляth, затем "исправьте" его до 2 марта nd. Но когда вы это сделаете, вы обнаружите, что 2 марта nd - 1 месяц явно 2 февраля nd. Ergo, reductio ad absurdum (эта операция не очень хорошо определена).
существует простое решение, основанное на 360 дневных годах, где все месяцы имеют 30 дней. Он подходит для большинства случаев использования, когда, учитывая две даты, вам нужно рассчитать количество полных месяцев плюс оставшиеся дни.
from datetime import datetime, timedelta def months_between(start_date, end_date): #Add 1 day to end date to solve different last days of month s1, e1 = start_date , end_date + timedelta(days=1) #Convert to 360 days s360 = (s1.year * 12 + s1.month) * 30 + s1.day e360 = (e1.year * 12 + e1.month) * 30 + e1.day #Count days between the two 360 dates and return tuple (months, days) return divmod(e360 - s360, 30) print "Counting full and half months" print months_between( datetime(2012, 01, 1), datetime(2012, 03, 31)) #3m print months_between( datetime(2012, 01, 1), datetime(2012, 03, 15)) #2m 15d print months_between( datetime(2012, 01, 16), datetime(2012, 03, 31)) #2m 15d print months_between( datetime(2012, 01, 16), datetime(2012, 03, 15)) #2m print "Adding +1d and -1d to 31 day month" print months_between( datetime(2011, 12, 01), datetime(2011, 12, 31)) #1m 0d print months_between( datetime(2011, 12, 02), datetime(2011, 12, 31)) #-1d => 29d print months_between( datetime(2011, 12, 01), datetime(2011, 12, 30)) #30d => 1m print "Adding +1d and -1d to 29 day month" print months_between( datetime(2012, 02, 01), datetime(2012, 02, 29)) #1m 0d print months_between( datetime(2012, 02, 02), datetime(2012, 02, 29)) #-1d => 29d print months_between( datetime(2012, 02, 01), datetime(2012, 02, 28)) #28d print "Every month has 30 days - 26/M to 5/M+1 always counts 10 days" print months_between( datetime(2011, 02, 26), datetime(2011, 03, 05)) print months_between( datetime(2012, 02, 26), datetime(2012, 03, 05)) print months_between( datetime(2012, 03, 26), datetime(2012, 04, 05))
#This definition gives an array of months between two dates. import datetime def MonthsBetweenDates(BeginDate, EndDate): firstyearmonths = [mn for mn in range(BeginDate.month, 13)]<p> lastyearmonths = [mn for mn in range(1, EndDate.month+1)]<p> months = [mn for mn in range(1, 13)]<p> numberofyearsbetween = EndDate.year - BeginDate.year - 1<p> return firstyearmonths + months * numberofyearsbetween + lastyearmonths<p> #example BD = datetime.datetime.strptime("2000-35", '%Y-%j') ED = datetime.datetime.strptime("2004-200", '%Y-%j') MonthsBetweenDates(BD, ED)
обычно 90 дней не 3 месяца буквально, просто ссылка.
Итак, наконец, вам нужно проверить, если дни больше, чем 15, чтобы добавить +1 к счетчику месяца. или лучше, добавьте еще один elif с половиной месяца счетчика.
С этот другой ответ stackoverflow я, наконец, закончил с этим:
#/usr/bin/env python # -*- coding: utf8 -*- import datetime from datetime import timedelta from dateutil.relativedelta import relativedelta import calendar start_date = datetime.date.today() end_date = start_date + timedelta(days=111) start_month = calendar.month_abbr[int(start_date.strftime("%m"))] print str(start_date) + " to " + str(end_date) months = relativedelta(end_date, start_date).months days = relativedelta(end_date, start_date).days print months, "months", days, "days" if days > 16: months += 1 print "around " + str(months) + " months", "(", for i in range(0, months): print calendar.month_abbr[int(start_date.strftime("%m"))], start_date = start_date + relativedelta(months=1) print ")"выход:
2016-02-29 2016-06-14 3 months 16 days around 4 months ( Feb Mar Apr May )Я заметил, что это не работает, если вы добавляете больше дней, оставшихся в текущем году, и это неожиданный.
вот как это сделать с пандами FWIW:
import pandas as pd pd.date_range("1990/04/03", "2014/12/31", freq="MS") DatetimeIndex(['1990-05-01', '1990-06-01', '1990-07-01', '1990-08-01', '1990-09-01', '1990-10-01', '1990-11-01', '1990-12-01', '1991-01-01', '1991-02-01', ... '2014-03-01', '2014-04-01', '2014-05-01', '2014-06-01', '2014-07-01', '2014-08-01', '2014-09-01', '2014-10-01', '2014-11-01', '2014-12-01'], dtype='datetime64[ns]', length=296, freq='MS')обратите внимание, что он начинается с месяца после указанной даты начала.
Это можно сделать с помощью datetime.timedelta, где количество дней для перехода на следующий месяц можно получить по календарю.месяц назад. monthrange возвращает будний день (0-6 ~ Пн-Вс) и количество дней (28-31) для данного года и месяца.
Например: monthrange (2017, 1) возвращает (6,31).вот скрипт, использующий эту логику для итерации между двумя месяцами.
from datetime import timedelta import datetime as dt from calendar import monthrange def month_iterator(start_month, end_month): start_month = dt.datetime.strptime(start_month, '%Y-%m-%d').date().replace(day=1) end_month = dt.datetime.strptime(end_month, '%Y-%m-%d').date().replace(day=1) while start_month <= end_month: yield start_month start_month = start_month + timedelta(days=monthrange(start_month.year, start_month.month)[1])'
предполагая, что upperDate всегда позже, чем lowerDate, и оба являются datetime.объекты даты:
if lowerDate.year == upperDate.year: monthsInBetween = range( lowerDate.month + 1, upperDate.month ) elif upperDate.year > lowerDate.year: monthsInBetween = range( lowerDate.month + 1, 12 ) for year in range( lowerDate.year + 1, upperDate.year ): monthsInBetween.extend( range(1,13) ) monthsInBetween.extend( range( 1, upperDate.month ) )Я не проверял это тщательно, но похоже, что это должно сделать трюк.
вот такой метод:
def months_between(start_dt, stop_dt): month_list = [] total_months = 12*(stop_dt.year-start_dt.year)+(stop_dt.month-start_d.month)+1 if total_months > 0: month_list=[ datetime.date(start_dt.year+int((start_dt+i-1)/12), ((start_dt-1+i)%12)+1, 1) for i in xrange(0,total_months) ] return month_listЭто первое вычисление общего количества месяцев между двумя датами, включительно. Затем он создает список, используя первую дату в качестве базы и выполняет арифметику модуля для создания объектов даты.
мне действительно нужно было сделать что-то очень похожее прямо сейчас
закончил писать функцию, которая возвращает список кортежей с указанием
startиendкаждого месяца между двумя наборами дат, чтобы я мог написать некоторые SQL-запросы с обратной стороны для ежемесячных итогов продаж и т. д.Я уверен, что это может быть улучшено кем-то, кто знает, что они делают, но надеюсь, что это поможет...
возвращаемое значение выглядит следующим образом (генерация на сегодня - 365days до сегодняшнего дня в качестве примера)
[ (datetime.date(2013, 5, 1), datetime.date(2013, 5, 31)), (datetime.date(2013, 6, 1), datetime.date(2013, 6, 30)), (datetime.date(2013, 7, 1), datetime.date(2013, 7, 31)), (datetime.date(2013, 8, 1), datetime.date(2013, 8, 31)), (datetime.date(2013, 9, 1), datetime.date(2013, 9, 30)), (datetime.date(2013, 10, 1), datetime.date(2013, 10, 31)), (datetime.date(2013, 11, 1), datetime.date(2013, 11, 30)), (datetime.date(2013, 12, 1), datetime.date(2013, 12, 31)), (datetime.date(2014, 1, 1), datetime.date(2014, 1, 31)), (datetime.date(2014, 2, 1), datetime.date(2014, 2, 28)), (datetime.date(2014, 3, 1), datetime.date(2014, 3, 31)), (datetime.date(2014, 4, 1), datetime.date(2014, 4, 30)), (datetime.date(2014, 5, 1), datetime.date(2014, 5, 31))]код выглядит следующим образом (некоторые отладочные вещи, которые могут быть удалены):
#! /usr/env/python import datetime def gen_month_ranges(start_date=None, end_date=None, debug=False): today = datetime.date.today() if not start_date: start_date = datetime.datetime.strptime( "{0}/01/01".format(today.year),"%Y/%m/%d").date() # start of this year if not end_date: end_date = today if debug: print("Start: {0} | End {1}".format(start_date, end_date)) # sense-check if end_date < start_date: print("Error. Start Date of {0} is greater than End Date of {1}?!".format(start_date, end_date)) return None date_ranges = [] # list of tuples (month_start, month_end) current_year = start_date.year current_month = start_date.month while current_year <= end_date.year: next_month = current_month + 1 next_year = current_year if next_month > 12: next_month = 1 next_year = current_year + 1 month_start = datetime.datetime.strptime( "{0}/{1}/01".format(current_year, current_month),"%Y/%m/%d").date() # start of month month_end = datetime.datetime.strptime( "{0}/{1}/01".format(next_year, next_month),"%Y/%m/%d").date() # start of next month month_end = month_end+datetime.timedelta(days=-1) # start of next month less one day range_tuple = (month_start, month_end) if debug: print("Month runs from {0} --> {1}".format( range_tuple[0], range_tuple[1])) date_ranges.append(range_tuple) if current_month == 12: current_month = 1 current_year += 1 if debug: print("End of year encountered, resetting months") else: current_month += 1 if debug: print("Next iteration for {0}-{1}".format( current_year, current_month)) if current_year == end_date.year and current_month > end_date.month: if debug: print("Final month encountered. Terminating loop") break return date_ranges if __name__ == '__main__': print("Running in standalone mode. Debug set to True") from pprint import pprint pprint(gen_month_ranges(debug=True), indent=4) pprint(gen_month_ranges(start_date=datetime.date.today()+datetime.timedelta(days=-365), debug=True), indent=4)
предполагая, что вы хотели знать "долю" месяца, в котором были даты, что я и сделал, тогда вам нужно сделать немного больше работы.
from datetime import datetime, date import calendar def monthdiff(start_period, end_period, decimal_places = 2): if start_period > end_period: raise Exception('Start is after end') if start_period.year == end_period.year and start_period.month == end_period.month: days_in_month = calendar.monthrange(start_period.year, start_period.month)[1] days_to_charge = end_period.day - start_period.day+1 diff = round(float(days_to_charge)/float(days_in_month), decimal_places) return diff months = 0 # we have a start date within one month and not at the start, and an end date that is not # in the same month as the start date if start_period.day > 1: last_day_in_start_month = calendar.monthrange(start_period.year, start_period.month)[1] days_to_charge = last_day_in_start_month - start_period.day +1 months = months + round(float(days_to_charge)/float(last_day_in_start_month), decimal_places) start_period = datetime(start_period.year, start_period.month+1, 1) last_day_in_last_month = calendar.monthrange(end_period.year, end_period.month)[1] if end_period.day != last_day_in_last_month: # we have lest days in the last month months = months + round(float(end_period.day) / float(last_day_in_last_month), decimal_places) last_day_in_previous_month = calendar.monthrange(end_period.year, end_period.month - 1)[1] end_period = datetime(end_period.year, end_period.month - 1, last_day_in_previous_month) #whatever happens, we now have a period of whole months to calculate the difference between if start_period != end_period: months = months + (end_period.year - start_period.year) * 12 + (end_period.month - start_period.month) + 1 # just counter for any final decimal place manipulation diff = round(months, decimal_places) return diff assert monthdiff(datetime(2015,1,1), datetime(2015,1,31)) == 1 assert monthdiff(datetime(2015,1,1), datetime(2015,02,01)) == 1.04 assert monthdiff(datetime(2014,1,1), datetime(2014,12,31)) == 12 assert monthdiff(datetime(2014,7,1), datetime(2015,06,30)) == 12 assert monthdiff(datetime(2015,1,10), datetime(2015,01,20)) == 0.35 assert monthdiff(datetime(2015,1,10), datetime(2015,02,20)) == 0.71 + 0.71 assert monthdiff(datetime(2015,1,31), datetime(2015,02,01)) == round(1.0/31.0,2) + round(1.0/28.0,2) assert monthdiff(datetime(2013,1,31), datetime(2015,02,01)) == 12*2 + round(1.0/31.0,2) + round(1.0/28.0,2)предоставляет пример, который определяет количество месяцев между двумя датами включительно, включая долю каждого месяца, в котором находится дата. Это означает, что вы можете определить, сколько месяцев находится между 2015-01-20 и 2015-02-14, где доля даты в январе месяце определяется количеством дней в году. Январь; или в равной степени с учетом того, что количество дней в феврале может меняться из года в год.
для моей справки, этот код также находится на github -https://gist.github.com/andrewyager/6b9284a4f1cdb1779b10
попробуйте это:
dateRange = [datetime.strptime(dateRanges[0], "%Y-%m-%d"), datetime.strptime(dateRanges[1], "%Y-%m-%d")] delta_time = max(dateRange) - min(dateRange) #Need to use min(dateRange).month to account for different length month #Note that timedelta returns a number of days delta_datetime = (datetime(1, min(dateRange).month, 1) + delta_time - timedelta(days=1)) #min y/m/d are 1 months = ((delta_datetime.year - 1) * 12 + delta_datetime.month - min(dateRange).month) print monthsНе имеет значения, в каком порядке вы вводите даты, и он учитывает разницу в длинах месяцев.
это работает...
from datetime import datetime as dt from dateutil.relativedelta import relativedelta def number_of_months(d1, d2): months = 0 r = relativedelta(d1,d2) if r.years==0: months = r.months if r.years>=1: months = 12*r.years+r.months return months #example number_of_months(dt(2017,9,1),dt(2016,8,1))
from datetime import datetime def diff_month(start_date,end_date): qty_month = ((end_date.year - start_date.year) * 12) + (end_date.month - start_date.month) d_days = end_date.day - start_date.day if d_days >= 0: adjust = 0 else: adjust = -1 qty_month += adjust return qty_month diff_month(datetime.date.today(),datetime(2019,08,24)) #Examples: #diff_month(datetime(2018,02,12),datetime(2019,08,24)) = 18 #diff_month(datetime(2018,02,12),datetime(2018,08,10)) = 5
мое простое решение:
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))
вы могли бы использовать что-то вроде:
import datetime days_in_month = 365.25 / 12 # represent the average of days in a month by year month_diff = lambda end_date, start_date, precision=0: round((end_date - start_date).days / days_in_month, precision) start_date = datetime.date(1978, 12, 15) end_date = datetime.date(2012, 7, 9) month_diff(end_date, start_date) # should show 403.0 months
Comments