'Get (year,month) for the last X months

I got a very simple thing to to in python: I need a list of tuples (year,month) for the last x months starting (and including) from today. So, for x=10 and today(July 2011), the command should output:

[(2011, 7), (2011, 6), (2011, 5), (2011, 4), (2011, 3), 
(2011, 2), (2011, 1), (2010, 12), (2010, 11), (2010, 10)]

Only the default datetime implementation of python should be used. I came up with the following solution:

import datetime
[(d.year, d.month) for d in [datetime.date.today()-datetime.timedelta(weeks=4*i) for i in range(0,10)]]

This solution outputs the correct solution for my test cases but I'm not comfortable with this solution: It assumes that a month has four weeks and this is simply not true. I could replace the weeks=4 with days=30 which would make a better solution but it is still not correct.

The other solution which came to my mind is to use simple maths and subtract 1 from a months counter and if the month-counter is 0, subtract 1 from a year counter. The problem with this solution: It requires more code and isn't very readable either.

So how can this be done correctly?



Solution 1:[1]

Using relativedelta ...

import datetime
from dateutil.relativedelta import relativedelta

def get_last_months(start_date, months):
    for i in range(months):
        yield (start_date.year,start_date.month)
        start_date += relativedelta(months = -1)

>>> X = 10       
>>> [i for i in get_last_months(datetime.datetime.today(), X)]
>>> [(2013, 2), (2013, 1), (2012, 12), (2012, 11), (2012, 10), (2012, 9), (2012, 8), (2012, 7), (2012, 6), (2012, 5)]

Solution 2:[2]

Neatest would be to use integer division (//) and modulus (%) functions, representing the month by the number of months since year 0:

months = year * 12 + month - 1 # Months since year 0 minus 1
tuples = [((months - i) // 12, (months - i) % 12 + 1) for i in range(10)]

The - 1 in the months expression is required to get the correct answer when we add 1 to the result of the modulus function later to get 1-indexing (i.e. months go from 1 to 12 rather than 0 to 11).

Or you might want to create a generator:

def year_month_tuples(year, month):
    months = year * 12 + month - 1 # -1 to reflect 1-indexing
    while True:
        yield (months // 12, months % 12 + 1) # +1 to reflect 1-indexing
        months -= 1 # next time we want the previous month

Which could be used as:

>>> tuples = year_month_tuples(2011, 7)
>>> [tuples.next() for i in range(10)]

Solution 3:[3]

Update: Adding a timedelta version anyway, as it looks prettier :)

def get_years_months(start_date, months):
    for i in range(months):
        yield (start_date.year, start_date.month)
        start_date -= datetime.timedelta(days=calendar.monthrange(start_date.year, start_date.month)[1])

You don't need to work with timedelta since you only need year and month, which is fixed.

def get_years_months(my_date, num_months):
    cur_month = my_date.month
    cur_year = my_date.year

    result = []
    for i in range(num_months):
        if cur_month == 0:
            cur_month = 12
            cur_year -= 1
        result.append((cur_year, cur_month))
        cur_month -= 1

    return result

if __name__ == "__main__":
    import datetime
    result = get_years_months(datetime.date.today(), 10)
    print result

Solution 4:[4]

If you create a function to do the date maths, it gets almost as nice as your original implementation:

def next_month(this_year, this_month):
  if this_month == 0: 
    return (this_year - 1, 12)
  else:
    return (this_year, this_month - 1)

this_month = datetime.date.today().month()
this_year = datetime.date.today().year()
for m in range(0, 10):
  yield (this_year, this_month)
  this_year, this_month = next_month(this_year, this_month)

Solution 5:[5]

if you want to do it without datetime libraries, you can convert to months since year 0 and then convert back

end_year = 2014
end_month = 5
start_year = 2013
start_month = 7

print list = [(a/12,a % 12+1) for a in range(12*end_year+end_month-1,12*start_year+start_month-2,-1)]

python 3 (// instead of /):

list = [(a//12,a % 12+1) for a in range(12*end_year+end_month-1,12*start_year+start_month-2,-1)]
print(list)

[(2014, 5), (2014, 4), (2014, 3), (2014, 2), (2014, 1), (2013, 12), (2013, 11), (2013, 10), (2013, 9), (2013, 8), (2013, 7)]

Solution 6:[6]

Or you can define a function to get the last month, and then print the months ( it's a bit rudimentary)

def last_month(year_month):#format YYYY-MM
    aux = year_month.split('-')
    m = int(aux[1])
    y = int(aux[0])

    if m-1 == 0:
        return str(y-1)+"-12"
    else:
        return str(y)+"-"+str(m-1)

def print_last_month(ran, year_month= str(datetime.datetime.today().year)+'-'+str(datetime.datetime.today().month)):
    i = 1 
    if ran != 10:
        print( last_month(year_month) )
        print_last_month(i+1, year_month= last_month(year_month))

Solution 7:[7]

    def list_last_year_month(self):
    last_day_of_prev_month = date.today()
    number_of_years = self.number_of_years
    time_list = collections.defaultdict(list)
    for y in range(number_of_years+1):
        for m in range(13):
            last_day_of_prev_month = last_day_of_prev_month.replace(day=1) - timedelta(days=1)
            last_month = str(last_day_of_prev_month.month)
            last_year = str(last_day_of_prev_month.year)
            time_list[last_year].append(last_month)
    return time_list

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 trinchet
Solution 2
Solution 3
Solution 4 rafalotufo
Solution 5 bArmageddon
Solution 6 fsalazar_sch
Solution 7 Hanish Madan