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}']))




Attachment: signature.asc
Description: OpenPGP digital signature

Reply via email to