2
\$\begingroup\$

I have a start and end date. I would like to return a list of tuples, which have the start and end dates of each month.

Input
start_date = '2020-10-14'
end_date = '2021-03-26'

Output
[('2020-10-14', '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-26')]

My code

from datetime import datetime, timedelta, date
import calendar

def get_first_day(year, month):
    return f'{year}-{str(month).zfill(2)}-01'

def get_last_day(year, month):
    # https://stackoverflow.com/a/23447523/5675288
    dt = datetime.strptime(f'{year}-{month}', '%Y-%m')
    dt = (dt.replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1)
    return dt.strftime('%Y-%m-%d')

def diff_month(d1, d2):
    # https://stackoverflow.com/a/4040338/5675288
    return abs((d1.year - d2.year) * 12 + d1.month - d2.month)

def add_months(sourcedate, months):
    # https://stackoverflow.com/a/4131114/5675288
    month = sourcedate.month - 1 + months
    year = sourcedate.year + month // 12
    month = month % 12 + 1
    day = min(sourcedate.day, calendar.monthrange(year,month)[1])
    return date(year, month, day)

def get_dates(start_date, end_date):
    """get list of tuples of dates
    [
        (start_date, end-of-month),
        (start-of-next-month, end-of-next-month),
        (start-of-next-next-month, end-of-next-next-month),
        ...
        (start-of-last-month, end_date)
    ]
    """
    sd = datetime.strptime(f'{start_date}', '%Y-%m-%d')
    ed = datetime.strptime(f'{end_date}', '%Y-%m-%d')

    diff_months = diff_month(sd, ed)

    first_days = []
    last_days = []

    for month in range(diff_months+1):
        d = add_months(sd, month)
        first_days.append(get_first_day(d.year, d.month))
        last_days.append(get_last_day(d.year, d.month))

    first_days = [start_date] + first_days[1:]
    last_days = last_days[:-1] + [end_date]

    return list(zip(first_days, last_days))

start_date = '2020-10-14'
end_date = '2021-03-26'

print(get_dates(start_date, end_date))

Is there a cleaner / better approach for this?

\$\endgroup\$
4
\$\begingroup\$

None of this code should deal in strings, except maybe the final output stage. It can all be thrown away in favour of a single generator function that uses the highly-useful dateutil library. Also type hints will help:

from datetime import date
from typing import Iterable, Tuple
from dateutil.relativedelta import relativedelta


def get_dates(start: date, end: date) -> Iterable[Tuple[date, date]]:

    while True:
        next_start = start + relativedelta(months=1, day=1)
        this_end = next_start - relativedelta(days=1)

        if end <= this_end:
            yield start, end
            break

        yield start, this_end
        start = next_start


def main() -> None:
    start = date.fromisoformat(input('Enter start date: '))
    end = date.fromisoformat(input('Enter end date: '))
    for range_start, range_end in get_dates(start, end):
        print(f'{range_start.isoformat()} - {range_end.isoformat()}')

if __name__ == '__main__':
    main()

Output:

Enter start date: 2020-10-14
Enter end date: 2021-03-26
2020-10-14 - 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-26
\$\endgroup\$
2
  • \$\begingroup\$ Thank you for the answer. Although it makes sense that I should not be doing this with strings, my use case is that I will be getting the start and end dates as strings from the user, and the date tuples will be used as parameters to request from an API. That's why I was returning them as strings as well. I do like your solution though, maybe I can make slight modifications to get it as strings \$\endgroup\$ – scientific_explorer Apr 27 at 15:38
  • 1
    \$\begingroup\$ Just wrap the solution with parse calls beforehand, and formatting calls after. \$\endgroup\$ – Reinderien Apr 27 at 15:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.