Почему `datetime.strptime` получает неверную дату во вторник на неделе 0 2015 года?

Я нашел ошибку в функции 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

Что мне делать с этой информацией?

Ответ 1

Я смотрел больше лет, и у меня такое же загадочное поведение, но я нашел некоторую логику.

После чтения документов, я понимаю это немного лучше:

% 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)

Итак, после начала года поведение кажется предсказуемым и согласуется с документами.

Ответ 2

Я смог подтвердить, что это ошибка. Я изучил модуль _strptime.py и могу подтвердить его краевое условие, как он обрабатывает вычисления юлианских дат.

Проблема связана с тем, что вызовы _calc_julian_from_U_or_W() могут возвращать -1, что при нормальных обстоятельствах недействительно. Функция strptime() проверяет и корректирует, когда значения julian равны -1... но это НЕ должно делать это, когда week_of_year равно нулю.

BTW: Тот факт, что он тестирует ТОЛЬКО -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