Take the 2-minute tour ×

I have a list of stings representing a month in a year (not sorted and not consecutive): ['1/2013', '7/2013', '2/2013', '3/2013', '4/2014', '12/2013', '10/2013', '11/2013', '1/2014', '2/2014']

I'm looking for a Pythonic way to sort all of them and separate each consecutive sequence as the following suggests:

[ ['1/2013', '2/2013', '3/2013', '4/2013'], 
  ['10/2013', '11/2013', '12/2013', '1/2014', '2/2014'] 

Any ideas?

share|improve this question
how they are grouped, in consecutive months? –  Grijesh Chauhan Apr 15 at 5:42
What have you tried to implement? –  RedBaron Apr 15 at 5:46

5 Answers 5

up vote 4 down vote accepted

Based on the example from the docs that shows how to find runs of consecutive numbers using itertools.groupby():

from itertools import groupby
from pprint import pprint

def month_number(date):
    month, year = date.split('/')
    return int(year) * 12 + int(month)

L = [[date for _, date in run]
     for _, run in groupby(enumerate(sorted(months, key=month_number)),
                           key=lambda (i, date): (i - month_number(date)))]

The key to the solution is differencing with a range generated by enumerate() so that consecutive months all appear in same group (run).


[['1/2013', '2/2013', '3/2013'],
share|improve this answer
months is not defined? –  RedBaron Apr 15 at 6:25
@RedBaron: months is the original list from the question. –  9000 Apr 15 at 6:27
Ah! missed that –  RedBaron Apr 15 at 6:28
While the shortest, this solution does not look like extremely idiomatic Python, though a Haskell or F# fan would appreciate it :) It can be slightly simplified by casting the dates as number of months: pastebin.com/8KR8Ayzc –  9000 Apr 15 at 6:56
@9000: using number of months is definitely an improvement. I've updated the answer –  J.F. Sebastian Apr 15 at 7:08

The groupby examples are cute, but too dense and will break on this input: ['1/2013', '2/2017'], i.e. when there are adjacent months from non-adjacent years.

from datetime import datetime
from dateutil.relativedelta import relativedelta

def areAdjacent(old, new):
    return old + relativedelta(months=1) == new

def parseDate(s):
    return datetime.strptime(s, '%m/%Y')

def generateGroups(seq):
    group = []
    last = None
    for (current, formatted) in sorted((parseDate(s), s) for s in seq):
        if group and last is not None and not areAdjacent(last, current):
            yield group
            group = []
        last = current
    if group:
        yield group


share|improve this answer
You're correct. Thanks. –  devnull Apr 15 at 7:04

If you wants to just sort your list then use sorted function and pass key value = a function that converts date-string into Python's datetime object as lambda d: datetime.strptime(d, '%m/%Y'), check following code example for your list as L:

>>> from datetime import datetime
>>> sorted(L, key = lambda d: datetime.strptime(d, '%m/%Y'))
['1/2013', '2/2013', '3/2013', '7/2013', '10/2013', 
 '11/2013', '12/2013', '1/2014', '2/2014', '4/2014'] # indented by hand

To split "list of month/year strings" into "list of list of consecutive months", you can use following script (read comments), In which, first I sorted the list L then groups strings on the basis of consecutive month (to check consecutive month I written a function):

def is_cm(d1, d2):
    """ is consecutive month pair?
        : Assumption d1 is older day's date than d2
    d1 = datetime.strptime(d1, '%m/%Y')
    d2 = datetime.strptime(d2, '%m/%Y') 

    y1, y2 = d1.year, d2.year
    m1, m2 = d1.month, d2.month

    if y1 == y2: # if years are same d2 should be in next month
        return (m2 - m1) == 1
    elif (y2 - y1) == 1: # if years are consecutive
        return (m1 == 12 and m2 == 1)

It works as follows:

>>> is_cm('1/2012', '2/2012')
True # yes, consecutive
>>> is_cm('12/2012', '1/2013')
True # yes, consecutive
>>> is_cm('1/2015', '12/2012') # None --> # not consecutive
>>> is_cm('12/2012', '2/2013')
False # not consecutive

Code to split your code:

def result(dl):
    dl: dates list - a iterator of 'month/year' strings
    type: list of strings

    returns: list of lists of strings
    #Sort list:
    s_dl = sorted(dl, key=lambda d: datetime.strptime(d, '%m/%Y'))
    r_dl = [] # list to be return
    # split list into list of lists
    t_dl = [s_dl[0]] # temp list
    for d in s_dl[1:]:
        if not is_cm(t_dl[-1], d): # check if months are not consecutive
            t_dl = [d]
    return r_dl


Don't forget to include from datetime import datetime, This trick I believe you can easily update for a new list of dates in which dates are in other format.

After @9000 hint I could simplified my sorted function and removed old answer if you wants to check old script check @codepad.

share|improve this answer
Adding a link of script that makes final nested list @codepad –  Grijesh Chauhan Apr 15 at 6:38
If you stopped thinking about the pairs of numbers as of dates, you could simplify your solution :) –  9000 Apr 15 at 7:02
@9000 Thanks If you have further suggestion please help me to improve my answer. –  Grijesh Chauhan Apr 15 at 8:29

An easy solution in this specific case (not many elements) is just to iterate over all the months:

year = dates[0].split('/')[1]
result = []
current = []
for i in range(1, 13):
    x = "%i/%s" % (i, year)
    if x in dates:
        if len(current) == 1:
        current = []
share|improve this answer

Well, here's one without itertools and as short as I could make it without hurting readability. The trick is in use of zip. It's basically @moe's answer unwrapped a bit.

def parseAsPair(piece):
  """Transforms things like '7/2014' into (2014, 7) """
  m, y = piece.split('/')
  return (int(y), int(m))

def goesAfter(earlier, later):
  """Returns True iff earlier goes right after later."""
  earlier_y, earlier_m = earlier
  later_y, later_m = later
  if earlier_y == later_y:  # same year?
    return later_m == earlier_m + 1 # next month
  else: # next year? must be Dec -> Jan
    return later_y == earlier_y + 1 and earlier_m == 12 and later_m == 1

def groupSequentially(months):
  result = []  # final result
  if months:
    sorted_months = sorted(months, key=parseAsPair)
    span = [sorted_months[0]]  # current span; has at least the first month
    for earlier, later in zip(sorted_months, sorted_months[1:]):
      if not goesAfter(parseAsPair(earlier), parseAsPair(later)):
        # current span is over
        span = []
    # last span was not appended because sequence ended without breaking
  return result

Trying it:


print groupSequentially(months)  # output wrapped manually


We could save a bit of performance and cognitive load if we mapped parseAsPair over the list in the very end. Then every invocation of parseAsPair could be removed from groupSequentially, but we'd have to transform the result to strings again.

share|improve this answer

Your Answer


