最符合Python风格的日期序列排序方法是什么?
我有一个字符串列表,里面是一些年份和月份的组合(这些数据不是按顺序排列的,也不是连续的):
['1/2013', '7/2013', '2/2013', '3/2013', '4/2014', '12/2013', '10/2013', '11/2013', '1/2014', '2/2014']
我想用一种Python的方式,把这些数据排序,并把每一段连续的月份分开,像下面这样:
[ ['1/2013', '2/2013', '3/2013', '4/2013'],
['7/2013'],
['10/2013', '11/2013', '12/2013', '1/2014', '2/2014']
]
有没有什么好主意呢?
5 个回答
在这种特定情况下(元素不多),一个简单的解决办法就是逐个月份进行遍历:
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 = []
好吧,这里有一个不使用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
调用去掉,不过我们还得把结果再转换成字符串。
如果你只是想对你的列表进行排序,可以使用 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
拆分你代码的代码:
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
。我相信这个技巧你可以很容易地更新到一个新的日期列表中,只要日期格式不同即可。
这些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']]
这个内容是基于文档中的一个例子,展示了如何使用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']]