6
\$\begingroup\$

My solution to this feels 'icky' and I've got calendar math falling out of my ears after working on similar problems for a week so I can't think straight about this.

Is there a better way to code this?

import datetime
from dateutil.relativedelta import relativedelta


def date_count(start, end, day_of_month=1):
    """
    Return a list of datetime.date objects that lie in-between start and end.

    The first element of the returned list will always be start and the last
    element in the returned list will always be:
        datetime.date(end.year, end.month, day_of_month)

    If start.day is equal to day_of_month the second element will be:
        start + 1 month

    If start.day is after day_of_month then the second element will be:
        the day_of_month in the next month

    If start.day is before day_of_month then the second element will be:
        datetime.date(start.year, start.month, day_of_month)


    >>> start = datetime.date(2012, 1, 15)
    >>> end = datetime.date(2012, 4, 1)
    >>> date_count(start, end, day_of_month=1) #doctest: +NORMALIZE_WHITESPACE
    [datetime.date(2012, 1, 15), datetime.date(2012, 2, 1),
    datetime.date(2012, 3, 1), datetime.date(2012, 4, 1)]

    Notice that it's not a full month between the first two elements in the
    list.

    If you have a start day before day_of_month:
    >>> start = datetime.date(2012, 1, 10)
    >>> end = datetime.date(2012, 4, 1)
    >>> date_count(start, end, day_of_month=15) #doctest: +NORMALIZE_WHITESPACE
    [datetime.date(2012, 1, 10), datetime.date(2012, 1, 15),
    datetime.date(2012, 2, 15), datetime.date(2012, 3, 15),
    datetime.date(2012, 4, 15)]

    Notice that it's not a full month between the first two elements in the
    list and that the last day is rounded to
    datetime.date(end.year, end.month, day_of_month)
    """
    last_element = datetime.date(end.year, end.month, day_of_month)

    if start.day == day_of_month:
        second_element = start + relativedelta(start, months=+1)
    elif start.day > day_of_month:
        _ = datetime.date(start.year, start.month, day_of_month)
        second_element = _ + relativedelta(_, months=+1)
    else:
        second_element = datetime.date(start.year, start.month, day_of_month)

    dates = [start, second_element]

    if last_element <= second_element:
        return dates

    while dates[-1] < last_element:
        next_date = dates[-1] + relativedelta(dates[-1], months=+1)
        next_date = datetime.date(next_date.year, next_date.month, day_of_month)
        dates.append(next_date)
    dates.pop()

    dates.append(last_element)

    return dates
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

How about...

def date_count(start, end, day_of_month=1):
    dates = [start]
    next_date = start.replace(day=day_of_month)
    if day_of_month > start.day:
        dates.append(next_date)
    while next_date < end.replace(day=day_of_month):
        next_date += relativedelta(next_date, months=+1)
        dates.append(next_date)
    return dates

And by the way it seems like a nice opportunity to use yield, if you wanted to.

def date_count2(start, end, day_of_month=1):
    yield start
    next_date = start.replace(day=day_of_month)
    if day_of_month > start.day:
        yield next_date
    while next_date < end.replace(day=day_of_month):
        next_date += relativedelta(next_date, months=+1)
        yield next_date

Another possibility - discard the first value if it is earlier than the start date:

def date_count(start, end, day_of_month=1):
    dates = [start.replace(day=day_of_month)]
    while dates[-1] < end.replace(day=day_of_month):
        dates.append(dates[-1] + relativedelta(dates[-1], months=+1))
    if dates[0] > start:
        return [start] + dates
    else:
        return [start] + dates[1:]

Or use a list comprehension to iterate over the number of months between start and end.

def date_count(start, end, day_of_month=1):
    round_start = start.replace(day=day_of_month)
    gap = end.year * 12 + end.month - start.year * 12 - start.month + 1
    return [start] + [round_start + relativedelta(round_start, months=i) 
                      for i in range(day_of_month <= start.day, gap)]

Finally, I don't know dateutil but it seems you can use rrule:

from dateutil import rrule
def date_count(start, end, day_of_month=1):
    yield start
    for date in rrule(MONTHLY, 
                      dtstart=start.replace(day=day_of_month),
                      until=end.replace(day=day_of_month)):
        if date > start:
            yield date
\$\endgroup\$
1
  • \$\begingroup\$ Excellent answer. Not sure which solution I will use, but thanks! \$\endgroup\$ Commented Dec 25, 2012 at 22:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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