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