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.

Asked By: dgel

||

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))
Answered By: dfan

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.

Answered By: S.Lott

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.

Answered By: kindall

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))
Answered By: martineau

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)
Answered By: John Giotta

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).

Answered By: Eric O Lebigot

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)
Answered By: k107
    for year in range(2017, 2021):
        for month in range(1, 13):
            this_month = datetime.date.today().replace(year=year, month=month, day=1)
Answered By: Brian Sanchez

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))
Answered By: etotheipi
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.