3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Intl.DateTimeFormat の落とし穴

Last updated at Posted at 2025-12-09

はじめに

Intl.DateTimeFormat を使う際に、指定したフォーマットが無視されて、想定外の表示になることがあります。この記事では、そのような落とし穴について扱います。

Intl.DateTimeFormat について

を見ると分かるように、Intl.DateTimeFormat は、与えられた locale やフォーマットに従って日時を文字列に変換します。
formatformatToParts というメソッドで、Date オブジェクトをフォーマットされた文字列に変換することができます。

落とし穴の具体例

const date = new Date(2000, 1, 2, 3, 4, 5);
const formatter = Intl.DateTimeFormat('ja-JP', {
  minute: 'numeric',
  second: 'numeric'
});
console.log(formatter.format(date));

とすると、4:5 のような表示を期待しますが、おそらくほとんどの環境では、04:05 と表示されます。

console.log(formatter.resolvedOptions());

を見てみると、

結果
{
  "locale": "ja-JP",
  "calendar": "gregory",
  "numberingSystem": "latn",
  "timeZone": "Asia/Tokyo",
  "minute": "2-digit",
  "second": "2-digit"
}

となっており、numeric の指定が無視されていることが分かります。
何故このようなことになるのでしょうか?

formatMatcher について

Intl.DateTimeFormat のコンストラクタには、formatMatcher というオプションがあります。

このオプションは、指定したフォーマットを、実際に使用可能なフォーマットにマッチングさせる際のアルゴリズムを選択します。

formatMatcher には以下のいずれかの値を設定することができます。

  • best fit: 実装依存で、basic と同等か、それよりも最適と判断したフォーマットを選択 (デフォルト)
  • basic: 仕様として定義されている BasicFormatMatcher を利用

上記の通り、best fit は最適と判断されるものを選ぶというアルゴリズムで、実装に裁量がゆだねられています。
一方で、basic は仕様で明確に定義されているアルゴリズムです。

そこで、デフォルトの best fit ではなく、basic を利用して再度試してみます。

const formatter = Intl.DateTimeFormat('ja-JP', {
  formatMatcher: 'basic',
  minute: 'numeric',
  second: 'numeric'
});
console.log(formatter.resolvedOptions());
結果
{
    "locale": "ja-JP",
    "calendar": "gregory",
    "numberingSystem": "latn",
    "timeZone": "Asia/Tokyo",
    "minute": "2-digit",
    "second": "2-digit"
}

残念ながら、変化はありませんでした。

Intl.DateTimeFormat の仕様について

internationalization API の仕様である、ECMA-402 を確認してみます。
まずは、BasicFormatMatcher の仕様を見てみます。

簡単に言うと、利用可能なフォーマットと、指定されたフォーマットオプションの近さをスコアとして算出し、最も良いスコアであった利用可能なフォーマットを返すアルゴリズムです。
重要なのは、利用可能なフォーマットのリストの中から返している、ということです。

そこで、BasicFormatMatcherBestFormatMatcher に渡しているフォーマットのリストについても見てみます。

f. Let formats be resolvedLocaleData.[[formats]].[[<resolvedCalendar>]].
g. If formatMatcher is "basic", then
    i. Let bestFormat be BasicFormatMatcher(formatOptions, formats).
h. Else,
    i. Let bestFormat be BestFitFormatMatcher(formatOptions, formats).

上記では、formatMatcher に渡される formatsresolvedLocaleData.[[formats]].[[<resolvedCalendar>]] から取得されることが示されています。
つまり、2-digit にされてしまうのは、利用可能なフォーマットのリストに m:s が存在しないからです。

これはおそらく、m:s という表記が一般的ではないから、という理由であると推測されます。
とはいえ、日本語であれば m分s秒 というようなものは存在していても良い気はしますが…

resolvedLocaleData.[[formats]].[[<resolvedCalendar>]] は、実装定義のもので、これより深く見ようと思うと、各実装を実際に見ていく必要があるので割愛しています。

また、https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots の NOTE 3 によると、実装は基本的に CLDR というリポジトリのデータに準拠することを推奨しているので、こちらを見てみるのも良いと思います。

まとめ

仕様では、BasicFormatMatcherBestFormatMatcher が利用可能なフォーマットのリストの中からベストマッチを選ぶと定義されています。
このリストは実装定義ですが、通常は CLDR というリポジトリに基づいています。
そのため、ユーザーが指定した m:s という組み合わせが利用可能なフォーマットに存在しない場合、マッチングアルゴリズムは mm:ss を最適なものとして選択し、両方とも 2-digit に変更されてしまいます。

回避策

上記のような理由で numeric2-digit に変更されてしまうわけですが、このような変更を避けたい場合、いくつかの方法が考えられます。
例えば、parseInt のような処理を挟んだり、getMinutesgetSeconds を代わりに使ったりするのが挙げられます。
基本的には後者で十分だと思いますが、特殊なタイムゾーンで Date オブジェクトをフォーマットしたいような場合は前者にする必要があるでしょう。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?