8

I have the following date range:

begin: 2018-02-15
end: 2018-04-23

I want to achieve the following:

["2018-02-15 - 2018-02-28", "2018-03-01 - 2018-03-31", "2018-04-01 - 2018-04-23"]

Essentially, I want to divide a given date range into months. I can't think of a way to accomplish this in Python.

I have considered the solution here, however, this splits the date range based on a specified interval. I want to be able to split a date range dynamically.

Hence, given a date range from 15 February 2018 to 23 April 2018, I want to be able to get the individual months in the range, like so:

  • 15 February 2018 to 28 February 2018
  • 01 March 2018 to 31 March 2018
  • 01 April 2018 to 23 April 2018
Brandon Chetty
  • 152
  • 1
  • 1
  • 9
  • there is similar question like this https://stackoverflow.com/questions/993358/creating-a-range-of-dates-in-python – ZhouQuan Jul 11 '18 at 20:22
  • Possible duplicate of: https://stackoverflow.com/questions/29721228/given-a-date-range-how-can-we-break-it-up-into-n-contiguous-sub-intervals – Harley Jul 11 '18 at 20:22
  • 1
    Possible duplicate of [Given a date range how can we break it up into N contiguous sub-intervals?](https://stackoverflow.com/questions/29721228/given-a-date-range-how-can-we-break-it-up-into-n-contiguous-sub-intervals) – Harley Jul 11 '18 at 20:23
  • 1
    What have you tried already? What did it produce and how does that fall short of what you are looking for? – Engineero Jul 11 '18 at 20:23
  • What's the difference with which you want to split the dates? Like your first date is 15, then it's 28, then it's 1, so it pretty much seem random – Sushant Jul 11 '18 at 20:25
  • @ThatBird: each section stops and starts at the next whole month. Februari had only 28 days in 2018. – Jongware Jul 11 '18 at 20:27
  • What about April? – Sushant Jul 11 '18 at 20:29
  • @ThatBird: it starts at the 1st but ends at the 23rd because that is the end of the original range. – Jongware Jul 11 '18 at 20:31
  • 1
    @Harley Thanks for the suggestion. I have looked at the top answer, but I'm unsure of how to adapt it to my needs. The answer splits the date range into a specified interval. I do not know how many days there could be in a specific month, hence, using the answer and splitting by an interval of 30 may not always be correct – Brandon Chetty Jul 11 '18 at 20:32
  • 2
    Had a similar need. Ended up using [arrow](https://arrow.readthedocs.io/en/latest/#ranges-spans). `arrow.Arrow.span_range('month', start, end)` does the trick. – Kiran Subbaraman Mar 19 '20 at 17:39
  • @KiranSubbaraman thanks for the suggestion to use arrow! – Try431 Oct 06 '21 at 17:57

8 Answers8

9

In a loop; starting at the first day continually add one day till you get to the end date; whenever the month changes save the dates.

import datetime
begin = '2018-02-15'
end = '2018-04-23'

dt_start = datetime.datetime.strptime(begin, '%Y-%m-%d')
dt_end = datetime.datetime.strptime(end, '%Y-%m-%d')
one_day = datetime.timedelta(1)
start_dates = [dt_start]
end_dates = []
today = dt_start
while today <= dt_end:
    #print(today)
    tomorrow = today + one_day
    if tomorrow.month != today.month:
        start_dates.append(tomorrow)
        end_dates.append(today)
    today = tomorrow

end_dates.append(dt_end)


out_fmt = '%d %B %Y'
for start, end in zip(start_dates,end_dates):
    print('{} to {}'.format(start.strftime(out_fmt), end.strftime(out_fmt)))

Result:

>>>
15 February 2018 to 28 February 2018
01 March 2018 to 31 March 2018
01 April 2018 to 23 April 2018
>>>

You could probably figure out a way to get a range of months between the start and end dates; create a datetime object for the first day of each of those months store them and the days just prior to them. Dates spanning a change of year might be problematic though.

wwii
  • 23,232
  • 7
  • 37
  • 77
  • Upvote for mentioning crossing the year as a problem! I added a quick fix to my function, just for that. – Jongware Jul 11 '18 at 21:29
  • I believe this is incorrect for the case when the start and end coincide with the first and last days of the month? – mcansado Mar 01 '19 at 11:42
  • @mcansado, I haven't tested it yet but what error do you *see*? an extra start date at the end? I did not think of that *situation* when I wrote it. Do you think it could be fixed with some extra logic? Is usr2564301 's answer better? – wwii Mar 01 '19 at 16:43
  • The error seems to be that, when the dates match exactly the start and end of a month, another pair is added where the start date is the first day of the following month and the last day is the last day of the previous month. There are ways of solving it for sure, I haven't written the code for it but just thought I'd mention it here for whoever ends up here :) – mcansado Mar 07 '19 at 10:11
  • @wwii Should be `today < dt_end`. I got an error for this test case `start = date(2021, 12, 31); end = date(2022, 1, 1)` – Andra Oct 13 '22 at 02:47
5

To work with convenient date objects, always use the standard module datetime. This wraps your string formatted dates, and allows easier calculations as well as tailored output formatting.

Unfortunately, it seems to miss one important piece of information: the last day of each month, given a year (which is necessary for Februari). There is an additional module calendar which returns the last day for a month, but since this is all you need of it and there is a simple datetime based function that does the same thing, I chose the latter.

With that, you can set any begin date and append it to your list, together with its last day of that month, then set begin to the next month's 1st and continue until you pass end.

A caveat/finetuning: I realized it would not work if both begin and end fall inside the same month. That needs an interim check, so I changed my initial while begin < end to while True and moved the check for crossing the end date into a separate line.

Also, to cross a year needs a separate test again, because else the statement month+1 will fail on December.

import datetime

# borrowed from https://stackoverflow.com/a/13565185
# as noted there, the calendar module has a function of its own
def last_day_of_month(any_day):
    next_month = any_day.replace(day=28) + datetime.timedelta(days=4)  # this will never fail
    return next_month - datetime.timedelta(days=next_month.day)

begin = "2018-02-15"
end = "2018-04-23"

def monthlist(begin,end):
    begin = datetime.datetime.strptime(begin, "%Y-%m-%d")
    end = datetime.datetime.strptime(end, "%Y-%m-%d")

    result = []
    while True:
        if begin.month == 12:
            next_month = begin.replace(year=begin.year+1,month=1, day=1)
        else:
            next_month = begin.replace(month=begin.month+1, day=1)
        if next_month > end:
            break
        result.append ([begin.strftime("%Y-%m-%d"),last_day_of_month(begin).strftime("%Y-%m-%d")])
        begin = next_month
    result.append ([begin.strftime("%Y-%m-%d"),end.strftime("%Y-%m-%d")])
    return result


date_list = monthlist(begin,end)
print (date_list)

results in

[ ['2018-02-15', '2018-02-28'],
  ['2018-03-01', '2018-03-31'],
  ['2018-04-01', '2018-04-23'] ]

(slightly formatted for readability only)

Jongware
  • 22,200
  • 8
  • 54
  • 100
4

If you don't mind using pandas, there's a nice helper date_range that will achieve what you want:

import pandas as pd
start = pd.Timestamp('20180215')
end = pd.Timestamp('20180423')

parts = list(pd.date_range(start, end, freq='M')) 
# parts = [Timestamp('2018-02-28 00:00:00', freq='M'), Timestamp('2018-03-31 00:00:00', freq='M')]

if start != parts[0]:
  parts.insert(0, start)
if end != parts[-1]:
  parts.append(end)
parts[0] -= pd.Timedelta('1d')  # we add back one day later

pairs = zip(map(lambda d: d + pd.Timedelta('1d'), parts[:-1]), parts[1:])

pairs_str = list(map(lambda t: t[0].strftime('%Y-%m-%d') + ' - ' + t[1].strftime('%Y-%m-%d'), pairs))

# pairs_str = ['2018-02-15 - 2018-02-28', '2018-03-01 - 2018-03-31', '2018-04-01 - 2018-04-23']
Richard
  • 828
  • 1
  • 8
  • 28
0

Using python calendar and accounting for change of the year

import calendar
from datetime import datetime
begin = '2018-02-15'
end= '2018-04-23'

begin_year, begin_month, begin_date = [int(i) for i in begin.split("-")]
end_year, end_month, end_date = [int(i) for i in end.split("-")]

years = end_year - begin_year
# if date range contains more than single year, we calculate total months
if years:
    months = (12 - begin_month) + end_month + (12 * (years - 1))
else:
    months = end_month - begin_month
dates = []
month = begin_month
year = begin_year

def create_datetime_object(y, m, d):
    return datetime.strptime('{}-{}-{}'.format(y, m, d), '%Y-%m-%d')
# append the first date
dates.append(create_datetime_object(begin_year, begin_month, begin_date))

for i in range(months+1):
    days_in_month = calendar.monthrange(year, month)[-1]
    if month == begin_month and year == begin_year:
        dates.append(create_datetime_object(begin_year, begin_month, days_in_month))
    elif month == end_month and year == end_year:
        dates.append(create_datetime_object(end_year, end_month, 1))
    else:
        dates.append(create_datetime_object(year, month, 1))
        dates.append(create_datetime_object(year, month, days_in_month))
    if month == 12:
        month = 0
        year += 1
    month += 1
# append the last date
dates.append(create_datetime_object(end_year, end_month, end_date))

And to get a list in the question, we could do something like -

dates = [datetime.strftime(dt, '%Y-%m-%d') for dt in dates]

Sushant
  • 3,499
  • 3
  • 17
  • 34
0

For people using Pendulum :

import pendulum
start = pendulum.now().subtract(months=6)
end = pendulum.today()
period = pendulum.period(start, end)

time_ranges = list(period.range("months"))

arr = []

for index, dt in enumerate(time_ranges):
    if index < len(time_ranges) - 1:
        start_range = time_ranges[index].format("YYYY-MM-D")
        end_range = time_ranges[index + 1].format("YYYY-MM-D")
        litt = F"{start_range} - {end_range}"
        print(litt)
        arr.append(litt)

print(arr)

More about period here

0

I had to do a similar manipulation and ended up building this function. I tested it on different use cases (different years, same month...) and it's working well.

It is inspired from S.Lott answer here Creating a range of dates in Python

import datetime

def get_segments(start_date, end_date):
    """
    Divides input date range into associated months periods

    Example:
        Input: start_date = 2018-02-15
               end_date   = 2018-04-23
        Output:
            ["2018-02-15 - 2018-02-28", 
             "2018-03-01 - 2018-03-31", 
             "2018-04-01 - 2018-04-23"]
    """
    curr_date = start_date
    curr_month = start_date.strftime("%m")
    segments = []

    loop = (curr_date!=end_date) 
    days_increment = 1

    while loop:
        # Get incremented date with 1 day
        curr_date = start_date + datetime.timedelta(days=days_increment)
        # Get associated month
        prev_month = curr_month
        curr_month = curr_date.strftime("%m")
        # Add to segments if new month
        if prev_month!=curr_month:
            # get start of segment
            if not segments:
                start_segment = start_date
            else:
                start_segment = segments[-1][1] + datetime.timedelta(days=1)
            # get end of segment
            end_segment = curr_date - datetime.timedelta(days=1)
            # define and add segment
            segment = [start_segment, end_segment]
            segments.append(segment)
        # stop if last day reached
        loop = (curr_date!=end_date) 
        # increment added days
        days_increment += 1

    if not segments or segments[-1][1]!=end_date:
        if not segments:
            start_last_segment = start_date
        else:
            start_last_segment = segments[-1][1] + datetime.timedelta(days=1)
        last_segment = [start_last_segment, end_date]
        segments.append(last_segment)

    for i in range(len(segments)):
        segments[i][0] = segments[i][0].strftime("%Y-%m-%d")
        segments[i][1] = segments[i][1].strftime("%Y-%m-%d")

    return segments

Here is an example:

start_date = datetime.datetime(2020, 5, 27)
end_date = datetime.datetime(2021, 3, 1)

segments = get_segments(start_date, end_date)

for seg in segments:
    print(seg)

Output:

['2020-05-27', '2020-05-31']
['2020-06-01', '2020-06-30']
['2020-07-01', '2020-07-31']
['2020-08-01', '2020-08-31']
['2020-09-01', '2020-09-30']
['2020-10-01', '2020-10-31']
['2020-11-01', '2020-11-30']
['2020-12-01', '2020-12-31']
['2021-01-01', '2021-01-31']
['2021-02-01', '2021-02-28']
['2021-03-01', '2021-03-01']
HLeb
  • 591
  • 5
  • 10
0

I extend the solution by @wwii

Now you will not have duplicate start and/or end dates

def date_range_split_monthly(begin, end):

    dt_start = datetime.strptime(begin, '%Y-%m-%d')
    dt_end = datetime.strptime(end, '%Y-%m-%d')
    one_day = timedelta(1)
    start_dates = [dt_start]
    end_dates = []
    today = dt_start
    while today <= dt_end:
        # print(today)
        tomorrow = today + one_day
        if tomorrow.month != today.month:
            if tomorrow <= dt_end:
                start_dates.append(tomorrow)
                end_dates.append(today)
        today = tomorrow

    end_dates.append(dt_end)

    return start_dates, end_dates
0

i'm quoting the comment of Kiran Subbaraman, just with addition of the exact keyword (otherwise, whole months will be returned even if ranges fall beyond the start or the end).

#!pip install arrow 
from arrow import Arrow
Arrow.span_range('month', start, end, exact=True)
Anatoly Alekseev
  • 2,011
  • 24
  • 27