Python中计算第n个星期几的代码有什么问题?

10

我正在尝试计算给定日期的第n个工作日。例如,我应该能够计算某个日期所在月份的第三个星期三。

我已经编写了两个版本的函数来实现这个目标:

from datetime import datetime, timedelta

### version 1
def nth_weekday(the_date, nth_week, week_day):
    temp = the_date.replace(day=1)
    adj = (nth_week-1)*7 + temp.weekday()-week_day
    return temp + timedelta(days=adj)

### version 2
def nth_weekday(the_date, nth_week, week_day):
    temp = the_date.replace(day=1)
    adj = temp.weekday()-week_day
    temp += timedelta(days=adj)
    temp += timedelta(weeks=nth_week)
    return temp

控制台输出

# Calculate the 3rd Friday for the date 2011-08-09
x=nth_weekday(datetime(year=2011,month=8,day=9),3,4)
print 'output:',x.strftime('%d%b%y') 

# output: 11Aug11 (Expected: '19Aug11')

这两个函数的逻辑明显是错误的,但我似乎找不到错误所在 - 有谁能发现代码中的问题并指导我如何修复以返回正确的值吗?

9个回答

11

你的问题出在这里:

adj = temp.weekday()-week_day

首先,您正在错误地进行减法:您需要从所需日期中减去实际日期,而不是反过来。

其次,您需要确保减法的结果不是负数 - 应该使用% 7将其放在0-6的范围内。

结果:

adj = (week_day - temp.weekday()) % 7

此外,在你的第二个版本中,你需要像在第一个版本中那样添加nth_week-1周。

完整示例:

def nth_weekday(the_date, nth_week, week_day):
    temp = the_date.replace(day=1)
    adj = (week_day - temp.weekday()) % 7
    temp += timedelta(days=adj)
    temp += timedelta(weeks=nth_week-1)
    return temp

>>> nth_weekday(datetime(2011,8,9), 3, 4)
datetime.datetime(2011, 8, 19, 0, 0)

当您运行我修正后的函数版本2,并使用我的示例日期时,您会得到什么结果?我得到的是28Sep11,这仍然是不正确的。正确的日期(可以轻松验证)应该是19Aug11 - Homunculus Reticulli
抱歉,但这个算法是错误的 - 你需要确保adj始终为正(因为你只是添加了n-1周) - 如果进行减法操作,则无法保证它始终为正! - tzelleke
@interjay 好的 - 我不知道%运算符是这样工作的 - 那么你的解决方案当然是正确的 - 抱歉! - tzelleke
@Goury请不要编辑其他人的代码,特别是当你的版本是错误的时候。 - interjay
谢谢这个。让我省了很多时间! - thang
显示剩余3条评论

9

一行代码

您可以使用标准库中的日历(calendar)模块,通过一行代码找到第 n 个工作日。

import calendar
calendar.Calendar(x).monthdatescalendar(year, month)[n][0]

where:

  • x:代表星期几的整数(0为星期一)

  • n:您问题的“第n个”部分

  • year,month:整数年份和月份

这将返回一个datetime.date对象。

分解

可以按以下方式进行细分:

calendar.Calendar(x)

创建一个日历对象,以您所需的工作日为起始日。
.monthdatescalendar(year, month)

返回该月份的所有日历日期。
[n][0]

返回第n周(从第x天开始的那一周的第一天)的第0个索引值。

为什么有用

将周的起始日设置为所需的星期几的原因是默认情况下0(星期一)将被用作一周的第一天,如果月份从星期三开始,则日历将认为第一个星期从第一个星期一(即第2周)开始,并且您会落后一周。

示例

如果您需要2013年9月的第三个星期六(该月的美国股票期权到期日),则可以使用以下方法:

calendar.Calendar(5).monthdatescalendar(2013,9)[3][0]

4
很抱歉,这个票数较高的答案是错误的。反例:2017年11月的第三个星期三:calendar.Calendar(2).monthdatescalendar(2017,11)[3][0] 返回的值是 datetime.date(2017, 11, 22),而正确的日期应该是 2017-11-15。 - arielf
1
我同意这是错误的。尝试使用(calendar.Calendar(calendar.SUNDAY).monthdatescalendar(2018, 7)[1][0]) - 如果你减去7天,它会给出2018-07-08,那么你就会得到2018-07-01,显然是错误的。 - JGFMK
如果Calendar(x)指示了一个月的第一天,它将从n/1开始。在其他情况下,第一行包括上个月的星期几。您可以使用calendar.Calendar(1).monthdatescalendar(2020, 12)进行检查。因此,这个答案需要稍作修正。 - fx-kirin

5

得票最多的这个一行代码存在问题,它不起作用。

然而,它可以用作改进的基础:

你看到的就是你得到的:

c = calendar.Calendar(calendar.SUNDAY).monthdatescalendar(2018, 7)
for c2 in c:
    print(c2[0])
 

2018-07-01
2018-07-08
2018-07-15
2018-07-22
2018-07-29

c = calendar.Calendar(calendar.SUNDAY).monthdatescalendar(2018, 8)
for c2 in c:
    print(c2[0])

2018-07-29
2018-08-05
2018-08-12
2018-08-19
2018-08-26

如果你思考一下,它是试图将日历组织成嵌套列表,以便一次打印一周的日期。所以来自其他月份的落单日期也会参与进来。通过使用一个包含该月有效日期的新列表-这就完成了任务。


附带列表的回答

import calendar
import datetime
def get_nth_DOW_for_YY_MM(dow, yy, mm, nth) -> datetime.date:
    #dow - Python Cal - 6 Sun 0 Mon ...  5 Sat
    #nth is 1 based... -1. is ok for last.
    i = -1 if nth == -1 or nth == 5 else nth -1
    valid_days = []
    for d in calendar.Calendar(dow).monthdatescalendar(yy, mm):
        if d[0].month == mm:
            valid_days.append(d[0])
    return valid_days[i]

所以这个可以被称为:
firstSundayInJuly2018 = get_nth_DOW_for_YY_MM(calendar.SUNDAY, 2018, 7, 1)
firstSundayInAugust2018 = get_nth_DOW_for_YY_MM(calendar.SUNDAY, 2018, 8, 1)
print(firstSundayInJuly2018)
print(firstSundayInAugust2018)

这里是输出:

2018-07-01 
2018-08-05

get_nth_DOW_for_YY_MM()可以使用lambda表达式进行重构,如下所示:

使用lambda表达式进行重构的答案

import calendar
import datetime
def get_nth_DOW_for_YY_MM(dow, yy, mm, nth) -> datetime.date:
    #dow - Python Cal - 6 Sun 0 Mon ...  5 Sat
    #nth is 1 based... -1. is ok for last.
    i = -1 if nth in [-1,5] else nth -1
    return list(filter(lambda x: x.month == mm, \
          list(map(lambda x: x[0], \ 
            calendar.Calendar(dow).monthdatescalendar(yy, mm) \
          )) \
        ))[i]


1
一行答案似乎无法处理目标日期恰好为每月第一天的情况。例如,如果您想要每个月的第二个星期五,则一行式方法不适用。
calendar.Calendar(4).monthdatescalendar(year, month)[2][0]

针对2013年3月的情况,返回的日期是2013年3月15日,实际上应该是2013年3月8日。可能需要添加一个检查,例如:

if date(year, month, 1).weekday() == x:
    delivery_date.append(calendar.Calendar(x).monthdatescalendar(year, month)[n-1][0])
else:
    delivery_date.append(calendar.Calendar(x).monthdatescalendar(year, month)[n][0])

1

relativedelta 模块是 Python dateutil package 的扩展模块(pip install python-dateutil),它可以完全满足你的需求:

from dateutil import relativedelta
import datetime


def nth_weekday(the_date, nth_week, week_day):
    return the_date.replace(day=1) + relativedelta.relativedelta(
        weekday=week_day(nth_week)
    )

print(nth_weekday(datetime.date.today(), 3, relativedelta.FR))

这里的关键部分是 weekday=relativedelta.FR(3):月份中的第三个星期五。以下是 docs 中与 weekday 参数相关的部分:

weekday:

一周中的某个星期几实例(MO、TU 等),可在 relativedelta 模块中使用。这些实例可以接收一个参数 N,指定第 N 个星期几,该参数可以是正数或负数(如 MO(+1) 或 MO(-2))。


0

或者这也适用于Python 2,返回所述月份中工作日的发生次数,即如果输入为2018年6月16日,则返回6月16日当天的发生次数

您可以将月/年/日期整数替换为任何您想要的内容 - 现在它通过datetime从系统获取输入/日期

省略打印语句或在不需要它们的地方使用pass

import calendar
import datetime
import pprint

month_number = int(datetime.datetime.now().strftime('%m'))
year_number = int(datetime.datetime.now().strftime('%Y'))
date_number = int(datetime.datetime.now().strftime('%d'))
day_ofweek = str(datetime.datetime.now().strftime('%A'))


def weekday_occurance():
print "\nFinding current date here\n"
for week in xrange(5):
    try:
        calendar.monthcalendar(year_number, month_number)[week].index(date_number)
        occurance = week + 1
        print "Date %s of month %s and year %s is %s #%s in this month." % (date_number,month_number,year_number,day_ofweek,occurance)
        return occurance
        break
    except ValueError as e:
        print "The date specified is %s which is week %s" % (e,week)



myocc = weekday_occurance()
print myocc

0
稍作调整即可使这个一行代码正常工作:
import calendar
calendar.Calendar((weekday+1)%7).monthdatescalendar(year, month)[n_th][-1]

这里的n_th应该被解释为C风格,例如,0是第一个索引。

例如,要找到2018年7月第一个星期日,可以输入:

>>> calendar.Calendar(0).monthdatescalendar(2018, 7)[0][-1]
datetime.date(2018, 7, 1)

0

这里的人似乎喜欢一行代码,我将在下面提出建议。

import calendar
[cal[0] for cal in calendar.Calendar(x).monthdatescalendar(year, month) if cal[0].month == month][n]

0

欧洲(ISO)日期格式(获取每月第n个工作日)

def is_nth_weekday(nth: int, isodaynum: int):
    if isodaynum not in range(1, 8):
        raise ValueError('isodaynum should be in [1, 7], for [monday, sunday]')
    daynum = isodaynum - 1

    if date(date.today().year, date.today().month, 1).weekday() == daynum:
        nth = nth - 1
    nth_weekday = calendar.Calendar(daynum).monthdatescalendar(
        date.today().year,
        date.today().month
    )[nth][0]
    return date.today() == nth_weekday

例如:

# is third Friday of the month (stock options expiration)
is_nth_weekday(nth=3, isodaynum=5)

只在当前月份的第三个星期五返回True

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