从两个日期生成日期列表的最优雅方式是什么

1 投票
4 回答
1290 浏览
提问于 2025-04-17 08:53

我想生成一个日期列表,这些日期是两个日期之间的某个星期几的第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的错误。

撰写回答