问题 为什么`datetime.strptime`在2015年第0周的星期二得到错误的日期?


我在python中发现了一个错误 datetime.strptime 功能。

我创造了 datetime 对象基于 周数 (%W (%Y)和 星期几 (%w)。 2015年第一周的星期二日期是错误的:

>>> from datetime import datetime

>>> datetime.strptime('%s %s %s' % (0, 2015, 1), '%W %Y %w').date()
datetime.date(2014, 12, 29) # OK

>>> datetime.strptime('%s %s %s' % (0, 2015, 2), '%W %Y %w').date()
datetime.date(2015, 1, 1) # WRONG !!!

>>> datetime.strptime('%s %s %s' % (0, 2015, 3), '%W %Y %w').date()
datetime.date(2014, 12, 31) # OK

>>> datetime.strptime('%s %s %s' % (0, 2015, 4), '%W %Y %w').date()
datetime.date(2015, 1, 1) # OK

>>> datetime.strptime('%s %s %s' % (0, 2015, 5), '%W %Y %w').date()
datetime.date(2015, 1, 2) # OK

>>> datetime.strptime('%s %s %s' % (0, 2015, 6), '%W %Y %w').date()
datetime.date(2015, 1, 3) # OK

>>> datetime.strptime('%s %s %s' % (0, 2015, 0), '%W %Y %w').date()
datetime.date(2015, 1, 4) # OK

我该怎么处理这些信息?


3211
2017-12-30 16:19


起源

docs.python.org/2/bugs.html - Aran-Fey
我认为所有这些日期都是不正确的。在2015年的第一周没有星期一。这是一个荒谬的日子。这就像要求2015年2月30日 - 没有这样的日期。 - Dunes
@Dunes然而,Python文档似乎将一年中的第一周定义为包含一年中第一个星期一的一周(周日 - 周六)。因此,根据定义,在2015年的第一周有一个星期一。在那个星期之前的几天,即2015年,但不是第一周的一部分被认为是第0周的一部分...所以,你的除了哲学偏好之外,2015年(或任何一年)的第一周实际上根据文档明确定义......在这种情况下,它不包含一年中的第一天...... - twalberg
@twalberg我似乎做了一个表达自己的可怕尝试。我的意思是,2015年第0周没有星期一。2015年第0周包含周四至周六的天数。因此,如果您指定了无效日期(例如,在这种情况下为“0 2015 0”),那么 strptime 不保证产生有效的输出。我认为它与询问基数10中的'a'没什么不同。'a'不是基数10中的定义整数,也不是“0 2015 0”格式为“%W%Y%w”的定义日期”。 - Dunes
@Dunes我会争辩 一切 无论何时开始或结束,一周应包含7天。问题是不可避免的,因为一年的长度不能被7整除。要么第0周不存在(它将是前一年的第53周),要么它应该在所有7天内有效。由于第0周被记录为有效,因此它应该表现一致。 - Mark Ransom


答案:


我看了很多年,我得到了同样令人费解的行为,但我发现了一些逻辑。

看完之后 文档,我理解它好一点:

%W - 一年中的周数(星期一作为一周的第一天)作为十进制数。每天 在新的一年里 在第一个星期一之前被认为是在第0周。

所以, %W 只在新的一年里的第0周填写正确的值!这与以下结果完全一致:

2015年:

>>> for i in range(7):
...     datetime.strptime('%s %s %s' % (0, 2015, i), '%W %Y %w').date()
... 
datetime.date(2015, 1, 4)
datetime.date(2014, 12, 29)
datetime.date(2015, 1, 1)
datetime.date(2014, 12, 31)
datetime.date(2015, 1, 1) # start of year
datetime.date(2015, 1, 2)
datetime.date(2015, 1, 3)

2016年

>>> for i in range(7):
...     datetime.strptime('%s %s %s' % (0, 2016, i), '%W %Y %w').date()
... 
datetime.date(2016, 1, 3)
datetime.date(2015, 12, 28)
datetime.date(2015, 12, 29)
datetime.date(2016, 1, 1)
datetime.date(2015, 12, 31)
datetime.date(2016, 1, 1) # start of year
datetime.date(2016, 1, 2)

2017年:

>>> for i in range(7):
...     datetime.strptime('%s %s %s' % (0, 2017, i), '%W %Y %w').date()
... 
datetime.date(2017, 1, 1)
datetime.date(2016, 12, 26)
datetime.date(2016, 12, 27)
datetime.date(2016, 12, 28)
datetime.date(2016, 12, 29)
datetime.date(2017, 1, 1)
datetime.date(2016, 12, 31)
# ... start of year

2018:

>>> for i in range(7):
...     datetime.strptime('%s %s %s' % (0, 2018, i), '%W %Y %w').date()
... 
datetime.date(2018, 1, 7)
datetime.date(2018, 1, 1) # start of year
datetime.date(2018, 1, 2)
datetime.date(2018, 1, 3)
datetime.date(2018, 1, 4)
datetime.date(2018, 1, 5)
datetime.date(2018, 1, 6)

因此,在实际开始这一年之后,行为似乎是可预测的并且与文档一致。


6
2017-12-30 16:49



他们对30/12做了什么? - njzk2
我认为情况可能就是这样。给出一个0的周数和在给定年份开始之前的一天数可能被认为超出了该函数的预期范围。虽然,我认为应该为这种情况抛出某种错误而不是返回坏数据... - twalberg
@twalberg - 我同意,这就是为什么我说没有进一步的实验就令人费解。这不是遵循python的“最不令人惊讶的行为”的事情...... - Reut Sharabani
尽管行为可能与文档一致,但我仍然想知道该函数的哪些实现会返回这些结果。 “令人费解”是对的! - Mark Ransom


我能够确认这是一个错误。我研究了 _strptime.py 模块并且可以确认它是如何处理朱利安日期计算的边缘条件。

问题源于呼叫的事实 _calc_julian_from_U_or_W() 可以返回-1,这在正常情况下是无效的。该 strptime() 当julian值为-1时,函数测试和校正...但是当week_of_year为零时,它不应该这样做。

顺便说一句:它只测试-1的事实是你在2015年看到这个问题的原因。这种情况只存在于一年的第一天比你测试的日期提前两天。

以下补丁更正了边缘条件

--- _strptime.py.orig   2014-12-30 15:47:05.069835336 -0500
+++ _strptime.py        2014-12-30 15:47:21.509139500 -0500
@@ -441,7 +441,7 @@
     # Cannot pre-calculate datetime_date() since can change in Julian
     # calculation and thus could have different value for the day of the week
     # calculation.
-    if julian == -1:
+    if julian == -1 and week_of_year != 0:
         # Need to add 1 to result since first day of the year is 1, not 0.
         julian = datetime_date(year, month, day).toordinal() - \
                   datetime_date(year, 1, 1).toordinal() + 1

我已经将这个补丁应用到我的本地机器上,现在我看到我认为OP想要的东西:

>>> datetime.strptime('%s %s %s' % (0, 2015, 2), '%W %Y %w').date()
datetime.date(2014, 12, 30)

提交的错误报告 http://bugs.python.org/issue23136


4
2017-12-30 16:47



Python 的文件 strptime 与你引用的不同。但是实现可能只是调用底层的C函数,因此它的行为是相同的。你有关于第0周的“奇异规则”的信息吗? - Mark Ransom
@MarkRansom strptime 是用纯Python实现的。看到 _strptime.py,可能在你的Python安装的/ lib /文件夹中。 - senshin
@MarkRansom,我在文档中添加了一个引用我的答案,这对我来说似乎与行为一致。 - Reut Sharabani