競プロをやる過程で小数の扱いがあったので深堀りしてみました。
to_fメソッドをデカい小数で扱うときに丸め誤差が出てしまった。

きっかけとなった事例

image1

僕が最初に提出したコードは以下です。

x = gets.chomp.to_f
s = x.floor
puts s.to_i

# サンプルを試す
x = 84939825309432908832902189.9092309409809091329
# => 84939825309432916635287552 # 数値が丸まっているため正解できない

RubyのFloatクラスについて

浮動小数点数のクラス。Float の実装は C 言語の double で、その精度は環境に依存します。
一般にはせいぜい15桁です。詳しくは多くのシステムで採用されている浮動小数点標準規格、IEEE (Institute of Electrical and Electronics Engineers: 米国電気電子技術者協会) 754を参照してください。リファレンスより引用

つまり、15桁までは浮動小数点として扱うことができ、16桁以上になると IEEE754 規格に従って数値が変換される。
以下ではあるところから桁数が増えてるにも関わらず、数値が変換されていることがわかる。

また、メソッドto_fはIntegrでもStringでも使える。

Integer#to_f

self を浮動小数点数(Float)に変換します。
selfがFloatの範囲に収まらない場合、Float::INFINITYを返します。リファレンスより引用

String#to_f

文字列を 10 進数表現と解釈して、浮動小数点数 Float に変換します。
浮動小数点数とみなせなくなるところまでを変換対象とします。変換対象が空文字列であれば0.0を返します。リファレンスより引用

浮動小数点とは

聞いたことあるけどなんだっけ。ってやつだったのであらためて学習した。
数字を「仮数」「基数」「指数」で表す小数のこと。

[https://wa3.i-3-i.info/word14959.html]

任意の精度で10進数で表現された浮動小数点を扱えるBigDecimalについて

小数について調べていくうちにBigDecimalというライブラリがあることを知った。

BigDecimalについて

BigDecimalを使うと桁数の多い小数でもきちんと扱える。
※Stringとして渡す必要があることに注意。

bigdecimal は浮動小数点数演算ライブラリです。任意の精度で 10 進表現された浮動小数点数を扱えます。リファレンスより引用

require "BigDecimal"

p BigDecimal("123.1234567890123456789") # => 0.1231234567890123456789e3

どういうときにBigDecimalを使うのか

Floatで扱える範囲を超えるかどうか というのが基準になりそうな気がします。

金額の計算ではBigDecimalを使うのが良いみたいです。

[https://qiita.com/jnchito/items/d0ef71b25732ad5a881c:title]

冒頭の問題をBigDecimalを使って解いてみた

require 'bigdecimal'
 
x = gets.chomp # stringでxに格納
s = BigDecimal(x).floor # BigDecimalで浮動小数として扱って、小数点以下を切り捨て
puts s

# サンプルを試す
x = 84939825309432908832902189.9092309409809091329
# => 84939825309432908832902189

おまけ(自戒)

[https://twitter.com/Aseiide/status/1373562468392267785:embed]

調べていくうちにコードを改善し、最終的には to_fBigDecimal も使わず、 to_i でかいけつできた。
脳死で.to_fやBigDecimalとするのではなく、to_iなども含めて適切なコードを使っていきたい。

番外編(追記)

Fukuoka.rbでお世話になっている @udzuraさんよりコメントを頂きました。
せっかくなのでRationalクラスについて追記していきます。

[https://twitter.com/udzura/status/1373590315647574017:embed]

Rationalクラス

知らないクラスでした。
競プロは知らないものに出会える良い機会だなと思っています。

有理数を扱うクラスです。
「1/3」のような有理数を扱うことができます。IntegerやFloatと同様にRational.newではなく、 Kernel.#Rationalを使用してRationalオブジェクトを作成します。リファレンスより引用

example.rb
Rational(1, 3)       # => (1/3)
Rational('1/3')      # => (1/3)
Rational('0.33')     # => (33/100)
Rational.new(1, 3)   # => NoMethodError