LoginSignup
88
59

More than 5 years have passed since last update.

Rubyによる 小数 と Float と BigDecimalについて...(初心者向け)

Last updated at Posted at 2017-02-14

はじめに

これまで、リアルなお金を扱うシステムを作ったこともなかったし、小数を含む演算を実装することがこれまでなかったので小数演算の経験がほとんどありませんでした。(ほんと整数だけで大抵のことは大丈夫!!!)

Float を使うと まるめ誤差 が生じて四捨五入や繰り上げ、繰り下げを利用するときに誤差が影響する場合があるということは話に聞いていたので、実際にどういう影響があるのか、対応する方法などについてRubyを使って調べてみました。

そもそも小数とは

まずは小数についてをざっくり調べて見ると、、 浮動小数点数 - Wikipedia とか 固定小数点数 - Wikipedia とか色々出てくる。。これらをざっくり読むだけではうーんという感じなのでとりあえず表記方法だけみていこうと思います。

小数の表記方法

小数といえば、例えば 1.23 これです!! 小数点の表記方法としては他には以下のような書き方ができます。全て 1.23 という小数を表しています。

  • 1.23 X 100
  • 0.123 X 101
  • 123 X 10-2

仮数部 X 基数 指数部 というような構造となっています。

さらにその他でよく見るのは基数となる 10 を固定して以下のような表記です。

  • 1.23E0
  • 0.123E1 = 0.123E+1
  • 123E-2

仮数部E指数部 というような構造となっています。(指数部には正の値の場合は + をつけても付けなくても同じです。)

上記のように 仮数基数指数 で表現される小数は浮動小数点の表記方法となります。

参考

Float と BigDecimal

Floatについて

  • class Float (Ruby 2.4.0) によるとRuby の Float は 浮動小数点数のクラス で 実装は C言語 の double ということみたいです。

  • 以下のように小数点を含んだ数値をリテラルで表現すると Float として扱われます。

    • プログラム上意識せずリテラルをつかて小数を扱っている場合は Float として扱っていたということになります。
1.2.class
> Float

Floatによる誤差について

irbなどで実行してみるとわかると思います。

1.2 - 1.0
> 0.19999999999999996  #<= 0.2 を期待するがズレが生じる

10.0 / 6.0
=> 1.6666666666666667  #<= 1.66666...と無限に続くはずがまるめられる

IEEE754 規格による2進数による浮動小数点計算では対応できない値の結果がまるめられ誤差が生じるということらしいです。詳しくは以下の記事などがとても参考になります。

BigDecimal について

  • library bigdecimal (Ruby 2.4.0) によると、 浮動小数点数演算ライブラリであり、任意の精度で10進数で表現された浮動小数点を扱えます。
  • <浮動小数点数> = 0.xxxxxxxxx*10**n という 10進形式で数値を保持します。
  • なぜBigDecimalを使わなければならないのか | Java好き の記事にもあるように2進数の浮動小数点演算で生じる丸め誤差が発生せず演算ができるライブラリです。
  • Floatのように組み込みライブラリではないので以下のように require する必要があります。
require 'bigdecimal'
require 'bigdecimal/util'  #<= to_d メソッドが使えるようになる


puts ('1.2'.to_d - 1.0) == 0.2
> true   #<= BigDecimalの演算ではtrueになる

puts (1.2 - 1.0) == 0.2
> false #<= Floatの演算ではfalse

※ BigDecimalをインスタンス化する際の注意
↑ 小数の値を 文字列 で渡して '1.2'.to_d しているのには理由があります。
小数点のリテラル表記の場合は Float として扱われるのでFloatで表現できる桁数で丸められてから to_d されてしまうからです。以下が実行例です。

puts '1.111111111111111111222222222'.to_d
> 0.1111111111111111111222222222E1  #<= 想定通りの桁数となる

puts 1.111111111111111111222222222.to_d
> 0.111111111111111E1    #<= 切り捨てられた桁がある

FloatとBigDecimalのパフォーマンスについて

float(double) を利用するメリットは丸め誤差が生じるけれど、それを無視できるのであればパフォーマンスがいいということです。アニメーションの演算などそういうのは多少の誤差は許容できるのでfloatが使われ、きちんとBigDecimalを使った演算をするときはお金の計算など少しの誤差も許容できない場合です。
その中でパフォーマンスについて簡単なスクリプトで実行してみた所、たしかにBigDecimalのほうが若干遅い感じはしました。(そんなに差はないかも)

require 'benchmark'
require 'bigdecimal'
require 'bigdecimal/util'

n = 100_000
Benchmark.bm(10) do |b|
  b.report('float')      { n.times{ ('1.2'.to_f - 1.0).to_s } }
  b.report("bigdecimal") { n.times{ ('1.2'.to_d - 1.0).to_s } }
end

>                 user     system      total        real
> float        0.120000   0.000000   0.120000 (  0.125545)
> bigdecimal   0.530000   0.010000   0.540000 (  0.528924) #<=若干遅い

丸め処理の指定を切り替えることが出来る

@rryu さんにコメントで指摘いただいたので修正します。

BigDecimalといえど、丸め誤差は生じます。 10進数の演算で 10/3 などは 3.33333... と無限に続くからです。irbなどで見てみると以下のようになります。

puts 10.0.to_d/3
> 0.3333333333333333333E1

BigDecimalを使うことでその丸め処理を任意に変更することができます。デフォルトでは四捨五入となっているようですが、modeを変えることで切替えられるます。

puts '0.1254'.to_d.round(3)
> 0.125E0  #<= 4が四捨五入で切り捨てられている


# 全てを切り上げるように設定する
BigDecimal::mode(BigDecimal::ROUND_MODE, BigDecimal::ROUND_UP)

puts '0.1254'.to_d.round(3)
> 0.126E0 #<= 4が切り上げられている

以下の記事がとても参考になりました

以上

ということでざっくり小数について調べてみたことを書いてみました。

88
59
6

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
88
59