PHPのdateでYとoを間違えると年末年始の週番号が1年ズレる
PHPのdate()やDateTime::format()で週番号を扱うときは、年に「Y」ではなく「o」(ISO-8601週番号用の年)を使います。Yのまま「W」と組み合わせると、年末年始だけ「2027-W53」のような存在しない週が出てしまいます。
週次集計のレポートで、1月1日のデータだけ「去年の第53週」に入っているのに、見出しは今年の年になっていた、という不具合を見たことがあります。普段は何も起きないので、年が変わる週になって初めて気づくやつですね。
なぜ年末年始だけ週番号がおかしくなるのか
Wが返すのはISO-8601の週番号で、月曜始まり、その年の最初の木曜日を含む週が第1週という決まりです。この決まりだと、1月1日が第52週や第53週に入ったり、12月末が翌年の第1週に入ったりします。つまり「暦の年」と「ISO週の年」は、年末年始の数日だけ一致しません。
Yは暦の年、oはISO週の年を返します。Wと組み合わせていいのはoだけです。
<?php
$d = new DateTimeImmutable('2024-12-30');
echo $d->format('Y-\WW'), "\n"; // 2024-W01
echo $d->format('o-\WW'), "\n"; // 2025-W01
2024年12月30日は月曜日で、ISO週では翌年の第1週に入ります。Yを使うと「2024年の第1週」という、年初の週と区別がつかない値になります。Wの前の「\W」は、Wを書式文字ではなく文字そのものとして出すためのエスケープです。
2026年は53週ある年ってご存じでしたか
2026年は1月1日が木曜日なので、ISO週では53週あります。2026年12月31日(木)は「2026-W53」、2027年1月1日(金)も同じ「2026-W53」です。ここでYを使うと、1月1日が「2027-W53」になってしまいます。
<?php
function isoWeekKey(DateTimeInterface $d): string
{
return $d->format('o-\WW');
}
echo isoWeekKey(new DateTimeImmutable('2027-01-01')), "\n"; // 2026-W53
週キーを作る関数を1か所にまとめておくと、あちこちでYとoが混在する事態を避けられます。集計のキーやキャッシュキーにはとくに効きます。
週番号から日付に戻すときはsetISODateを使う
逆に、年と週番号から月曜日の日付を出したいときは、DateTimeImmutable::setISODate(年, 週, 曜日)を使います。曜日は1が月曜、7が日曜です。ここに渡す年もISO週の年です。
<?php
$monday = (new DateTimeImmutable())->setISODate(2026, 53, 1);
echo $monday->format('Y-m-d'), "\n"; // 2026-12-28
echo $monday->modify('+6 days')->format('Y-m-d'), "\n"; // 2027-01-03
2026年の第53週は、2026-12-28(月)から2027-01-03(日)までです。週をまたいで年も変わる、というのが実際に目で見えると納得感がありますね。なお、setISODateは時刻部分を変えないので、日付だけ欲しいときは setTime(0, 0) を足すか、format側で日付だけ出す形が無難です。
年初の日付が前年の週に入るパターンもある
逆のパターンもあります。2025年12月29日(月)は、暦の上では2025年ですが、ISO週では「2026-W01」です。
<?php
$d = new DateTimeImmutable('2025-12-29');
echo $d->format('o-\WW'), "\n"; // 2026-W01
echo $d->format('N'), "\n"; // 1
echo $d->modify('monday this week')->format('Y-m-d'), "\n"; // 2025-12-29
Nは曜日をISO形式(月曜=1〜日曜=7)で返します。「その週の月曜」を求める modify(‘monday this week’) は月曜始まりで動くので、ISO週と相性がいいです。
まとめ
週番号のWを使うなら、年は必ずoにする。年を戻すときはsetISODateにISO週の年を渡す。この2点だけ覚えておけば、年末年始の謎の週ズレはほぼ避けられると思います。普段のテストデータだと再現しないので、12月末と1月頭の日付を、テストに入れておくのをおすすめします。
よくある質問
Q. 日曜始まりの週番号は取れますか?
A. WはISO-8601固定で月曜始まりです。日曜始まりにしたいときは、自分で基準日を決めて計算するか、intl拡張のIntlCalendarなどロケールに沿った週の扱いを使います。
Q. date(‘o’)とDateTime::format(‘o’)に違いはありますか?
A. 書式文字の意味は同じです。date()はタイムスタンプ、DateTimeはオブジェクトを渡す違いだけで、どちらもYとoは別物として扱われます。
Q. 月ごとの集計でもoを使うべきですか?
A. いいえ。月単位ならYとmで問題ありません。oが必要になるのは、Wで週番号を扱うときだけです。