I was recently hunting down the basis for a bug in Python where on OpenBSD 6.1, dates were not surviving a `strftime`->`strptime` round trip with the format '%G %V %w' for dates around the beginning of some years and I believe it is a bug in the implementation of wcsftime (and possibly strftime as well).
I've re-created BSD's algorithm for calculating the ISO calendar in Python in the attached `stftime_bug.py`, using this implementation as a guide: https://github.com/openbsd/src/blob/b66614995ab119f75167daaa7755b34001836821/lib/libc/time/wcsftime.c#L326 Comparing it to the Python builtin `datetime.date.isocalendar()`, running the script gives you: Python | BSD | Mismatch -------------------------------------------------------------- 1900, 01, 1 | 1899, 53, 1 | True 1901, 01, 2 | 1901, 01, 2 | False 1902, 01, 3 | 1902, 01, 3 | False 1903, 01, 4 | 1903, 01, 4 | False 1903, 53, 5 | 1904, 01, 5 | True 1904, 52, 7 | 1904, 53, 7 | True 1906, 01, 1 | 1905, 53, 1 | True 1907, 01, 2 | 1907, 01, 2 | False 1908, 01, 3 | 1908, 01, 3 | False 1908, 53, 5 | 1909, 01, 5 | True 1909, 52, 6 | 1910, 01, 6 | True I do not have an OpenBSD installation handy, but I've also attached `stftime_bug.c`, which should demonstrate the issue. On Linux, the output of the program is: 1900-01-01: 1900 01 1 1901-01-01: 1901 01 2 1902-01-01: 1902 01 3 1903-01-01: 1903 01 4 1904-01-01: 1903 53 5 1905-01-01: 1904 52 0 1906-01-01: 1906 01 1 1907-01-01: 1907 01 2 1908-01-01: 1908 01 3 1909-01-01: 1908 53 5 1910-01-01: 1909 52 6 This is consistent with the Python `isocalendar()` implementation above. Unfortunately, I don't quite understand how the OpenBSD ISO week calculation is *supposed* to work (though I did spend quite some time trying), so I don't know precisely what the problem is. Hopefully this report is helpful. This issue was originally reported on the CPython bug tracker here: https://bugs.python.org/issue31635
import calendar
from datetime import date
def isleap_sum(a, b):
return calendar.isleap((a % 400) + (b % 400))
def calculate_weekdate(year, week, day):
"""
Calculate the day of corresponding to the ISO year-week-day calendar.
This function is effectively the inverse of
:func:`datetime.date.isocalendar`.
:param year:
The year in the ISO calendar
:param week:
The week in the ISO calendar - range is [1, 53]
:param day:
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
:return:
Returns a :class:`datetime.date`
"""
if not 0 < week < 54:
raise ValueError('Invalid week: {}'.format(week))
if not 0 < day < 8: # Range is 1-7
raise ValueError('Invalid weekday: {}'.format(day))
# Get week 1 for the specific year:
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
# Now add the specific number of weeks and days to get what we want
week_offset = (week - 1) * 7 + (day - 1)
return week_1 + timedelta(days=week_offset)
def yconv(year, base):
trail = year % 100 + base % 100
lead = year // 100 + base // 100 + trail // 100
trail = trail % 100
if trail < 0 and lead > 0:
trail += 100
lead -= 1
elif lead < 0 and trail > 0:
trail -= 100
lead += 1
if not (lead == 0 and trail < 0):
trail = 100 * lead + abs(trail)
return abs(trail)
def get_iso(d):
tt = d.timetuple()
DAYSPERWEEK = 7
base = 1900
year = tt.tm_year - base
yday = tt.tm_yday
wday = tt.tm_wday
while True:
l = 365 if not isleap_sum(year, base) else 366
# What yday (-3 ... 3) does the ISO year begin on?
bot = ((yday + 11 - wday) % DAYSPERWEEK) - 3
top = bot - (l % DAYSPERWEEK)
if top < -3:
top += DAYSPERWEEK
top += l
if yday >= top:
base += 1
w = 1
break
if yday >= bot:
w = 1 + ((yday - bot) // DAYSPERWEEK)
break
base -= 1
yday += 366 if isleap_sum(year, base) else 365
if (w == 52 and tt.tm_mon == 1) or (w == 1 and tt.tm_mon == 12):
w = 53
return (yconv(year, base), w, tt.tm_wday + 1)
if __name__ == "__main__":
header = '|'.join(x.center(20) for x in ['Python', 'BSD', 'Mismatch'])
print(header)
print('-' * len(header))
for y in range(1900, 1911):
d = date(y, 2, 1)
t_py = d.isocalendar()
t_bsd = get_iso(d)
t_py_str = f'{t_py[0]:04d}, {t_py[1]:02d}, {t_py[2]}'
t_bsd_str = f'{t_bsd[0]:04d}, {t_bsd[1]:02d}, {t_bsd[2]}'
print('|'.join(x.center(20)
for x in [t_py_str, t_bsd_str, f'{t_py != t_bsd}']))
#include <time.h>
#include <wchar.h>
#include <stdio.h>
int main() {
wchar_t buffer[80];
char dtbuffer[80];
for (int y = 1900; y < 1911; ++y) {
struct tm d;
// This should be unambiguous, no bugs
sprintf(dtbuffer, "%04d-%02d-%02d", y, 1, 1);
strptime(dtbuffer, "%Y-%m-%d", &d);
wcsftime(buffer, 80, L"%G %V %w", &d);
wprintf(L"%s: %ls\n", dtbuffer, buffer);
}
}
signature.asc
Description: OpenPGP digital signature
