2
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?

和暦 ↔ 西暦の変換を「境界日まで正確に」書く — 昭和 64 年 1 月 7 日問題

2
Posted at

きっかけ

「令和 7 年って西暦何年?」くらいならスマホの電卓で暗算できますが、**「昭和 64 年 1 月 7 日ってパースできる?」**と聞くと、多くの和暦コンバータがコケます。

  • 昭和 64 年 1 月 7 日 → 1989-01-07
  • 昭和 64 年 1 月 8 日 → 存在しない(その日は平成 1 年 1 月 8 日)

元号の切り替えは「年の境目」ではなく特定の日付で起きるので、「昭和 64 年 = 1989 年」だけでは足りず、月日まで見ないと正しく変換できません。この、境界日をまたぐ変換を正確に扱うツールを作りました。

作ったもの

和暦 ↔ 西暦変換https://sen.ltd/portfolio/era-converter/

スクリーンショット

  • 西暦 → 和暦(1989-01-07 → 昭和 64 年 1 月 7 日)
  • 和暦 → 西暦(昭和62年5月3日 → 1987-05-03)
  • 「元年」パース対応平成元年 = 平成 1 年)
  • 元号一覧表(読み仮名と開始日付き)

vanilla JS + HTML + CSS、ゼロ依存、ビルド不要。元号データは明治以降の 5 元号のみ。node --test で 19 ケース。

元号データは境界日の配列

元号定義は「その元号がいつから始まるか」の一覧です。月日まで持つのがポイント:

export const ERAS = [
  { id: 'meiji',  name: '明治',  reading: 'めいじ',    start: [1868, 10, 23] },
  { id: 'taisho', name: '大正',  reading: 'たいしょう', start: [1912, 7, 30] },
  { id: 'showa',  name: '昭和',  reading: 'しょうわ',  start: [1926, 12, 25] },
  { id: 'heisei', name: '平成',  reading: 'へいせい',  start: [1989, 1, 8] },
  { id: 'reiwa',  name: '令和',  reading: 'れいわ',    start: [2019, 5, 1] },
]

よく見る「明治 = 1868 年」という記述だけでは、1868 年の前半が江戸時代だったことを見落としてしまいます。明治の開始は 1868 年 10 月 23 日(改元詔書の日付)なので、例えば 1868 年 3 月 15 日は「明治 1 年 3 月 15 日」ではありません。

同じく、昭和 64 年は 1989 年 1 月 1 日から 7 日までの 7 日間しか存在しません。8 日以降は平成 1 年。

西暦 → 和暦の逆順探索

年月日の 3 要素を 1 つの単調増加な値として比較できれば、「どの元号の範囲に入るか」を二分探索 or 線形探索できます。タプル比較を使って境界日より新しい元号を逆順に探すのが素直:

function tupleCmp(a, b) {
  if (a[0] !== b[0]) return a[0] - b[0]
  if (a[1] !== b[1]) return a[1] - b[1]
  return a[2] - b[2]
}

export function gregorianToEra(year, month, day) {
  const date = [year, month, day]
  if (tupleCmp(date, ERAS[0].start) < 0) {
    return { error: 'before Meiji' }
  }
  for (let i = ERAS.length - 1; i >= 0; i--) {
    const era = ERAS[i]
    if (tupleCmp(date, era.start) >= 0) {
      const eraYear = year - era.start[0] + 1
      return { era, year: eraYear }
    }
  }
}

逆順で回して最初に境界日を過ぎている元号で止まると、その日付が属する元号が求まります。1989-01-07 なら平成の境界 [1989, 1, 8] に届かないので昭和にマッチ、1989-01-08 なら平成の境界にぴったり or 超えたので平成にマッチ。

元号年 (eraYear) の計算は year - era.start[0] + 1。昭和の開始年が 1926 なので、1989 年は 1989 - 1926 + 1 = 64 年目、正確。

和暦 → 西暦の「元年」パース

和暦の文字列入力には 平成元年 とか 令和元年 という書き方が普通に出てきます。 は「1」の意味で、年を 1 から始める習慣の名残。

export function parseEraString(str) {
  const match = /^(明治|大正|昭和|平成|令和)\s*(\d+|元)(?:(?:\s*(\d+))?(?:\s*(\d+))?)?$/.exec(
    str.trim()
  )
  if (!match) return { error: '読み取れない和暦形式' }
  const [, name, yearStr, month, day] = match
  const era = ERAS.find((e) => e.name === name)
  const year = yearStr === '' ? 1 : Number(yearStr)
  return {
    era,
    year,
    month: month ? Number(month) : undefined,
    day: day ? Number(day) : undefined,
  }
}

正規表現の (\d+|元) で数字または「元」を捕まえて、year 計算時に '元' === 1 に変換。これで 平成元年平成1年 が同じ値に畳み込まれます。

月日は optional。令和7年 だけなら year: 7, month: undefined, day: undefined昭和62年5月3日 なら全部入る。UI 側で「月日を補完する場合は元号の開始月日を使う」か「西暦年だけ返す」を選べます。

範囲チェック:昭和 65 年は存在しない

昭和65年 と入力されると弾きたい。元号の範囲は次の元号の開始年で決まるので:

const nextIdx = ERAS.indexOf(era) + 1
if (nextIdx < ERAS.length) {
  const nextStart = ERAS[nextIdx].start
  if (gregYear > nextStart[0]) {
    return { error: `${era.name} ${eraYear}年 は範囲外(${era.name} は最大 ${nextStart[0] - era.start[0] + 1}年)` }
  }
}

昭和は 1926 + 64 - 1 = 1989 年までなので、昭和 65 年 (1990 年) は平成境界の 1989 を超えて NG。エラーメッセージに「最大 64 年」と具体的な数字を入れているのは、ユーザーが即座に訂正できるようにするため。

テスト

node --test で 19 ケース。境界周辺と「元年」処理が中心:

test('Showa to Heisei boundary: 1989-01-07 is Showa 64', () => {
  const r = gregorianToEra(1989, 1, 7)
  assert.equal(r.era.name, '昭和')
  assert.equal(r.year, 64)
})

test('Showa to Heisei boundary: 1989-01-08 is Heisei 1', () => {
  const r = gregorianToEra(1989, 1, 8)
  assert.equal(r.era.name, '平成')
  assert.equal(r.year, 1)
})

test('Heisei to Reiwa boundary: 2019-04-30 is Heisei 31', () => {
  const r = gregorianToEra(2019, 4, 30)
  assert.equal(r.era.name, '平成')
  assert.equal(r.year, 31)
})

test('Heisei to Reiwa boundary: 2019-05-01 is Reiwa 1', () => {
  const r = gregorianToEra(2019, 5, 1)
  assert.equal(r.era.name, '令和')
  assert.equal(r.year, 1)
})

test('parse 令和元年 as Reiwa year 1', () => {
  const r = parseEraString('令和元年')
  assert.equal(r.era.name, '令和')
  assert.equal(r.year, 1)
})

境界日の両側をテストしているのがミソ。片側だけテストすると >=> に書き間違えたバグが見逃されます。

おわりに

SEN 合同会社の ポートフォリオシリーズ 100+ の 10 件目です。

「平成から遡って慶応 / 江戸まで対応したい」系のリクエストは Issue でお待ちしてます(今のところ明治以降のみ)。

2
0
2

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
2
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?