Reputation: 1536
I've found a error in python datetime.strptime
function.
I've created datetime
object base on the week number (%W
), year (%Y
) and day of week (%w
). The date for Tuesday in the first week in 2015 is wrong:
>>> 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
What should I do with this information?
Upvotes: 12
Views: 1888
Reputation: 11728
I was able to confirm this is a bug. I studied the _strptime.py
module and can confirm it's an edge condition with how it handle's julian date calculations.
The issue stems from the fact that calls to _calc_julian_from_U_or_W()
can return a -1, which under normal circumstances is invalid. The strptime()
function tests and corrects when julian values are -1...but it should NOT do this when the week_of_year is zero.
BTW: The fact that it tests for ONLY -1 is why you are seeing the problem in 2015. This condition only exists when the first day of the year is exactly two days ahead of the date your are testing for.
The following patch corrects the edge condition
--- _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
I have applied this patch to my local machine, and I now see what I believe the OP wanted:
>>> datetime.strptime('%s %s %s' % (0, 2015, 2), '%W %Y %w').date()
datetime.date(2014, 12, 30)
Filed bug report http://bugs.python.org/issue23136
Upvotes: 4
Reputation: 31339
I've looked over more years and I get the same puzzling behaviour, but I found some logic.
After reading the docs, I understand it a bit better:
%W - Week number of the year (Monday as the first day of the week) as a decimal number. All days in a new year preceding the first Monday are considered to be in week 0.
So, %W
only fills the correct values in week 0 for the days in the new year! This is perfectly consistent with the following results:
>>> 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)
>>> 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)
>>> 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
>>> 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)
So after the year actually begins, the behaviour seems predictable and consistent with the docs.
Upvotes: 7