[JavaScript]Intlオブジェクトから過去の元号と改元日を調べる方法

※タイトルに[JavaScript]と書いておきながら、コードはTypeScriptです。JavaScriptにする方法は後述します。

JavaScriptのIntlオブジェクトを使えば、西暦から和暦を調べることができます。
参考:JavaScriptが令和に対応。Intl.DateTimeFormatで日付を和暦(元号)表記に変換する #Chrome – Qiita

例えば、「2019/5/1」を和暦に変換すると「令和1/5/1」になり、「2019/4/30」を和暦に変換すると「平成31/4/30」となります。

ということは、JavaScriptのエンジン内部では元号と改元日の情報を持っているわけです。
というわけで、調べるコードを書いてみました。

/** 元号の漢字取得用フォーマット */
const formatterKanji = new Intl.DateTimeFormat("ja-JP-u-ca-japanese", {
  era: "long",
});
/** 元号のイニシャル取得用フォーマット */
const formatterInitial = new Intl.DateTimeFormat("ja-JP-u-ca-japanese", {
  era: "narrow",
});

/** 一日のミリ秒 */
const ONEDAY_MS = 86400000;

/** 和暦情報の型 */
type WarekiInfo = {
  eraKanji: string;
  eraInitial: string;
  warekiYear: number | null;
  year: number;
  month: number;
  day: number;
  date: Date;
};

/** 和暦取得 */
function getWareki(date: Date): WarekiInfo {
  const partsKanji = formatterKanji.formatToParts(date);
  const partsInitial = formatterInitial.formatToParts(date);
  const eraKanji =
    partsKanji.find((val) => val.type === "era")?.value ?? "不明";
  const warekiYear =
    partsKanji.find((val) => val.type === "year")?.value ?? "-1";
  const eraInitial =
    partsInitial.find((val) => val.type === "era")?.value ?? "不明";
  return {
    eraKanji,
    eraInitial,
    warekiYear: warekiYear === "元" ? 1 : +warekiYear > 0 ? +warekiYear : null,
    year: date.getFullYear(),
    month: date.getMonth() + 1,
    day: date.getDate(),
    date,
  };
}

/** 指定の日付が元年でなければ、元年12月31日を取得 */
function getWarekiStartLastDate(date: Date): WarekiInfo | null {
  const dateWareki = getWareki(date);
  if (dateWareki.warekiYear === null) {
    return null;
  }
  if (dateWareki.warekiYear === 1) {
    return dateWareki;
  }
  const startYear = date.getFullYear() - dateWareki.warekiYear + 1;
  const retDate = new Date(startYear, 11, 31, 0, 0, 0);
  if (retDate.getFullYear() !== startYear) {
    // startYearが2桁の場合に1900年代になっている
    retDate.setFullYear(startYear);
  }
  return {
    ...dateWareki,
    warekiYear: 1,
    date: retDate,
  };
}

/** 同じ年の1月1日を取得 */
function getFirstDate(date: Date) {
  const firstDate = new Date(+date);
  firstDate.setMonth(0);
  firstDate.setDate(1);
  return firstDate;
}

/** 指定日の元号の開始日取得 */
function researchGengouStartDate(date: Date): WarekiInfo | null {
  let baseWareki = getWarekiStartLastDate(date);
  if (baseWareki === null) return null;
  let rightWareki = baseWareki;
  let leftWareki = getWareki(getFirstDate(rightWareki.date));
  if (leftWareki.eraInitial === rightWareki.eraInitial) {
    return leftWareki;
  }

  let middleWareki = getWareki(
    new Date((+rightWareki.date + +leftWareki.date) / 2)
  );
  while (+middleWareki.date - +leftWareki.date > ONEDAY_MS) {
    if (middleWareki.eraInitial === baseWareki.eraInitial) {
      rightWareki = middleWareki;
    } else {
      leftWareki = middleWareki;
    }

    middleWareki = getWareki(
      new Date((+rightWareki.date + +leftWareki.date) / 2)
    );
  }
  if (middleWareki.eraInitial === baseWareki.eraInitial) {
    return middleWareki;
  }
  return rightWareki;
}

/** 過去の元号と改元日取得 */
function getWarekiList(): WarekiInfo[] {
  const warekiList: WarekiInfo[] = [];

  let date = new Date();
  while (true) {
    const wareki = researchGengouStartDate(date);
    if (wareki === null) {
      break;
    }
    warekiList.push(wareki);
    // 改元日の一日前を取得
    date = new Date(+wareki.date - ONEDAY_MS);
  }

  return warekiList;
}

// 元号表示
getWarekiList().forEach((warekiInfo) => {
  console.log(
    `${warekiInfo.eraKanji}(${warekiInfo.eraInitial}):${warekiInfo.year}/${warekiInfo.month}/${warekiInfo.day}`
  );
});

長いですが、やっていることは下記の通りです。
1.指定の日付(最初は今日)を取得して、その和暦を取得する
2.1で取得した和暦が元年ならその日付、そうでなければ和暦の年を西暦から引いて+1したもの(元年)の年の12月31日を取得
3.2で取得した日付と、その年の1月1日の日付を初期値として2分探索で改元日を探す
4.3で取得した改元日の1日前を取得して1~3を取得できるまで繰り返す
5.取得した和暦情報の一覧をコンソールに出力する

TypeScript Playgroundにコード貼って、「Run」を押すと実行できます。

「.JS」タブには変換後のJavaScriptコードが記載されています。

注意点として、明治以前の改元日は現在の西暦(グレゴリオ暦)ではなく、旧暦の日付となっているようです。明治の改元日は西暦だと1868年10月23日なのですが、このコードで調べると1868年9月8日となっています。
また、イニシャルが取得できるのは明治以降のみです(慶応以前は、{era: “narrow”}としても漢字で取得されます)。
後、2分探索で探している都合上、WarekiInfo.dateの日時は0時0分0秒ではないです。

warekiYear === "元" ? 1 : +warekiYear > 0 ? +warekiYear : nullとしている箇所がありますが、場合によっては年が「元」となることがあるので一応、こうしています。
少なくとも、現在のChrome(v138)では、DateTimeFormatの第二引数のoptionsに{era: 'long', year: 'numeric'}と指定すると「元年」となるようです({era: 'long', year: 'numeric', month: 'numeric', day: 'numeric'}とすると「1」になるので仕様がよく分からないです…)。

それと、今回調べて初めて知ったのですが、Dateオブジェクトのコンストラクタの引数の年に2桁の数値をいれると、1990年代になってしまうようです。和暦が始まったの(大化の改心)は645年と3桁なので問題ないのですが、一応、年が変わってないか調べてます。

今に限ったことではないですが、Dateオブジェクトは扱いにくいですね。早く、Temporalが普通に使えるようにならないかなと思います(こちらは旧暦にも対応しているようです)。
Temporalで変わるJavaScriptの日時操作 [JS Modern Features no.1] | gihyo.jp

コメント

タイトルとURLをコピーしました