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
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Dec 24, 2012 at 19:49
\$\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
answered Dec 24, 2012 at 22:52
\$\endgroup\$
1
  • \$\begingroup\$ Excellent answer. Not sure which solution I will use, but thanks! \$\endgroup\$ Commented Dec 25, 2012 at 22:38

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.