Web便利ツール集

JavaScriptで西暦を和暦に変換|令和元年・境界日・年だけ変換の罠

JavaScript和暦令和Intl.DateTimeFormat日付

JavaScript で「2019-05-01」を「令和元年5月1日」と表示したい、というのは日本向けのフォームや帳票を作ると必ず出てくる要件です。結論から言うと、年月日が揃っている日付なら Intl.DateTimeFormat に ja-JP-u-ca-japanese を指定するのが最短です。ライブラリは要りません。

ただし、「年だけ」の変換や境界日、タイムゾーンが絡むと、そのままでは間違った和暦を出します。この記事では Node.js v20.20.1(ICU 78.2)で実際に動かした結果をもとに、書き方と落とし穴を整理します。ブラウザ各種や古い Node.js は検証していません。

結論:最短の書き方と、先に知っておきたい注意点

まず、動かして確認できた最小のコードです。

const fmt = new Intl.DateTimeFormat("ja-JP-u-ca-japanese", {
  dateStyle: "long",
  timeZone: "Asia/Tokyo", // 必ず指定する(理由は後述)
});

fmt.format(new Date("2019-05-01T12:00:00+09:00")); // "令和元年5月1日"
fmt.format(new Date("2019-04-30T12:00:00+09:00")); // "平成31年4月30日"
fmt.format(new Date("1989-01-07T12:00:00+09:00")); // "昭和64年1月7日"
fmt.format(new Date("1989-01-08T12:00:00+09:00")); // "平成元年1月8日"

注意点を先に表にします。

落とし穴何が起きるか対処
タイムゾーン未指定同じ瞬間でも実行環境によって元号が変わるtimeZone: "Asia/Tokyo" を指定する
西暦の「年だけ」で変換2019年や1989年は元号が2通りあり、一意に決まらない月日まで持つ。持てないなら両方を併記する
範囲外の日付1850年が「嘉永」、50年が「大化-594年」などになる対応範囲を決めて、範囲外は弾く
未来の日付2100年が「令和82年」になる(将来の改元は反映できない)将来日付は和暦表示しない、または注意書きを出す
環境依存の表記「元年」「R1/5/1」などの出力はICUのバージョンで変わりうる業務で固定したいなら自前テーブルにする

Intl.DateTimeFormat で和暦に変換する方法

Intl.DateTimeFormat のロケールに -u-ca-japanese という拡張を付けると、和暦(日本の暦)で出力されます。ja-JP だけでは西暦のままです。

const d = new Date("2019-05-01T12:00:00+09:00");
const opt = { timeZone: "Asia/Tokyo" };

new Intl.DateTimeFormat("ja-JP", { era: "long", year: "numeric", ...opt }).format(d);
// "西暦2019年" ← 拡張なしだと和暦にならない

new Intl.DateTimeFormat("ja-JP-u-ca-japanese", { era: "long", year: "numeric", ...opt }).format(d);
// "令和元年"

new Intl.DateTimeFormat("ja-JP", { calendar: "japanese", era: "long", year: "numeric", ...opt }).format(d);
// "令和元年"(calendar オプションでも同じ結果)

出力形式ごとの実測結果です(2019年5月1日、Node.js v20.20.1)。

指定出力
dateStyle: "long"令和元年5月1日
dateStyle: "full"令和元年5月1日水曜日
era: "short", year: "numeric"令和元年
era: "narrow", year: "numeric"R元年
year, month, day を "numeric"R1/5/1
era: "long" と year/month/day を "2-digit"令和01/05/01

文字で出す形式では年が「元」になり、数字だけの形式では「1」になる、という使い分けを ICU が行っています。帳票で「令和元年」と書きたいのか「令和1年」と書きたいのかは要件次第なので、出力形式を決めてから指定を選ぶのが安全です。

部品に分けて自分で組み立てる

「元」を数字の1にしたい、年号だけ別の要素にしたい、という場合は formatToParts で部品を取り出せます。

const parts = new Intl.DateTimeFormat("ja-JP-u-ca-japanese", {
  era: "long",
  year: "numeric",
  timeZone: "Asia/Tokyo",
}).formatToParts(new Date("2019-05-01T12:00:00+09:00"));

// [ { type: 'era', value: '令和' },
//   { type: 'year', value: '元' },
//   { type: 'literal', value: '年' } ]

year の値は「元」という文字列で返ります。後で数値として扱おうとして Number("元") や parseInt("元") を呼ぶと NaN になります(実測)。足し算や比較に使うつもりなら、「元」を1に置き換える処理が必要です。

落とし穴1:タイムゾーンで元号が変わる

最も見落としやすいのがこれです。同じ瞬間(2019年4月30日 20:00 UTC=日本時間の5月1日5:00)を、タイムゾーンだけ変えて整形した結果です。

timeZone出力
Asia/Tokyo令和元年5月1日
America/Los_Angeles平成31年4月30日

timeZone を省略すると、実行環境(ブラウザなら利用者の端末、サーバーならサーバー設定)のタイムゾーンが使われます。日本のユーザーの端末だけで動かすなら問題が表に出にくいのですが、サーバーが UTC 設定のとき、日本時間の早朝(0時〜9時)にだけ前日扱いになる、といった形で顕在化します。元号の切り替わり日と重なると、元号まで変わってしまいます。

また、日付だけの文字列は UTC 0時として解釈されます。

new Date("2019-05-01").toISOString(); // "2019-05-01T00:00:00.000Z"

これを日本時間で整形すれば9時になるので日付は変わりませんが、西側のタイムゾーンで整形すると前日になります。日付がずれる原因の切り分けは JavaScriptの日付が1日前になる原因 で別途実測しているので、あわせて参照してください。

落とし穴2:「年だけ」の変換は一意に決まらない

「生年(西暦)から和暦を出す」「西暦年だけ入力されたものを和暦にする」という処理は、次のように書きがちです。

// ダメな例:年だけで判定している
const naive = (y) => (y >= 2019 ? `令和${y - 2018}年` : y >= 1989 ? `平成${y - 1988}年` : "...");

naive(2019); // "令和1年" … 4月までは平成31年
naive(1989); // "平成1年" … 1月7日までは昭和64年
naive(2018); // "平成30年"

元号は年の途中で切り替わるため、境界の年は2通りに分かれます。

西暦前半の元号後半の元号切り替わり日
2019年平成31年令和元年5月1日
1989年昭和64年平成元年1月8日
1926年大正15年昭和元年12月25日
1912年明治45年大正元年7月30日

特に1989年は、昭和64年が1月1日から7日までの7日間しかありません。この7日間に生まれた人は「昭和64年生まれ」ですが、年だけで処理していると全員が「平成元年」と判定されてしまいます。

対処は、可能なら月日まで持つことです。年しか手元に無い場合は、境界年に限って「平成31年/令和元年」のように併記する、または入力欄で元号を選ばせる設計にすると、誤りを出さずに済みます。

落とし穴3:範囲外の日付が、もっともらしい文字列で返る

Intl は例外を出さず、範囲外の日付でも何かしらの文字列を返します。実測した結果です。

日付出力補足
1850-01-01嘉永3年1月1日明治より前の元号になる
1868-09-08慶応4年9月8日明治の開始は 1868-10-23 と出る
1872-12-31明治5年12月31日太陽暦の導入(明治6年1月1日)より前
0050-01-01大化-594年1月3日年が負の数になる
2100-01-01令和82年1月1日未来の改元は反映できない

明治6年(1873年)1月1日より前は、日本が旧暦(太陽暦導入前の暦)を使っていた時期です。Intl が返す「明治5年12月31日」は、現在の暦をさかのぼって当てはめた表示と考えられ、当時の人が実際に使っていた日付とは一致しません。歴史的に正確な和暦が必要な用途(古い戸籍や史料など)では、この出力を使わず、専門の資料にあたってください。

実用上は、対応範囲を「明治6年1月1日〜今日まで」と決めて、それ以外は変換せずに入力エラーにするのが無難です。生年月日の入力であれば、これで困る場面はほとんど無いはずです。

落とし穴4:出力表記は環境で変わりうる

今回の Node.js v20.20.1 では、2019年5月1日が「令和元年5月1日」と出ました。一方で、検索で見つかる過去の解説には「令和1年」「令和02年」といった出力例も載っています。ICU(ブラウザや Node.js が内蔵している国際化データ)のバージョンによって、元年の表記や数字の桁が変わる可能性があると考えられます。ただし、古い環境での挙動は今回確認していないため、断定はできません。

業務システムの帳票のように「表記を固定したい」「環境が変わっても同じ出力にしたい」という場合は、Intl に任せず、次の自前テーブルを使うほうが確実です。

自前の変換テーブル(Date を使わない版)

元号の開始日をテーブルで持ち、Date オブジェクトを経由せずに年月日の数値だけで判定する方法です。タイムゾーンの問題が原理的に起きません。

// 開始日の新しい順に並べる
const ERAS = [
  { name: "令和", start: [2019, 5, 1] },
  { name: "平成", start: [1989, 1, 8] },
  { name: "昭和", start: [1926, 12, 25] },
  { name: "大正", start: [1912, 7, 30] },
  { name: "明治", start: [1868, 10, 23] },
];
const MIN = 18730101; // 明治6年1月1日(太陽暦の導入)より前は対応しない
const key = (y, m, d) => y * 10000 + m * 100 + d;

function toWareki(y, m, d) {
  const k = key(y, m, d);
  if (k < MIN) return null; // 範囲外
  for (const e of ERAS) {
    if (k >= key(...e.start)) {
      const n = y - e.start[0] + 1;
      return { era: e.name, year: n, text: `${e.name}${n === 1 ? "元" : n}年${m}月${d}日` };
    }
  }
  return null;
}

toWareki(2019, 4, 30).text; // "平成31年4月30日"
toWareki(2019, 5, 1).text; // "令和元年5月1日"
toWareki(1989, 1, 7).text; // "昭和64年1月7日"
toWareki(1989, 1, 8).text; // "平成元年1月8日"
toWareki(1872, 12, 31); // null

このコードを、Intl.DateTimeFormat(ja-JP-u-ca-japanese、dateStyle: "long"、timeZone: "UTC")の出力と、1873年1月1日から2026年12月31日までの全56,247日分で突き合わせたところ、不一致は0件でした(Node.js v20.20.1 での実測)。

なお、明治の年数は開始日(1868年)を基準に数える必要があります。試作の段階で、明治の開始日を1873年にして年数の基準までずらしたところ、14,454日分が不一致になりました。テーブルの開始日と、年数の起点を分けて考えないことが大切です。このコードでは、起点を start[0](1868年)とし、対応範囲の下限は別の定数 MIN で持つことで両立させています。

将来の改元に備える

新しい元号が決まったら、ERAS の先頭に1行足すだけで対応できます。Intl は実行環境の ICU が更新されるまで新元号を知らないので、改元後しばらくは Intl と自前テーブルで結果が食い違う期間が出る可能性があります。公的な書類を扱うシステムでは、この点も運用に含めておくとよいでしょう。

和暦から西暦への逆変換

フォームに「令和元年5月1日」と入力された文字列を西暦に直すときも、「元」の扱いが罠になります。

const BASE = { 令和: 2018, 平成: 1988, 昭和: 1925, 大正: 1911, 明治: 1867 };

function fromWareki(text) {
  const m = text.match(/(令和|平成|昭和|大正|明治)(元|\d+)年(\d+)月(\d+)日/);
  if (!m) return null;
  const n = m[2] === "元" ? 1 : Number(m[2]);
  return { y: BASE[m[1]] + n, m: Number(m[3]), d: Number(m[4]) };
}

fromWareki("令和元年5月1日"); // { y: 2019, m: 5, d: 1 }

ここで Number("元") をそのまま呼ぶと NaN になるため、「元」を1に置き換える分岐が必須です。さらに、入力された和暦が実在するかの検証も要ります。「平成31年5月1日」は文字列としては正しい形ですが、実際には存在しない日付(平成は4月30日まで)です。逆変換した結果を toWareki に通して、元の入力と一致するか確認する方法が簡単です。

const r = fromWareki("平成31年5月1日");
toWareki(r.y, r.m, r.d).text; // "令和元年5月1日" → 入力と違うので不正と判断できる

どちらを選ぶか

条件おすすめ
画面表示だけ。多少の表記ゆれは許容できるIntl.DateTimeFormat(timeZone を必ず指定)
帳票・書類。表記を固定したい自前テーブル
入力された和暦を検証したい自前テーブル(逆変換→再変換で一致確認)
年だけしか持っていない変換しない、または境界年は併記する

まとめ

  1. 年月日が揃っているなら、ja-JP-u-ca-japanese の Intl.DateTimeFormat で和暦に変換できる。
  2. timeZone: "Asia/Tokyo" を指定しないと、同じ瞬間でも環境によって元号が変わる。実測では、同じ瞬間が日本で「令和元年5月1日」、ロサンゼルスで「平成31年4月30日」になった。
  3. 西暦の「年だけ」では、2019年や1989年のような境界年の元号が決まらない。月日まで持つか、併記する。
  4. Intl は範囲外でもそれらしい文字列を返す。明治6年より前や将来の日付は、入力エラーにする。
  5. 表記を固定したい場面では、Date を使わず数値だけで判定する自前テーブルが確実。全56,247日で Intl と一致することを確認した。

本記事の出力例は Node.js v20.20.1(ICU 78.2)での実測で、各ブラウザや他のバージョンでの出力は確認していません。公的な書類や契約など、和暦の表記に厳密さが求められる用途では、提出先の書式や指示を優先して確認してください。

関連記事