LoginSignup
2
0

Crystal で四捨五入するには

Last updated at Posted at 2023-08-15

この記事で扱う 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 of 0.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

あとがき

簡素なタイトルのくせして、どういう切り口でどこまで書くか迷いながら&調べながら書きました。
わかりにくかったり説明不足だったり不正確な記述が含まれていたりしたらごめんなさい&ご指摘大歓迎です。

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