ISO 8601 の週と年

ISO 8601 の週は月曜日から始まり日曜日で終わります。 数値で月曜日を 1、日曜日を 7 で表します。 年の最初の木曜日を含む週が最初の週になり W01 と表記します。 最初の週の前は前年の最後の週になります。

2016 年 5 月 14 日書き直し: Chronological Julian Day (CJD) で計算するように修正しました。

2015-12-28 2015-W53-1
2015-12-29 2015-W53-2
2015-12-30 2015-W53-3
2015-12-31 2015-W53-4
2016-01-01 2015-W53-5
2016-01-02 2015-W53-6
2016-01-03 2015-W53-7
2016-01-04 2016-W01-1
2016-01-05 2016-W01-2
2016-01-06 2016-W01-3
2016-01-07 2016-W01-4
2016-01-08 2016-W01-5
2016-01-09 2016-W01-6
2016-01-10 2016-W01-7

POSIX の tm_mday、 tm_mon、 tm_year から ISO 8601 の wyear、 week を求めるには、 第一週の月曜日を 1 とする通年日 wyday を計算するのが早道です。 おこりうる 7 つの状況をリストアップすると、次のとおりです。

newyearday = 元旦の Chronological Julian Day (CJD)
week01_monday = 第一週の月曜日の CJD

tm_wday  1  2  3  4  5  6  0
        mo tu we TH fr st su
tm_mday  1  2  3  4  5  6  7   newyearday % 7 = 0
wyday    1  2  3  4  5  6  7   week01_monday = newyearday

        mo tu we TH fr st su
tm_mday     1  2  3  4  5  6   newyearday % 7 = 1
wyday    1  2  3  4  5  6  7   week01_monday = newyearday - 1

        mo tu we TH fr st su
tm_mday        1  2  3  4  5   newyearday % 7 = 2
wyday    1  2  3  4  5  6  7   week01_monday = newyearday - 2

        mo tu we TH fr st su
tm_mday           1  2  3  4   newyearday % 7 = 3
wyday    1  2  3  4  5  6  7   week01_monday = newyearday - 3

        mo tu we TH fr st su
tm_mday              1  2  3   newyearday % 7 = 4
tm_mday  4  5  6  7  8  9 10
wyday    1  2  3  4  5  6  7   week01_monday = newyearday + 3

        mo tu we TH fr st su
tm_mday                 1  2   newyearday % 7 = 5
tm_mday  3  4  5  6  7  8  9
wyday    1  2  3  4  5  6  7   week01_monday = newyearday + 2

        mo tu we TH fr st su
tm_mday                    1   newyearday % 7 = 6
tm_mday  2  3  4  5  6  7  8
wyday    1  2  3  4  5  6  7   week01_monday = newyearday + 1

ところで、 上の状況からわかるように、 年末がその年の週になったり次の年の週になったり、 もしくは年始が前の年の週になったりその年の週になったります。 週のための年の扱いを楽にするために、 ユリウス日を使うことにします。 ただし、 曜日を扱うときは、 ユリウス日そのものよりもユリウス日に 0.5 を足した Chronological Julian Day (CJD) が向いているので、 POSIX tm_mday、tm_mon、tm_year から CJD を計算することにします。

// グレゴリオ暦用 Chronological Julian Day (CJD)
long
chrono_julian_day_gregorian (long const tm_mday, long const tm_mon, long const tm_year)
{
    long const mon = tm_mon + 1L;       // tm_mon = 0: Jan, 1: Feb ...
    long const year = tm_year + 1900L;  // tm_year = 101: 2001, ...

    long const y = mon < 3L ? year - 1L : year;
    long const m = mon < 3L ? mon + 12L : mon;
    long const n1 = y * 365L + y / 4L - y / 100L + y / 400L;
    long const n2 = (m + 1L) * 153L / 5L - 63L;
    return n1 + n2 + tm_mday + 1721060L;
}

なお、 POSIX の tm_wday は CJD に 1 を足して 7 で割った余りに一致します。

    tm.tm_wday == (chrono_julian_day_gregorian (tm.tm_mday, tm.tm_mon, tm.tm_year) + 1) % 7

tm_year 年の第一週月曜日の CJD は、上の状況から次のように計算できることがわかります。

long
cjd_iso_week01_monday (long const tm_year)
{
    long const newyearday = chrono_julian_day_gregorian (1, 0, tm_year);
    return newyearday - (newyearday + 3) % 7 + 3;
}

週のための年 wyear と週のための年内の通日 wyday を求めるには、 前年と今年と来年の 3 つの第一週月曜日の CJD を求めておき、 今日の CJD がどの範囲に入っているか比較します。

    long wyear, wyday;
    long const cjd = chrono_julian_day_gregorian (tm_mday, tm_mon, tm_year);
    long const week01_monday_1 = cjd_iso_week01_monday (tm_year - 1);
    long const week01_monday_2 = cjd_iso_week01_monday (tm_year);
    long const week01_monday_3 = cjd_iso_week01_monday (tm_year + 1);
    if (cjd < week01_monday_2) {
        wyear = tm_year + 1900 - 1;
        wyday = cjd - week01_monday_1 + 1;
    }
    else if (cjd < week01_monday_3) {
        wyear = tm_year + 1900;
        wyday = cjd - week01_monday_2 + 1;
    }
    else {
        wyear = tm_year + 1900 + 1;
        wyday = cjd - week01_monday_3 + 1;
    }

これで wyday が決定したので、 何週の何曜日なのかを計算できます。 週のための年は wyear に求まっています。

    int week = (wyday - 1) / 7 + 1;
    int isowday = (wyday - 1) % 7 + 1;