Sorry, in my previous bug report I accidentally included a version of the script that checks the first day in February rather than the first day in January. I've included the version that creates the quoted output in this e-mail. (change line 96 to `d = date(y, 1, 1)`)
On 9/18/18 10:30 AM, Paul Ganssle wrote: > 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, 1, 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}']))
signature.asc
Description: OpenPGP digital signature
