ふだん「0.1」だと思っている値が、実は何者なのか、Rubyを使って調べてみることにしました。
浮動小数点数の内部構造
「Decimal型」のように、十進法による浮動小数点型を提供している環境もありますが、多くのパソコン環境でデフォルトとして使われる浮動小数点数は、IEEE 754形式のものです(Javaなどでは、IEEE 754で実装することが必須となっています)。
これは、指数が「2の累乗」となるので、小さい方は1/2、1/4のような「2の累乗が分母に来る分数」のときだけ厳密な値を表すことができます。つまり、「0.1=1/10」のときすら、正確な値を入れることができません。いちばん近い、「2の累乗が分母に来る分数」に丸めているのです。
Rubyの場合、言語仕様上はIEEE 754が必須ではありませんが、以下ではIEEE 754など「基数が2の累乗」の浮動小数点数を使っていることを前提とします。
桁数指定で文字列化
Float#to_s
は「人間が読みやすい形の文字列表現」とだけなっていて、細かな制御は不可能ですが、C言語から由緒正しく続いているsprintf
であれば、小数点以下の桁数を指定して文字列化できます。
sprintf('%.100f', 0.1)
# => 0.1000000000000000055511151231257827021181583404541015625000000000000000000000000000000000000000000000
ご覧のように、「0.1」だと思っていた値の、下位が表示されるようにはなってきましたが、えんえん0が続いてしまっています。あとは「100」決め打ちではなく、必要な桁数を取れば完成、ということになります。
有理数化してみる
Float#to_r
というメソッドがありますが、一般的な用途では0.1.to_r
が1/10
として認識されないので、使い勝手が悪いです。…が、今回は「厳密に変換してくれる」ことを利用します。
0.1.to_r
# => (3602879701896397/36028797018963968)
IEEE 754など、基数が2の累乗の浮動小数点数では、この分母は必ず2の累乗となります(Ruby 1.9以降では、Rational
は必ず既約分数となります)。そして、10=2*5なので、分母のビット数=この数を小数で表した場合の、小数点以下の桁数となります。それがわかれば、あとはコードに起こすだけです。
実際のコード
便宜上、Float#to_exact_s
とオープンクラスしています。
class Float
def to_exact_s
digits = to_r.denominator.bit_length - 1
sprintf('%.*f', digits, self)
end
end
せっかくなので、0.1~0.9までを展開してみました。
irb(main):009:0> 0.1.to_exact_s
=> "0.1000000000000000055511151231257827021181583404541015625"
irb(main):010:0> 0.2.to_exact_s
=> "0.200000000000000011102230246251565404236316680908203125"
irb(main):011:0> 0.3.to_exact_s
=> "0.299999999999999988897769753748434595763683319091796875"
irb(main):012:0> 0.4.to_exact_s
=> "0.40000000000000002220446049250313080847263336181640625"
irb(main):013:0> 0.5.to_exact_s
=> "0.5"
irb(main):014:0> 0.6.to_exact_s
=> "0.59999999999999997779553950749686919152736663818359375"
irb(main):015:0> 0.7.to_exact_s
=> "0.6999999999999999555910790149937383830547332763671875"
irb(main):016:0> 0.8.to_exact_s
=> "0.8000000000000000444089209850062616169452667236328125"
irb(main):017:0> 0.9.to_exact_s
=> "0.90000000000000002220446049250313080847263336181640625"
大きい方、小さい方、振れ方もそれぞれだということがよくわかりました。