はじめに
Ruby の浮動小数点数型 Float の実体は C の double であり,通常は IEEE 754 / ISO/IEC 60559 に準拠した倍精度浮動小数点数である.INFINITY や NaN を除いた有限の値は,分母が 2 の累乗である有理数となる.Ruby には多倍長整数となりうる整数型を分子,分母として保持する有理数型 Rational があるので,有限の Float に対応する値を正確に表現する Rational が必ず存在する.本記事では,その Rational 値を返すメソッド Float#to_r を,基数 Float::RADIX が 2 の場合に限定して再実装してみる.
再実装
class Float
def to_r
raise FloatDomainError, self.inspect unless self.finite?
if RADIX == 2
s, e = Math.frexp(self)
if e < MANT_DIG
if self < 0.0
Rational(-Math.ldexp(s, MANT_DIG).to_i, 1<<(MANT_DIG-e))
else
Rational(Math.ldexp(s, MANT_DIG).to_i, 1<<(MANT_DIG-e))
end
else
Rational(self.to_i, 1)
end
else
raise NotImplementedError
end
end
end
解説
Math.#frexp(f) -> [s, e] は,符号を除いた浮動小数点数を仮数部(浮動小数点数$\in [0.5, 1)$)と指数部(整数)に分解する関数で,$|f|=s\times 2^e$ を満たす.double の仮数部は Float::RADIX$=52+1$ ビットなので,$s\times 2^{53}$ は必ず整数となる.Math.#ldexp(s, e) -> f は,frexp の逆を行う関数なので,frexp から得た s を使って,Math.ldexp(s, 53) は整数を表す浮動小数点数を返す.これを分子,$2^{53-e}$ を分母とすればよい.ここで,分子に符号をつけるのを忘れないようにする.
ただし,e が 53 より大きいと,$2^{53-e}$ は $1$ より小さい単位分数となってしまう.これは,もとの self がそもそも整数を表している場合に相当する.よって,この場合は,self を整数型に変換したものを分子,1 を分母とすればよい.$e=53$ の場合も,self は整数である.
また,self が INFINITY や NaN の場合は,Float#to_i と同様に,FloatDomainError を上げるのがよいだろう.
Float::RADIX が 2 でない場合の実装は省略した.実際の(CRubyにおける) Float#to_r は, C 言語で,(基数が 2 の場合については)これと同等の処理(細かく見るとちょっと違うけど)が書かれている.
おわりに
本記事では,Ruby の Float が有限の場合,それに正確に対応する Rational 値を返すメソッド Float#to_r を、Float::RADIX が 2 の場合に限って再実装した.これを使うと,0.1.to_r #=> (3602879701896397/36028797018963968) などとなり,二進浮動小数点数で正確に表現できない実数の,double における実際の近似値を知ることができ,面白い.分母が $2$ の累乗である有理数は,十進でも有限の小数となるが,その桁数は膨大となる場合があり,分数のほうが少ない桁で正確に表示できる.