はじめに
平成最後のアドベントカレンダーという事もあり、ここでは和暦に関する事を書いてみたいと思います。
と言っても和暦全般的な話ではなく、主に Dateオブジェクトから和暦を得る jisx0301
メソッドについて書きます。
途中からバグ探しの話になっていきます。
jisx0301 メソッドで和暦を得る
Dateクラスには jisx0301メソッドがあります。 このメソッドを使うと和暦を文字列として得ることができます。
動きを見てみましょう。
尚、 ruby のバージョンは 2.5.3 です。
$ pry
pry(main)> require 'date'
=> true
pry(main)> Date.new(1873, 1, 1).jisx0301
=> "M06.01.01"
1873年は明治時代です。ふむふむ。明治の頭文字 'M' が先頭にあるのですね。
ところで JISX0301規格 では明治6年1月1日以降を規格の適用範囲内としているようなので試してみましょう。
上記の例はちょうど明治6年1月1日でした。
では1日前の明治5年12月31日だとどうなるのでしょうか?
pry(main)> Date.new(1872, 12, 31).jisx0301
=> "1872-12-31"
適用範囲外なので西暦で表示されました。区切り文字もドットからハイフンに変わっています。
では未来はどうでしょう? 現在は 2018年12月15日とします。
pry(main)> Date.new(2019, 4, 30).jisx0301
=> "H31.04.30"
いい感じです。
ところで平成最後の日は 平成31年4月30日なので、翌日から新元号が始まります。
やってみましょう。
pry(main)> Date.new(2019, 5, 1).jisx0301
=> "H31.05.01"
平成の H
の文字が.... 新元号が決まっていないので当然と言えば当然の結果ですね。
新元号や明治6年より古い日付の扱いには各要件で対応する必要があります。
和暦をもうちょっといい感じにしたい
簡単なサンプル、 wareki
メソッドを書いてみました。実はこのプログラムにはバグがあります。
引数の日付が とある日だとバグります。
皆さん、バグを見つけてください。
まずは実行結果から
以下は後述する(バグを含む)プログラムで実行した結果です。
puts wareki(Date.new(2018, 12, 15)) # => "平成30年12月15日"
puts wareki(Date.new(1968, 11, 10)) # => "昭和43年11月10日"
puts wareki(Date.new(1921, 12, 31)) # => "大正10年12月31日"
puts wareki(Date.new(1874, 1, 1)) # => "明治7年1月1日"
それなりに動いているようです。
次にプログラムです
バグっている行は何行目でしょうか?
1 require 'date'
2
3 # 和暦な日付文字列を得る。尚、「元年」は「1年」と表記する。
4 # !!!!!!!!! 引数の date は必ず jisx0301 で変換できる値であること !!!!!!!!!
5 def wareki(date = Date.today)
6 _wareki, mon, day = date.jisx0301.split(".")
7 gengou, year = _wareki.partition(/\d+/).take(2) # 元号と和暦の年に分解
8
9 # 元号のアルファベットを漢字に変換
10 gengou.sub!(/[MTSH]/,
11 'M' => '明治',
12 'T' => '大正',
13 'S' => '昭和',
14 'H' => '平成')
15
16 # ゼロサプレスした和暦の日付を返す
17 sprintf("%s%d年%d月%d日", gengou, year, mon, day)
18 end
ここで答えがわかった方は100点あげます。
小ヒント
- 今年の場合だと1年365日のうち、エラーとなる日は4つあります。つまり、365分の4でバグります。
ここで答えがわかった方は80点。
大ヒント
以下の日をパラメータとして渡すとエラーになります。
- 明治8年と9年の全ての日
- 大正8年と9年の全ての日
- 昭和8年と9年の全ての日
- 平成8年と9年の全ての日
と、上記以外の以下の日
- 8月8日
- 8月9日
- 9月8日
- 9月9日
ここで理由がわかった方は60点
理由
8進数→10進数の変換ができなかったからです。
ここでピンと来た方は50点
答え
17行目です。
17 sprintf("%s%d年%d月%d日", gengou, year, mon, day)
year, mon, day はそれぞれ文字列型です。
%d
で数字を数値に変換していますが、ゼロで始まる値は ruby では8進数として扱います。
以下で動きを確認してみます。
pry(main)> 012.to_i
=> 10
pry(main)> 0xff.to_i
=> 255
pry(main)> 08.to_i
SyntaxError: (eval):2: Invalid octal digit
08.to_i
^
8進数は 0~7 の範囲なので、 "08"
や "09"
は変換できないのです。
ちなみに 10
は先頭がゼロではないので10進数として扱われ、問題なく動作しているという訳です。
修正版は以下のようになります。
sprintf("%s%d年%d月%d日", gengou, year.to_i, mon.to_i, day.to_i)
各値を to_i
してあげればよいことになります。
教訓
このバグは仕事中に実際に私が書いてしまったバグです。
幸い大事には至りませんでしたが発覚するまで少し時間がかかりました。
まぁ、シンプルに
"#{gengou}#{year.to_i}年#{mon.to_i}月#{day.to_i}日"
と書けばよかったんですけどね。なんか見づらく感じたので最初の書き方にしました。テクニックに走って溺れた感じです。
これ以来、テストプログラムのサンプルデータに「8月9日」を含めるようにしてます ;-p
最後に
- 実は
jisx0301
メソッドの存在は最近知りました。それまではオレオレ実装で書いてました。 - 個人的には
sprintf
より String#% の方をよく好んで使います。 - https://github.com/tomiacannondale/era_ja というのもあるようです。
ここでふと思ったのだが、なぜ、 sprintf("%d", "08")
はエラーになるのだろう?
pry(main)> sprintf("%d", "08")
ArgumentError: invalid value for Integer(): "08"
from (pry):3:in `sprintf'
エラーメッセージも "Invalid octal digit" じゃないし、そもそも "07" を 7 としてくれるんなら "08" も認識してくれてもいいんじゃね? c言語や perl だとどうなるんだろ? sprintf あったよね。 内部実装を見たくなってきた....
参考
cf. https://developers.freee.co.jp/entry/japanese-calendar-in-application-development