会計ソフトを作る上で避けては通れない和暦の話

エンジニアの大橋 @_tohashi です。会計freeeで確定申告や記帳機能などの開発を担当しています。

Webに限らず、日本向けのアプリケーションにおける特有の要素として和暦があります。プロダクトによっては最初から和暦を扱わずに西暦に統一してしまうという手もありますが、弊社のプロダクトのように会計や労務管理に関わるものの場合、決算書上の表記など和暦が必要とされる場面は多々あるため避けて通ることはできません。本記事ではUIや実装における和暦の扱いについてご紹介したいと思います。

f:id:t930:20170321155846j:plain

和暦の範囲

そもそも「和暦」とはどこからどこまでの期間を指すのでしょうか。Wikipediaによれば

和暦(われき)は、元号とそれに続く年数によって年を表現する、日本独自の紀年法である。邦暦(ほうれき)とも。また「和暦」は、西暦に対する表現としても使用されることが多い。 この手法自体は東アジアで広く行われてきたが、日本独自の元号を用いているため日本固有の紀年法となる。飛鳥時代の孝徳天皇によって西暦645年に制定された「大化」がその始まりであり、以来15世紀に亘って使われ続けてきている。 たとえば、西暦2000年は平成12年に当たる。明治改暦(明治6年/西暦1873年)以降、グレゴリオ暦を採用しており西暦とも月日が一致している。

とかなり広い期間が和暦とされています。

一方、日時の国際標準規格である ISO 8601 の日本独自の拡張である JIS X 0301 では、グレゴリオ暦に改暦された明治6年1月1日以降を規格の適用範囲内としています。

実際に利用する範囲から考えてみると、例えば弊社のプロダクトで日付入力が発生する箇所としては

  • 取引の発生日
  • 従業員の誕生日
  • 会計期間の設定
  • 固定資産の取得日

などがありますが、固定資産の耐用年数は最長でも水道用ダムの80年、公式に認定されている寿命の最長記録は122歳です。この場合システム上における「和暦」は、2017年現在においては JIS X 0301 の通り明治6年1月1日以降を考慮しておけば問題ないと言えるでしょう。

UI

日付選択UIにおける和暦の扱いとしては例えば下記のようなものが挙げられます。

元号選択

f:id:t930:20170321140018g:plain

和暦をユーザーに選択させる方法です。直感的ではありますが、入力したい年が西暦でしかわからない場合は調べる必要があります。実装面では「昭和100年」といったケースのバリデーションや、DBにDate型のカラムとして保存するのであればどこかでキャストするといった事が必要になります。

西暦と併記

f:id:t930:20170321140032g:plain

西暦と和暦を併記する方法です。西暦・和暦の片方しかわからなくても入力可能ですが、セレクトボックスが縦に長くなりがちという欠点もあります。

DatePicker

f:id:t930:20170321140034g:plain

JavaScriptによる実装もありますが、画像は Google Chrome において input type="date" を使用したもの1です。前後1年間ぐらいの日付を選択するのには便利ですが、生年月日のように数十年遡るような場合はあまり向いているとは言えないでしょう。

自由入力

f:id:t930:20170321140036g:plain

西暦・和暦どちらも入力可能にしておき、適宜パースして扱う方法です。扱うフォーマットの数によっては実装コストがそれなりに大きくなるでしょう。

いずれもメリット・デメリットがあるので、日付の用途に応じて最適なUIを選択していくのが大事ですね。

実装

続いて実装面における和暦の扱いを言語ごとに見ていきます。

Ruby

Ruby の Date クラスには JIS X 0301 書式の日付を返す jisx0301 というメソッドが用意されているため、このように簡単に和暦を得ることができます。

require('date')
Date.new(2017, 3, 15).jisx0301 # => "H29.03.15"

境界値も問題ありません。

Date.new(1989, 1, 7).jisx0301 # => "S64.01.07"
Date.new(1989, 1, 8).jisx0301 # => "H01.01.08"

上述したように明治6年以前は JIS X 0301 の対象外であるため、そのまま西暦(ISO 8601 拡張形式)が返ります。

Date.new(1873, 1, 1).jisx0301 # => "M06.01.01"
Date.new(1872, 12, 31).jisx0301 # => "1872-12-31"

逆に和暦をパースすることも可能です。

Date.parse('H29.01.01') # => #<Date: 2017-01-01 ((2457755j,0s,0n),+0s,2299161j)>
Date.parse('S50.01.01') # => #<Date: 1975-01-01 ((2442414j,0s,0n),+0s,2299161j)>

JIS X 0301 の範囲外でもパースできますが、グレゴリオ暦と太陽太陰暦のずれが考慮されていない点は留意が必要でしょう。

Date.parse('M5.12.31') # => #<Date: 1872-12-31 ((2405159j,0s,0n),+0s,2299161j)>
Date.parse('M1.01.01') # => #<Date: 1868-01-01 ((2403333j,0s,0n),+0s,2299161j)>

ちなみにこのような日付もパース可能です。

Date.parse('M50.01.01') # => #<Date: 1917-01-01 ((2421230j,0s,0n),+0s,2299161j)>

Java

locale を ja_JP_JP に設定することで和暦への対応が可能となります。

import java.util.*;
import java.text.*;

public class Wareki {
    public static void main () {
      Locale locale = new Locale("ja", "JP", "JP");
      Calendar calendar = Calendar.getInstance();
      DateFormat dateFormat = new SimpleDateFormat("Gyy.MM.dd", locale);

      // 西暦から和暦へ
      calendar.set(2017, 2, 20);
      dateFormat.format(calendar.getTime()); // H29.03.20

      calendar.set(1989, 0, 7);
      dateFormat.format(calendar.getTime()); // S64.01.07

      calendar.set(1989, 0, 8);
      dateFormat.format(calendar.getTime()); // H01.01.08

      calendar.set(1872, 11, 31);
      dateFormat.format(calendar.getTime()); // M05.12.31

      // 和暦から西暦へ
      dateFormat.parse("H29.01.01"); // Sun Jan 01 00:00:00 JST 2017
      dateFormat.parse("M6.01.01");  // Wed Jan 01 00:00:00 JST 1873
      dateFormat.parse("M1.01.01");  // Wed Jan 01 00:00:00 JST 1868
    }
}

Ruby と異なり明治6年以前もそのまま和暦に変換されるため、厳密には JIS X 0301 に準拠していないとも言えます。

その他

他にも Swiftなでしこなどが和暦に対応しているようですが、多くのプログラミング言語はそうではないため、ライブラリを使うか自前で変換ロジックを用意する必要があります。

例えば JavaScript では日付を扱うライブラリとして Moment.js が有名ですが、残念ながら和暦には対応していないようです。以下は西暦を和暦に変換する拙作のライブラリ wareki を基にした実装の一例です。

const eraDataList = [
  {
    code: 'H',
    firstDate: '1989-01-08',
  },
  {
    code: 'S',
    firstDate: '1926-12-25',
  },
  {
    code: 'T',
    firstDate: '1912-07-30',
  },
  {
    code: 'M',
    firstDate: '1873-01-01'
  }
];

function fillZero(value) {
  return `0${value}`.slice(-2);
}

function toWareki(value = Date.now()) {
  const dateObj = new Date(value);
  const year = dateObj.getFullYear();
  const month = dateObj.getMonth() + 1;
  const date = dateObj.getDate();
  let wareki = value;

  for (let i = 0; i < eraDataList.length; i++) {
    let eraData = eraDataList[i];
    let eraFirstDateObj = new Date(eraData.firstDate);
    if (dateObj - eraFirstDateObj >= 0) {
      let eraYear = year - eraFirstDateObj.getFullYear() + 1;
      wareki = `${eraData.code}${fillZero(eraYear)}.${fillZero(month)}.${fillZero(date)}`;
      break;
    }
  }
  return wareki;
}

toWareki('1989-01-01');
// => "S64.01.01"

新元号について

現在、天皇陛下の譲位に伴い平成31年元日より新元号となることが検討されています。当然 JIS X 0301 においてはまだ未定義のため、現時点では未来の日時は全て平成として扱いつつ、新元号の制定に伴うアップデートの準備をしておくのが良さそうです。

さいごに

日付を扱う上で和暦の対応は面倒なところもありますが、本記事が一助となれば幸いです。

ちなみに僕は昭和64年生まれなのですが、たまにこういう悲しい目に合います。

f:id:t930:20170321140039p:plain

freeeでは和暦にこだわりのあるエンジニアを募集しています!


  1. 一部ブラウザでは未対応