How to create a month iterator
Question:
I would like to create a python function that would allow me to iterate over the months from a start point to a stop point. For example it would look something like
def months(start_month, start_year, end_month, end_year):
Calling months(8, 2010, 3, 2011)
would return:
((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))
The function could just return a tuple of tuples, but I would love to see it as a generator (ie using yield
).
I’ve checked the calendar
python module and it doesn’t appear to provide this functionality. I could write a nasty for
loop to do it easily enough, but I’m interested to see how gracefully it could be done by a pro.
Thanks.
Answers:
Perhaps the elegance or speed of this could be improved, but it’s the straightforward solution:
def months(start_month, start_year, end_month, end_year):
month, year = start_month, start_year
while True:
yield month, year
if (month, year) == (end_month, end_year):
return
month += 1
if (month > 12):
month = 1
year += 1
EDIT: And here’s a less straightforward one that avoids even needing to use yield
by using a generator comprehension:
def months2(start_month, start_year, end_month, end_year):
return (((m_y % 12) + 1, m_y / 12) for m_y in
range(12 * start_year + start_month - 1, 12 * end_year + end_month))
The calendar works like this.
def month_year_iter( start_month, start_year, end_month, end_year ):
ym_start= 12*start_year + start_month - 1
ym_end= 12*end_year + end_month - 1
for ym in range( ym_start, ym_end ):
y, m = divmod( ym, 12 )
yield y, m+1
All multiple-unit things work like this. Feet and Inches, Hours, Minutes and Seconds, etc., etc. The only thing that’s not this simple is months-days or months-weeks because months are irregular. Everything else is regular, and you need to work in the finest-grained units.
This isn’t as short as the other solutions, but it’s straightforward to understand. Basically, it has two branches.
- The start year is the same as the end year
- The start year is different from the end year
The latter case has three phases:
- From the start month to December of the start year
- Every month from each year between the start year to the end year
- From January to the end month of the end year
If the end year is the year after the start year, the second phase above is skipped (no need for an explicit test, the range is simply empty).
def months(start_month, start_year, end_month, end_year):
if start_year == end_year:
for month in xrange(start_month, end_month+1):
yield month, start_year
else:
for month in xrange(start_month, 13):
yield month, start_year
for year in xrange(start_year+1, end_year):
for month in xrange(1, 13):
yield month, year
for month in xrange(1, end_month+1):
yield end_month, end_year
For Python 3.x, change xrange
to range
.
Your question is a little ambiguous in that you ask for an iterator, but then show a function returning a tuple of tuples. So here’s both:
import calendar
import datetime
def months_iter(start_month, start_year, end_month, end_year):
start_date = datetime.date(start_year, start_month, 1)
end_date = datetime.date(end_year, end_month, 1)
date = start_date
while date <= end_date:
yield (date.month, date.year)
days_in_month = calendar.monthrange(date.year, date.month)[1]
date += datetime.timedelta(days_in_month)
def months(start_month, start_year, end_month, end_year):
return tuple(d for d in months_iter(start_month, start_year, end_month, end_year))
print months(8, 2010, 3, 2011)
# ((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))
Having a little fun with Python built-in iterators, but certainly not elegant 😉
from datetime import timedelta, date
class MonthRange:
def __init__ (self, date1, date2):
self.start_date = date1 - timedelta(days=1)
self.end_date = date2
self.data = self.start_date
def __iter__(self):
return self
def next(self):
if self.data >= self.end_date.replace(day=1) + timedelta(days=32):
raise StopIteration
ret = self.data
self.data = self.data + timedelta(days=32)
return ret.replace(day=1)
for x in MonthRange(date.today(), date(2012, 11, 01)):
print (x.year, x.month)
A simpler version of dfan’s approach, and also a simpler solution than S. Lott’s (no division, no modulo):
def months(start_month, start_year, end_month, end_year):
month, year = start_month, start_year
while (year, month) <= (end_year, end_month):
yield month, year
month += 1
if month > 12:
month = 1
year += 1
This approach is close to the method one would use if they had to do this by hand. It runs in the same amount of time as S. Lott’s (the tests in the code above take about as much time as a division and a modulo).
months
function using the dateutil
module
from dateutil.rrule import rrule, MONTHLY
from datetime import datetime
def months(start_month, start_year, end_month, end_year):
start = datetime(start_year, start_month, 1)
end = datetime(end_year, end_month, 1)
return [(d.month, d.year) for d in rrule(MONTHLY, dtstart=start, until=end)]
Example Usage
print months(11, 2010, 2, 2011)
#[(11, 2010), (12, 2010), (1, 2011), (2, 2011)]
Or in iterator form
def month_iter(start_month, start_year, end_month, end_year):
start = datetime(start_year, start_month, 1)
end = datetime(end_year, end_month, 1)
return ((d.month, d.year) for d in rrule(MONTHLY, dtstart=start, until=end))
Iterator usage
for m in month_iter(11, 2010, 2, 2011):
print m
#(11, 2010)
#(12, 2010)
#(1, 2011)
#(2, 2011)
for year in range(2017, 2021):
for month in range(1, 13):
this_month = datetime.date.today().replace(year=year, month=month, day=1)
Since others have already provided code for generators, I wanted to add that Pandas has a method called ‘period_range‘ that, in this case, can take in a starting and end, year and month, and return a period index, suitable for iteration.
import pandas as pd
pr = pd.period_range(start='2010-08',end='2011-03', freq='M')
prTupes=tuple([(period.month,period.year) for period in pr])
#This returns: ((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))
I would like to create a python function that would allow me to iterate over the months from a start point to a stop point. For example it would look something like
def months(start_month, start_year, end_month, end_year):
Calling months(8, 2010, 3, 2011)
would return:
((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))
The function could just return a tuple of tuples, but I would love to see it as a generator (ie using yield
).
I’ve checked the calendar
python module and it doesn’t appear to provide this functionality. I could write a nasty for
loop to do it easily enough, but I’m interested to see how gracefully it could be done by a pro.
Thanks.
Perhaps the elegance or speed of this could be improved, but it’s the straightforward solution:
def months(start_month, start_year, end_month, end_year):
month, year = start_month, start_year
while True:
yield month, year
if (month, year) == (end_month, end_year):
return
month += 1
if (month > 12):
month = 1
year += 1
EDIT: And here’s a less straightforward one that avoids even needing to use yield
by using a generator comprehension:
def months2(start_month, start_year, end_month, end_year):
return (((m_y % 12) + 1, m_y / 12) for m_y in
range(12 * start_year + start_month - 1, 12 * end_year + end_month))
The calendar works like this.
def month_year_iter( start_month, start_year, end_month, end_year ):
ym_start= 12*start_year + start_month - 1
ym_end= 12*end_year + end_month - 1
for ym in range( ym_start, ym_end ):
y, m = divmod( ym, 12 )
yield y, m+1
All multiple-unit things work like this. Feet and Inches, Hours, Minutes and Seconds, etc., etc. The only thing that’s not this simple is months-days or months-weeks because months are irregular. Everything else is regular, and you need to work in the finest-grained units.
This isn’t as short as the other solutions, but it’s straightforward to understand. Basically, it has two branches.
- The start year is the same as the end year
- The start year is different from the end year
The latter case has three phases:
- From the start month to December of the start year
- Every month from each year between the start year to the end year
- From January to the end month of the end year
If the end year is the year after the start year, the second phase above is skipped (no need for an explicit test, the range is simply empty).
def months(start_month, start_year, end_month, end_year):
if start_year == end_year:
for month in xrange(start_month, end_month+1):
yield month, start_year
else:
for month in xrange(start_month, 13):
yield month, start_year
for year in xrange(start_year+1, end_year):
for month in xrange(1, 13):
yield month, year
for month in xrange(1, end_month+1):
yield end_month, end_year
For Python 3.x, change xrange
to range
.
Your question is a little ambiguous in that you ask for an iterator, but then show a function returning a tuple of tuples. So here’s both:
import calendar
import datetime
def months_iter(start_month, start_year, end_month, end_year):
start_date = datetime.date(start_year, start_month, 1)
end_date = datetime.date(end_year, end_month, 1)
date = start_date
while date <= end_date:
yield (date.month, date.year)
days_in_month = calendar.monthrange(date.year, date.month)[1]
date += datetime.timedelta(days_in_month)
def months(start_month, start_year, end_month, end_year):
return tuple(d for d in months_iter(start_month, start_year, end_month, end_year))
print months(8, 2010, 3, 2011)
# ((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))
Having a little fun with Python built-in iterators, but certainly not elegant 😉
from datetime import timedelta, date
class MonthRange:
def __init__ (self, date1, date2):
self.start_date = date1 - timedelta(days=1)
self.end_date = date2
self.data = self.start_date
def __iter__(self):
return self
def next(self):
if self.data >= self.end_date.replace(day=1) + timedelta(days=32):
raise StopIteration
ret = self.data
self.data = self.data + timedelta(days=32)
return ret.replace(day=1)
for x in MonthRange(date.today(), date(2012, 11, 01)):
print (x.year, x.month)
A simpler version of dfan’s approach, and also a simpler solution than S. Lott’s (no division, no modulo):
def months(start_month, start_year, end_month, end_year):
month, year = start_month, start_year
while (year, month) <= (end_year, end_month):
yield month, year
month += 1
if month > 12:
month = 1
year += 1
This approach is close to the method one would use if they had to do this by hand. It runs in the same amount of time as S. Lott’s (the tests in the code above take about as much time as a division and a modulo).
months
function using the dateutil
module
from dateutil.rrule import rrule, MONTHLY
from datetime import datetime
def months(start_month, start_year, end_month, end_year):
start = datetime(start_year, start_month, 1)
end = datetime(end_year, end_month, 1)
return [(d.month, d.year) for d in rrule(MONTHLY, dtstart=start, until=end)]
Example Usage
print months(11, 2010, 2, 2011)
#[(11, 2010), (12, 2010), (1, 2011), (2, 2011)]
Or in iterator form
def month_iter(start_month, start_year, end_month, end_year):
start = datetime(start_year, start_month, 1)
end = datetime(end_year, end_month, 1)
return ((d.month, d.year) for d in rrule(MONTHLY, dtstart=start, until=end))
Iterator usage
for m in month_iter(11, 2010, 2, 2011):
print m
#(11, 2010)
#(12, 2010)
#(1, 2011)
#(2, 2011)
for year in range(2017, 2021):
for month in range(1, 13):
this_month = datetime.date.today().replace(year=year, month=month, day=1)
Since others have already provided code for generators, I wanted to add that Pandas has a method called ‘period_range‘ that, in this case, can take in a starting and end, year and month, and return a period index, suitable for iteration.
import pandas as pd
pr = pd.period_range(start='2010-08',end='2011-03', freq='M')
prTupes=tuple([(period.month,period.year) for period in pr])
#This returns: ((8, 2010), (9, 2010), (10, 2010), (11, 2010), (12, 2010), (1, 2011), (2, 2011), (3, 2011))