[ruby-list:42510] Float/String#to_r /Re: floatの値がずれる
From:
Masaaki Sakano <mas@...>
Date:
2006-07-04 16:50:56 UTC
List:
ruby-list #42510
坂野 正明です。
At Tue, 4 Jul 2006 18:51:38 +0900,
rubikitch <rubikitch@ruby-lang.org> wrote:
> From: Sato Hiroshi <hirocy.f01@plala.to>
> Subject: [ruby-list:42503] floatの値がずれる
> Date: Tue, 4 Jul 2006 18:38:19 +0900
> > floatをどんどん加算していくと,ときどき期待する値からずれることがあります.
> これはruby以前の問題で、浮動小数点数の仕様(丸め誤差)です。
> 10進数で1/3が無限小数になるように、2進数で0.001は無限小数になるのです。
今までにこの ruby-list でも何度も繰り返されてきた FAQ、つまり
落とし穴ですね。たとえば、[ruby-list:41514] 辺りのスレッドとか。
一般に、
>> 2. 繰り返される驚き: 改善の目印になる。
(田中哲「使いやすいライブラリ API デザイン」)
http://jp.rubyist.net/magazine/?RubyKaigi2006-0610-3
という話もあります。
僕も、(実行コストでなく)思考コストを下げるために、ここは(Ruby の
仕様を)考える価値あるところだと思います。
# 拙見は、[ruby-list:39484] でまとめたことがあります。
さて、hirocy さんの質問に関しては、Float を使わない、というのが、
最も一般的で明快な解だと思います。
# 原さん御提案のように、Float の "+" の定義を書き換えるのは
# 立派な一案ですが、ちょっと過激かも? (^^; 影響範囲広そうで。
# もっとも、僕の持論は Floatのリテラルを追放しませう、だから、
# 人様のことを言えたものではないかも知れませんけど。
具体的には、Rational に変換するメソッド to_r (下記参照) を使って、
x = [0] ## 0.0 (=Float) ではない。
100.times { x << x[-1] + 0.001.to_r }
p x
とすれば、丸め誤差の入る余地はなく、解決できます。
Float#to_r は、昔、僕が投稿しました。
[ruby-list:35966]
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/35966
でも、当時は 1.6 対応だったので、Ruby 1.8 対応にしたものを、
以下に添付します。String#to_r も含んでいます。
# String#to_r は、あくつさんが、[ruby-list:39574] で要望を出されて
# いましたね。
坂野 正明
# $Id: to_r.rb,v 1.5 2006/07/04 15:43:58 sakano Exp $
=begin
= Float#to_r({:precision=>15})
= String#to_r({:divide=>false, :precision=>15})
= Integer#to_r({:precision=>15})
= Rational#to_r({:precision=>15})
All the to_r methods take the optional one argument.
It can be a simple integer number, meaning the precision,
or a hash as indicated.
Float#to_r({:precision=>15}) returns the instance of Rational,
converting the Float number with the given precision (Integer,
optional). The default precision is the maximum float digit
number that the system can handle (Float::DIG) (or is 15 in Ruby 1.6).
String#to_r({:divide=>false}) is similar. The precision is
infinite, though, unless the precision is also specified.
As the optional 1st argument, either
{:divide=>(true|false), :precision=>Integer},
or simply (true|false) (meaning :divide),
or a simple integer number (meaning :precision)
is accepted.
If the argument is {:divide=>true}, the format like "55/13",
i.e., that including a single '/', is accepted (Default: false).
Integer#to_r and Rational#to_r are similar to the other to_r().
If the precision is specified, the rounded number with the given
precision is returned (nb. it could be identical to the value where
the precision is not specified), otherwise the precision is infinite.
If the given precision exceeds the maximum precision that the system
can handle for a float, then simply the infinite precision is applied
(nb., in Ruby1.6, it tries to apply the given precision, but the
true precision of the returned value is not guaranteed.).
: Examples
Math::PI.to_r({:precision=>2}) # => Rational(31, 10) (=3.1)
Math::PI.to_r({:precision=>5}) # => Rational(3927, 1250) (=3.1416)
"3.1416".to_r # => Rational(3927, 1250) (=3.1416)
"31/10".to_r({:divide=>true}) # => Rational(31, 10) (=3.1)
: Acknowledgement
Many thanks to Nobu Nakada, who has kindly improved the original code!
: References
Subject: [ruby-list:35966] Re: Float#to_r (Re: 9.2 ...)
From: nobu.nakada@nifty.ne.jp
Message-Id: <200209180120.g8I1Krv01082@sharui.nakada.kanuma.tochigi.jp>
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/35966
Subject: [ruby-list:35956] Re: Float#to_r (Re: 9.2 ...)
From: mas@star.le.ac.uk (Masaaki Sakano)
Message-Id: <86vg54wflu.wl@dhcp.star.le.ac.uk>
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/35956
=end
require "rational"
class Float
begin
@@def_precision_to_r=Float::DIG
rescue NameError ## Ruby 1.6 (Float::DIG is not defined)
@@def_precision_to_r=15 ## Integer (>0)
end
def to_r(prec={:precision=>@@def_precision_to_r})
## Input: Precision(Hash or Number)
## Return: Rational
if defined? prec.key?
begin
ofs = (prec[:precision] || prec['precision']).round
rescue NameError ## if (prec['precision']).nil?
ofs = @@def_precision_to_r
end
else
## Form like to_r(5) is accepted.
ofs = prec.round
end
ofs -= Math.log10(self.abs).floor + 1
## i.e., ofs = ofs(=prec.round) - Math.log10(self.abs).floor - 1
num = (self * 10.0 ** ofs).round
if ofs > 0
Rational.reduce(num, 10.power!(ofs))
else
Rational.new!(num * 10.power!(-ofs), 1)
end
end
end
class String
### For Ruby 1.6 or before
if ! defined?("".lstrip)
def lstrip
sub(/^[ \t\r\n\f\v]*/, '')
end
end
if ! defined?("".rstrip)
def rstrip
sub(/[ \t\r\n\f\v]*$/, '')
end
end
def to_r(hsallowdivide={'divide'=>false})
## Input: {"divide"=>true} if you want to allow the format 'A/B' (Default: false)
## : Simple true|false is accepted, too.
## Return: nil if /^[\s\n]*$/, or TypeError if non-number, else Rational
if hsallowdivide == true
allowdivide=true
hsallowdivide={'divide'=>true}
elsif defined?(hsallowdivide.key?)
allowdivide = hsallowdivide['divide'] || hsallowdivide[:divide] ## possibly nil
else
allowdivide=false
if hsallowdivide == false
hsallowdivide={'divide'=>false}
end
end
selfstripped=self.strip
if /[\n\r]/ =~ selfstripped
raise TypeError, "(#{selfstripped}) does not look like a number."
elsif selfstripped.empty?
return nil
end
if allowdivide
matched=%r@^([^/]+)(?:(/)\s*(\S+))?$@.match(selfstripped)
if matched.nil?
raise TypeError, "(#{selfstripped}) does not look like a number."
elsif matched[2].nil?
denom=1
else
denom=matched[3].__to_r_core
end
(matched[1].rstrip.__to_r_core/denom).to_r(hsallowdivide)
else
(selfstripped.__to_r_core).to_r(hsallowdivide) # to_r(): for precision
end
end
def __to_r_core
## self should be already "stripped".
# s=self.strip
## Accept -0009 but -0009.5
#if (/^([+\-]?)0+(\d*)$/ =~ self) # NOT accept "_"
if (/^([+\-]?)0+([0-9_]*)$/ =~ self) # Accept "_"
self.to_i.to_r
## Style in Ruby 1.6 or before
# if (/^([+\-]?((0|[1-9]\d*)?\.[0-9]+|(0|[1-9]\d*)\.?)([Ee][+\-]?\d+)?)$/ =~ self)
# elsif (/^(([+\-]?(?:0|[1-9]\d*))(?:\.(\d+))?)(?:[Ee]([+\-]?)(\d+))?$/ =~ self)
## ## Note that the string including "_" is NOT regarded as a number.
elsif (/^(([+\-]?(?:0|[1-9][0-9_]*))(?:\.(\d[0-9_]*))?)(?:[Ee]([+\-]?)(\d[0-9_]*))?$/ =~ self)
# The string including "_" IS regarded as a number.
# pow = (10.power($5.size) rescue 1)
if $5.nil?
pow=1
else
pow = 10.power!($5.to_i)
end
if $3.nil? ## NOT \d.\d form.
numer=$2.to_i
denom=1
else
numer=($2+$3).to_i
denom=10.power!($3.size)
end
if $4 == '-'
Rational(numer,denom) / pow
else ## $4 == '+' OR ''
Rational(numer,denom) * pow
end
else
raise TypeError, "(#{self}) does not look like a number."
end
end
end
class Integer
def to_r(prec=nil)
Rational(self, 1).to_r(prec)
end
end
class Rational
def to_r(prec=nil)
## Input: [precision]
## If the precision is defined, the value closest within the precision
## is returned. If the precision is greater than the maximum
## precision that the system can handle for a float, self
## is simply returned, regardless of the input "precision".
if prec.nil?
self
elsif defined? prec.key?
precnow = (prec[:precision] || prec['precision'])
if precnow.nil?
self
else
begin
if precnow > Float::DIG
self
else
self.to_f.to_r(prec)
end
rescue NameError ## Ruby 1.6 (Float::DIG is not defined)
self.to_f.to_r(prec)
end
end
else
## Assuming the form like to_r(5).
begin
if prec > Float::DIG
self
else
self.to_f.to_r(prec)
end
rescue NameError ## Ruby 1.6 (Float::DIG is not defined)
self.to_f.to_r(prec)
end
end
end
end