从两个日期生成日期列表的最优雅方式是什么
我想生成一个日期列表,这些日期是两个日期之间的某个星期几的第N天。
比如,给定的日期是“20111101”和“20111201”,假设我想生成这个月份的第三个星期三的日期,我想得到的日期是:
["20111116", "20111221"]
所以我想写一个这样的函数:
def generateNthWeekdayDatesList(startdate, enddate, nthday=3, dayofweek="wed"):
pass
实现这个函数的最佳方法是什么?(也就是最符合Python风格的方法)
4 个回答
1
用周而不是天来循环是可以做到的;这只需要一些逻辑、用7取余的数学运算(不是6哦!)以及测试用例来确定正确的起始点。这样一开始的投入会带来一个简单易读的小while
循环。如果有人在意的话,这样的做法也应该会快很多。
import datetime
def nth_weekdays(startdate, enddate, n, isoweekday):
"""
Generate in ascending order all dates x
such that startdate <= x <= enddate
and x is the nth ISO weekday in its month.
"""
if not (1 <= n <= 5):
raise ValueError("n should be 1 to 5, not %r" % n)
if not (1 <= isoweekday <= 7): # Monday = 1
raise ValueError("isoweekday should be 1 to 7, not %r" % isoweekday)
# get day of week of the first day in the start month
dow1 = startdate.replace(day=1).isoweekday()
# get date which is the first Wday in the start month
daynum = (isoweekday - dow1 + 7) % 7 + 1
candidate = startdate.replace(day=daynum)
seen_in_month = 1
current_month = candidate.month
one_week = datetime.timedelta(days=7)
while candidate <= enddate:
if seen_in_month == n and candidate >= startdate:
yield candidate
candidate += one_week
if candidate.month == current_month:
seen_in_month += 1
else:
seen_in_month = 1
current_month = candidate.month
if __name__ == "__main__":
from pprint import pprint as pp
tests = """
2011-01-01 2012-01-01 3 3 # 3rd Wednesday
2011-06-14 2011-06-30 3 3 # 3rd Wednesday
2011-06-15 2011-06-30 3 3 # 3rd Wednesday
2011-06-16 2011-06-30 3 3 # 3rd Wednesday, no results
2011-01-01 2012-01-01 5 7 # 5th Sunday
# 2011-12-01 was a Thursday. Check 1st Mon Wed Thu Fri Sun
2011-12-01 2011-12-31 1 1
2011-12-01 2011-12-31 1 3
2011-12-01 2011-12-31 1 4
2011-12-01 2011-12-31 1 5
2011-12-01 2011-12-31 1 7
# 2011-08-01 was a Monday. Check 1st Mon Tue Sun
2011-08-01 2011-08-31 1 1
2011-08-01 2011-08-31 1 2
2011-08-01 2011-08-31 1 7
# 2011-05-01 was a Sunday. Check 1st Mon Sat Sun
2011-05-01 2011-05-31 1 1
2011-05-01 2011-05-31 1 6
2011-05-01 2011-05-31 1 7
# input errors
2011-01-01 2012-01-01 6 1 # 6th Monday
2011-01-01 2012-01-01 0 1 # 0th Monday
2011-01-01 2012-01-01 3 0 # 3rd ???day
2011-01-01 2012-01-01 3 8 # 3rd ???day
"""
dconv = lambda s: datetime.datetime.strptime(s, "%Y-%m-%d")
funcs = [dconv, dconv, int, int]
for test in tests.splitlines():
test = test.strip()
if not test: continue
print
print test
data = test.split("#")[0]
if not data: continue
args = [func(x) for func, x in zip(funcs, data.split())]
try:
pp([x.strftime("%Y-%m-%d") for x in nth_weekdays(*args)])
except BadArg, e:
print "%s: %s" % (e.__class__.__name__, e)
1
你可以使用非常棒的 dateutil
这个工具包。
import dateutil.relativedelta as R
import dateutil.parser as P
from datetime import datetime
def generateNthWeekdayDatesList(startdate, enddate, nthday=3, dayofweek=R.WE):
s = P.parse(startdate).replace(day=1, minute=0, second=0, microsecond=0)
e = P.parse(enddate)
while s < e:
n = s + R.relativedelta(weekday=dayofweek(nthday))
if s <= n <= e:
yield n
s += R.relativedelta(months=1)
def main():
print list(generateNthWeekdayDatesList("20111116", "20111221"))
if __name__ == '__main__':
main()
运行它会给我以下结果:
$ python f.py
[datetime.datetime(2011, 11, 16, 0, 0), datetime.datetime(2011, 12, 21, 0, 0)]
1
这是一个使用生成器非常合适的例子,符合Python的风格。
def nth_day_of_month(start, end, nth, weekday):
assert start.day == 1, "start on the first day of a month"
assert nth > 0
assert 1 <= weekday <= 7
candidate = start
seen_in_month = 0
while candidate <= end:
if candidate.isoweekday() == weekday:
seen_in_month += 1
if seen_in_month == nth:
yield candidate
current_month = candidate.month
while candidate.month == current_month:
candidate += timedelta(1)
seen_in_month = 0
else:
if (candidate + timedelta(1)).month != candidate.month:
seen_in_month = 0
candidate += timedelta(1)
else:
if (candidate + timedelta(1)).month != candidate.month:
seen_in_month = 0
candidate += timedelta(1)
# third wednesday
print list(nth_day_of_month(date(2011, 1, 1), date(2012, 1, 1), nth=3, weekday=3))
# fifth sunday
print list(nth_day_of_month(date(2011, 1, 1), date(2012, 1, 1), nth=5, weekday=7))
# 9th monday
print list(nth_day_of_month(date(2011, 1, 1), date(2012, 1, 1), nth=9, weekday=1))
你甚至可以创建一个无限生成器(就是那种永远不会停止的生成器):
def infinite_nth_day_of_month(start, nth, weekday):
assert start.day == 1, "start on the first day of a month"
assert nth > 0
assert 1 <= weekday <= 7
candidate = start
seen_in_month = 0
while True:
if candidate.isoweekday() == weekday:
seen_in_month += 1
if seen_in_month == nth:
yield candidate
current_month = candidate.month
while candidate.month == current_month:
candidate += timedelta(1)
seen_in_month = 0
else:
if (candidate + timedelta(1)).month != candidate.month:
seen_in_month = 0
candidate += timedelta(1)
else:
if (candidate + timedelta(1)).month != candidate.month:
seen_in_month = 0
candidate += timedelta(1)
# this will create an infinite list, not a good idea
# print list(infinite_nth_day_of_month(date(2011, 1, 1), 3, 3))
import itertools
date_generator = infinite_nth_day_of_month(date(2011, 1, 1), 3, 3)
# create a list the 10000 third wednesdays of 2011 and into the future
date_list = list(itertools.islice(date_generator, 10000))
>>> print date_list[0]
2011-01-19
>>> print date_list[-1]
2844-04-20
可以查看itertools的文档。
不过,如果你给无限生成器传入的参数导致它生成的内容永远不会有结果(比如说第九个星期二),那么它就会不停地搜索,直到达到datetime.date
支持的最大日期,这时会抛出一个OverflowError: date value out of range
的错误。