第53週の週番号が「01」になる件に対応する関数を書いた

※下書き段階で投稿してしまったので、再投稿

PHPのdate('W')が返す週番号は、第53週では'01'を返します。strftime('%W')も同様です。

  <?php
  echo date('W', strtotime('2013-12-31')), PHP_EOL; // '01'

この仕様は、ISO規格に基づいています。

http://ja.wikipedia.org/wiki/ISO_8601#.E5.B9.B4.E3.81.A8.E9.80.B1.E3.81.A8.E6.9B.9C.E6.97.A5

以下、上記Wikipedia記事より引用

年末において以下の曜日に該当する場合、その日は当年最終週の曜日としてでは無く、翌年第1週の曜日として扱うものとされている。
12月29日が月曜日の場合。
12月30日が月曜日または火曜日の場合。
12月31日が月曜日・火曜日・水曜日のいずれかの場合。

この挙動は、PHPのバグではありません。しかし、PHPアプリケーションにおいてバグを作りこみがちな挙動の1つだと思います(実際、私も2014年最初に修正したバグは、この挙動に起因するバグでした)。

以下は、上記条件の場合には、'01'ではなく'53'を返す関数です。

<php
/**
* date('W')は、以下の場合に週番号として'01'を返す。
* (1) 12月29日が月曜日の場合。
* (2) 12月30日が月曜日または火曜日の場合。
* (3) 12月31日が月曜日・火曜日・水曜日のいずれかの場合。
*
* この関数では、12/29~31で、週番号が'01'となる場合には、'53'を返す。
*
* @param $time UNIXタイムのタイムスタンプ
* @return string
*/
function getWeekNumber($time)
{
    $week_number     = date('W', $time); // 週番号(01から52)
    $month_and_day   = date('m-d', $time); // 月-日
    $day_of_the_week = date('w', $time); // 0 (日曜)から 6 (土曜)

    // 処理方法(1): 仕様通りに月日と曜日で判定
    if ($month_and_day === '12-29' && in_array($day_of_the_week, array(1))) {
        $week_number = '53';
    }
    if ($month_and_day === '12-30' && in_array($day_of_the_week, array(1, 2))) {
        $week_number = '53';
    }
    if ($month_and_day === '12-31' && in_array($day_of_the_week, array(1, 2, 3))) {
        $week_number = '53';
    }
    
    // 処理方法(2): 月日+date('W')の値で判定(こちらでも実用上は問題ないかと…)
    // if (in_array(date('m-d', $time), array('12-29', '12-30', '12-31')) && date('W', $time) === '01') {
    //    $week_number = '53';
    // }

    return $week_number;
}

「53」を返す処理については、仕様通りに「月日」+「曜日」で判定を行っています。ただ、この関数が必要な場合というのは、「01」が返されて困る場合に「53」を返すことだと思います。したがって、処理方法(2)でも、実用上は問題無いでしょう。

以下は、テスト付きのバージョンです。phpコマンドで実行して、結果が「ok」ならテストに合格したことになります。また、TAPに基づいたテストなので、proveコマンドでもテストできます(proveで実行するため、1行目にシバンを付けています)。

#!/usr/bin/env php
<?php

test(9);
is(getWeekNumber(strtotime('2014-12-29')), '53', 1, '2014年12月29日(月) => 53');
is(getWeekNumber(strtotime('2014-12-30')), '53', 2, '2014年12月30日(火) => 53');
is(getWeekNumber(strtotime('2014-12-31')), '53', 3, '2014年12月31日(水) => 53');
is(getWeekNumber(strtotime('2013-12-30')), '53', 4, '2013年12月30日(月) => 53');
is(getWeekNumber(strtotime('2013-12-31')), '53', 5, '2013年12月31日(火) => 53');
is(getWeekNumber(strtotime('2012-12-31')), '53', 6, '2012年12月31日(月) => 53');
is(getWeekNumber(strtotime('2011-12-29')), '52', 7, '2011年12月29日(木) => 52');
is(getWeekNumber(strtotime('2011-12-30')), '52', 8, '2011年12月30日(金) => 52');
is(getWeekNumber(strtotime('2011-12-31')), '52', 9, '2011年12月31日(土) => 52');

/**
 * @param int $number_of_tests
 */
function test($number_of_tests = 0)
{
    if ($number_of_tests > 0) {
        echo '1..', $number_of_tests, PHP_EOL;
    }
}

/**
 * @param        $got
 * @param        $expected
 * @param int    $test_number
 * @param string $description テストの説明
 * @param string $directive   「TODO」又は「SKIP」+ TODO/SKIPである理由
 */
function is($got, $expected, $test_number, $description = '', $directive = '')
{
    if ($expected === $got) {
        echo 'ok ', $test_number;
    } else {
        echo 'not ok ', $test_number;
    }
    if ($description !== '') {
        echo ' - ', $description;
    }
    if ($directive !== '') {
        echo ' # ' . $directive;
    }
    echo PHP_EOL;
    if ($expected !== $got) {
        echo '# got:      ', $got, PHP_EOL;
        echo '# expected: ', $expected, PHP_EOL;
    }
}

/**
* date('W')は、以下の場合に週番号として'01'を返す。
* (1) 12月29日が月曜日の場合。
* (2) 12月30日が月曜日または火曜日の場合。
* (3) 12月31日が月曜日・火曜日・水曜日のいずれかの場合。
*
* この関数では、12/29~31で、週番号が'01'となる場合には、'53'を返す。
*
* @param $time UNIXタイムのタイムスタンプ
* @return string
*/
function getWeekNumber($time)
{
    $week_number     = date('W', $time); // 週番号(01から52)
    $month_and_day   = date('m-d', $time); // 月-日
    $day_of_the_week = date('w', $time); // 0 (日曜)から 6 (土曜)

    // 処理方法(1): 仕様通りに月日と曜日で判定
    if ($month_and_day === '12-29' && in_array($day_of_the_week, array(1))) {
        $week_number = '53';
    }
    if ($month_and_day === '12-30' && in_array($day_of_the_week, array(1, 2))) {
        $week_number = '53';
    }
    if ($month_and_day === '12-31' && in_array($day_of_the_week, array(1, 2, 3))) {
        $week_number = '53';
    }
    
    //処理方法(2): 月日+date('W')の値で判定(こちらでも実用上は問題ないかと…)
    // if (in_array(date('m-d', $time), array('12-29', '12-30', '12-31')) && date('W', $time) === '01') {
    //    $week_number = '53';
    // }

    return $week_number;
}