この記事で扱う Crystal のバージョンは 1.9.1 です。
結論
#round メソッドの引数で :TIES_AWAY
モードを指定します。
puts 0.5.round(:TIES_AWAY) #=> 1.0
モード指定は Symbol で。String だとエラーになります。
Symbol であればアルファベット部分は小文字でも OK のようです。
丸める対象の桁や基数を指定できる overload を使う場合は名前付き引数 mode
を明示的に書く必要があります。
puts 0.5.round("TIES_AWAY") #=> エラー
puts 0.5.round(:ties_away) #=> 1.0
puts 5.round(-1, mode: :TIES_AWAY) #=> 10
この記事を書いたきっかけ
Crystal で四捨五入しようとして #round を使ったけど期待と異なる結果になって困惑したのがきっかけです。
{0.4, 0.5, 0.6}.each do |a|
puts "#{a} => #{a.round}"
end
上のコードの実行結果は以下のようになります。
0.4 => 0.0
0.5 => 0.0 # えっ...
0.6 => 1.0
ちなみに同様のコードでも Ruby の場合は以下のようになります。(Ruby には Tuple が無いので Array を使っています)
[0.4, 0.5, 0.6].each do |a|
puts "#{a} => #{a.round}"
end
0.4 => 0
0.5 => 1 # はい。
0.6 => 1
ちなみに、本題からそれますが、Ruby の Float#round は小数第一位で丸めたときには整数(Integer)が返るのに対して Crystal では元の数が小数なら結果もあくまで小数(Float64 など)が返るという違いもありますね。(この記事を書いていて今気づきました)
Crystal の #round のデフォルトの丸め動作モードは四捨五入ではなく偶数丸め
Crystal では #round のデフォルトの丸め動作モードは「偶数丸め」で、これは「最も近い偶数に丸める」というものらしいです
公式ドキュメントを見ると、名前付き引数 mode
のデフォルト値が :ties_even
になっています。
def round(mode : RoundingMode = :ties_even) : self
Rounds
self
to an integer value using rounding mode.The rounding mode controls the direction of the rounding. The default is
RoundingMode::TIES_EVEN
which rounds to the nearest integer, with ties (fractional value of0.5
) being rounded to the even neighbor (Banker's rounding).
「偶数丸め」を 0.5, 1.5, … , 9.5 について行うと以下のような結果になります。
0.5.step(to: 9.5, by: 1.0) do |a|
puts "#{a} => #{a.round}"
end
0.5 => 0.0
1.5 => 2.0
2.5 => 2.0
3.5 => 4.0
4.5 => 4.0
5.5 => 6.0
6.5 => 6.0
7.5 => 8.0
8.5 => 8.0
9.5 => 10.0
寡聞にして知らなかったのですが、デフォルトの丸め動作が偶数丸めというのは特に珍しいことでもなくて Python3 の round 関数 や C# の Math.round メソッド などでも同様のようです。
「偶数丸め」は別名「銀行家の丸め」や「銀行丸め」とも呼ばれていて、誤差の累積が小さくなるので銀行家に好まれたのがその由来なのだとか。
(参考)他にはどんなモードがあるのか?
下記のページに記載されているように全部で 5 つのモードが用意されているようです。
モード | 丸め対象の桁が中間値(10 進数での 5)の場合の処理 |
---|---|
TIES_EVEN | 偶数丸め。これがデフォルトの動作モード。 |
TIES_AWAY | 0 から遠い方に丸める。四捨五入。 |
TO_ZERO | 0 に近い方に丸める。切り捨て。truncate。 |
TO_POSITIVE | 正の無限大の方向に丸める。ceil。 |
TO_NEGATIVE | 負の無限大の方向に丸める。floor。 |
それぞれのモードの丸め動作を眺めてみたくて書いたサンプルコードとその結果は以下のとおりです。
Number::RoundingMode.each do |mode|
puts "\n---- #{mode} ----"
{0.4, 0.5, 0.6, -0.4, -0.5, -0.6}.each do |a|
puts "#{a} => #{a.round(mode)}"
end
end
---- TIES_EVEN ----
0.4 => 0.0
0.5 => 0.0
0.6 => 1.0
-0.4 => -0.0
-0.5 => -0.0
-0.6 => -1.0
---- TIES_AWAY ----
0.4 => 0.0
0.5 => 1.0
0.6 => 1.0
-0.4 => -0.0
-0.5 => -1.0
-0.6 => -1.0
---- TO_ZERO ----
0.4 => 0.0
0.5 => 0.0
0.6 => 0.0
-0.4 => -0.0
-0.5 => -0.0
-0.6 => -0.0
---- TO_POSITIVE ----
0.4 => 1.0
0.5 => 1.0
0.6 => 1.0
-0.4 => -0.0
-0.5 => -0.0
-0.6 => -0.0
---- TO_NEGATIVE ----
0.4 => 0.0
0.5 => 0.0
0.6 => 0.0
-0.4 => -1.0
-0.5 => -1.0
-0.6 => -1.0
あとがき
簡素なタイトルのくせして、どういう切り口でどこまで書くか迷いながら&調べながら書きました。
わかりにくかったり説明不足だったり不正確な記述が含まれていたりしたらごめんなさい&ご指摘大歓迎です。