「午後12:00」はどこから来るのか
この記事は、社内のLT会で発表した内容をもとに、技術記事として再構成したものです。Node.jsやブラウザなど、JavaScriptのランタイムによって「正午」の表記が異なる現象について、調査と考察をまとめました。
正午を返す処理の挙動
まず、以下のコードが何を出力するかを考えてみます。
new Date("2024-01-01T12:00:00").toLocaleTimeString("ja", {
timeStyle: "short",
hour12: true,
});
この関数は、2024年1月1日、お昼12時(正午)を日本語ロケールで12時間表記に変換します。
実行結果の違い
実際に各ランタイムで実行すると、以下のような違いが見られます。
- Node v20:
午後0:00
- Node v22:
午後12:00
また、他の環境でも結果が異なります。
ランタイム | 結果 |
---|---|
Node 18 | 午後0:00 |
Node 20 | 午後0:00 |
Node 22 | 午後12:00 |
Bun 1.2.2 | 午後0:00 |
Chromium | 午後12:00 |
Firefox | 午後0:00 |
Safari | 午後0:00 |
挙動を揃える方法
ランタイムごとの差異を吸収するには、hourCycle
オプションを明示的に指定します。
new Date("2024-01-01T12:00:00").toLocaleTimeString("ja", {
timeStyle: "short",
hourCycle: "h11",
});
この場合、どの環境でも 午後0:00
となります。
ちなみに hourCycle
の値は以下のように定義されています。
-
h11
: 0〜11までの数値で時間を表現 -
h12
: 1〜12までの数値で時間を表現
Intl.Locale.prototype.hourCycle - JavaScript | MDN
なぜ挙動が異なるのか
ロケールが ja
かつ hour12: true
の場合、デフォルトの hourCycle
の値がランタイムによって異なっています。なぜこのような違いが生じるのでしょうか。
実装調査: V8とICU
Node.js や Chrome は V8 エンジンを使っています。V8 の実装を調べると、ロケールごとのデフォルト値は ICU(International Components for Unicode)ライブラリに委ねられており、CLDR(Common Locale Data Repository)のデータが参照されています。
CLDRの該当部分:
supplementalData.xml
<hours preferred="H" allowed="H K h" regions="JP"/>
-
H
: 24時間表示 -
K
: 12時間表示の0-11(午後0時) -
h
: 12時間表示の1-12(午後12時)
この allowed 属性の左から順にデフォルト値が決められるとすれば、日本では24時間表示の「12:00」の次に、12時間表示の「午後0時」が選ばれるのが自然です。
V8の仕様変更
次に、V8 の方に原因があるのではないかと推測し、該当のコードを探してみたところ、
V8のバージョンアップにより、hour12: true
の場合のデフォルト値を決定するロジックが2024年の2月ごろに変更されていたことを発見しました。国コードが JP
の場合は h11
(0〜11時)を返すようになっています。
12時間表示のデフォルト値を返す DefaultHourCycle12
が導入されたコミット: github.com/v8/v8#f92857d
JSDateTimeFormat::HourCycle DefaultHourCycle12(
const icu::Locale& locale, JSDateTimeFormat::HourCycle defaultHourCycle) {
if (defaultHourCycle == JSDateTimeFormat::HourCycle::kH11 ||
defaultHourCycle == JSDateTimeFormat::HourCycle::kH12) {
return defaultHourCycle;
}
if (std::strcmp(locale.getCountry(), "JP") == 0) {
return JSDateTimeFormat::HourCycle::kH11;
}
return JSDateTimeFormat::HourCycle::kH12;
}
このコードを見て「ピン」ときました。locale.getCountry()
が JP
の時だけ h11
になっているため、国コードを明示的に指定すれば h11
になるのではないかと。
Node v22.14.0 で国コードを明示すると、予想通り「午後0:00」と出力されました。
new Date("2024-01-01T12:00:00").toLocaleTimeString("ja-JP", {
timeStyle: "short",
hour12: true,
});
// => '午後0:00'
一方で、変更前(Node < v22)のロジックは日本語では hc_default
は h23
なので h11
が返るようになっていました。
if (hc_default == HourCycle::kH11 || hc_default == HourCycle::kH23) {
// 1. Set hc to "h11".
hc = HourCycle::kH11;
// ii. Else,
} else {
// 1. Set hc to "h12".
hc = HourCycle::kH12;
}
変更の経緯
過去のV8で default: h12
となるロケールで hour12=false
の時に h24
が返っていたという不具合が報告されていました。一方でFirefoxのJSエンジンである SpiderMonkey では h23
を返していました。h24
を使っている国は現在存在しません。(24時間表記で1〜24時を使うのは不自然なため)
この問題を修正するために TC39 の仕様書に修正が求められ、ロジックが整理されたようです。
仕様整理のタイミングで日本だけ hour12=true
でも h11
にするという条件も加えられました。日本以外の他の国は全て h12
を使っているのが理由のようです。
この TC39 の仕様変更をもとに V8 が実装を修正したのがv12.4.75でリリースされ、この V8 の変更が Chrome と Node に取り込まれて今回の問題の再現に至ったというわけです。
V8 の Issue Tracker にバグ報告をしてみた
そもそも ja-JP
が「午後0時」なのに ja
は「午後12時」になるのは正しい仕様なのか?という点について考えたところ、個人的には「正しくない」と判断しました。
現在の V8 は TC39 の「日本だけh11
にする」という仕様を実現するための実装で国コードチェックのロジックが入っていますが、本来は国コードが未指定の ja
だけの時も h11
にするのが正しいと思っています。
そこで V8 に修正をしてもらうべく issue 登録をすることにしました。V8 は Chromium の Issue Tracker を使っているようなので、そちらで同様のバグ報告がないか検索したのですが、見当たらなかったので報告してみました。
そもそも「午後0時」と「午後12時」どちらが正しいのか
調べたところ、どちらかを明確に決めることはできないものの、「午後0時」の方がやや優勢のようでした。
Wikipediaより
午前・午後の概念を明示した法令によると「午後12時」が記載されていますが、「午後0時」の表現を用いる法令も存在しているようです。
しかし、午刻(正午)については、午前の欄にのみ「午前12時」とのみ記載されており、午後の欄には午刻を表す時刻の記載がない。つまり「午後0時」という言い方は、日本における現行法令に基づく限り、「定義上は存在しない」ということになる
...略...
しかし、現行法規では、「午前十二時」の表現を用いる立法例は確認されておらず、逆に本来は定義されていない「午後零時」の表現を用いる法令は存在している
午前と午後 - Wikipedia
家庭用デジタル時計の表示では「午後0時」を用いるのが妥当とされています。
『家庭用デジタル時計の表示は、国の内外を問わずすべて12時間方式を採用 し、“11”時の次に来る数字は、例外なく“12”時を採用している。しか し、午前・午後の表示を伴うデジタル時計については、明かに“12”時は不 適当であり“0”時を用いるのが妥当である。また、デジタル時計の究極の形 としては24時間表示が至当であろう。しかし、この問題は従来からの慣習と 深く関連するので早急に表示方式の規正統一をはかることは時期尚早である。』
時計メーカー業界団体、日本時計協会の見解
小学校の算数教科書
算数の教科書では「午後0時」表記が一般的なようです。
ただ,この表では,午後0時という表記がなく,午前12時と午後1時の間が午前と午後のどちらに属するのかがわからないのですが,本則では,午の刻から子の刻までを午後とするとしています。
小学校算数教科書「わくわく算数」Q&A
まとめ: ランタイムごとの差異の背景
- V8やICU、CLDRの仕様・データにより、デフォルトの
hourCycle
が異なる - V8の仕様変更により、
ja
とja-JP
で挙動が分かれるようになった- 不具合の可能性もあるため、issue 報告済み
- 日本の慣習や法令では「午後0時」表記が主流
- 挙動を揃えたい場合は
hourCycle
を明示的に指定するのが確実
今後も報告した issue の状況を見てこの記事を更新していこうと思います。

TimeTreeのエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion