最符合Python风格的日期序列排序方式是什么?

4
我有一个字符串列表表示一年中的月份(未排序且不连续):['1/2013', '7/2013', '2/2013', '3/2013', '4/2014', '12/2013', '10/2013', '11/2013', '1/2014', '2/2014'] 我正在寻找一种Pythonic方式对它们进行排序并将每个连续序列分开,如下所示:
[ ['1/2013', '2/2013', '3/2013', '4/2013'], 
  ['7/2013'], 
  ['10/2013', '11/2013', '12/2013', '1/2014', '2/2014'] 
]

有什么想法吗?

4
它们是如何分组的,按连续几个月来算? - Grijesh Chauhan
5个回答

4

基于文档中展示如何使用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)))]
pprint(L)

解决方案的关键在于使用enumerate()生成的范围进行差分,以便连续的月份都出现在同一组(运行)中。

输出

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

months 未定义? - RedBaron
2
@RedBaron:months是问题中的原始列表。 - 9000
虽然这是最短的解决方案,但它看起来并不像是非常符合Python惯用法的,尽管Haskell或F#的粉丝可能会欣赏它 :) 通过将日期转换为月数可以稍微简化一下:http://pastebin.com/8KR8Ayzc - 9000
@9000:使用月份作为计量单位确实是一种改进。我已经更新了答案。 - jfs

2

groupby 的示例很好,但是对于这种输入:['1/2013', '2/2017'],即相邻年份的相邻月份时,会过于复杂而出错。

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 = []
        group.append(formatted)
        last = current
    if group:
        yield group

结果:

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

1
如果您只想对列表进行排序,则可以使用sorted函数,并传递key值 = 将日期字符串转换为Python的datetime对象的函数,例如lambda d: datetime.strptime(d,'%m /%Y'),请参见以下代码示例,用于您的列表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

要将“月/年字符串列表”拆分为“连续月份列表”,可以使用以下脚本(请阅读注释)。在此脚本中,我首先对列表L进行了排序,然后根据连续月份对字符串进行分组(为了检查连续月份,我编写了一个函数):
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)

它的作用如下:

>>> 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
            r_dl.append(t_dl)
            t_dl = [d]
        else:
            t_dl.append(d)
    return r_dl

result(L)

不要忘记包含 from datetime import datetime,我相信你可以轻松地更新新日期列表,其中日期以其他格式表示。
在 @9000 的提示下,我简化了我的排序函数并删除了旧答案,如果您想检查旧脚本,请查看 @codepad

添加一个脚本链接,用于生成最终嵌套列表 @codepad - Grijesh Chauhan
如果你不再把数字对看作日期,你就可以简化你的解决方案 :) - 9000
@9000 感谢,如果您有进一步的建议,请帮助我改进我的答案。 - Grijesh Chauhan

0
在这种特定情况下(元素不多),一个简单的解决方案就是遍历所有月份:
year = dates[0].split('/')[1]
result = []
current = []
for i in range(1, 13):
    x = "%i/%s" % (i, year)
    if x in dates:
        current.append(x)
        if len(current) == 1:
            result.append(current)
    else:
        current = []

0

好的,这是一个没有使用 itertools 并且尽可能简短的代码,而不会影响可读性。技巧在于使用 zip。它基本上是 @moe 的答案展开一点。

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
        result.append(span)
        span = []
      span.append(later)
    # last span was not appended because sequence ended without breaking
    result.append(span)
  return result

尝试一下:

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

print groupSequentially(months)  # output wrapped manually

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

我们可以在最后将`parseAsPair`映射到列表上,这样可以节省一些性能和认知负荷。然后,我们可以从`groupSequentially`中移除对`parseAsPair`的每次调用,但是我们需要再次将结果转换为字符串。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接