Ruby Float Round Error Bug? - ruby

I am getting the following rounding error when I try to Unit test the class below:
class TypeTotal
attr_reader :cr_amount, :dr_amount,
:cr_count, :dr_count
def initialize()
#cr_amount=Float(0); #dr_amount=Float(0)
#cr_count=0; #dr_count= 0
end
def increment(is_a_credit, amount, count=1)
case is_a_credit
when true
#cr_amount = Float(amount)+ Float(#cr_amount)
#cr_count += count
when false
#dr_amount = Float(amount)+ Float(#dr_amount)
#dr_count += count
end
end
end
Unit Test:
require_relative 'total_type'
require 'test/unit'
class TestTotalType < Test::Unit::TestCase
#rounding error
def test_increment_count()
t = TypeTotal.new()
t.increment(false, 22.22, 2)
t.increment(false, 7.31, 3)
assert_equal(t.dr_amount, 29.53)
end
end
Output:
1) Failure:
test_increment_count(TestTotalType) [total_type_test.rb:10]:
<29.529999999999998> expected but was
<29.53>.
1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
I am using Floats because it was recommended in the Pick Ax book for dollar values because they shouldn't be effective by round errors.
I am running on ruby 1.9.2p290 (2011-07-09) [i386-mingw32] on Windows 7 64-bit Home and Windows XP 32-bit Pro.
I've tried
casting my variables to floats
removing += and spelling out increment
The behavior appears random:
12.22 + 7.31 works
11.11 + 7.31 doesn't work
11.111 + 7.31 works
Any ideas whats going wrong?

Are you sure that was the advice given? I'd expect the advice to be not to use Floats, precisely because they use binary floating point arithmetic, and so are prone to rounding errors. From the Float documentation:
Float objects represent inexact real numbers using the native architecture's double-precision floating point representation.
If you could quote the exact advice you're referring to, that would help.
I would suggest you use BigDecimal instead, or use an integer with implicit units of "cents" or "hundreds of a cent" or something similar.

The problems with float are already mentioned.
When you test with floats, you should not use assert_equal but assert_in_delta.
Example:
require 'test/unit'
class TestTotalType < Test::Unit::TestCase
TOLERANCE = 1E-10 #or another (small) value
#rounding error
def test_increment_count()
t = TypeTotal.new()
t.increment(false, 22.22, 2)
t.increment(false, 7.31, 3)
#~ assert_equal(t.dr_amount, 29.53) #may detect float problems
assert_in_delta(t.dr_amount, 29.53, TOLERANCE)
end
end

The solution was to use Big Decimal:
require 'bigdecimal'
class TypeTotal
attr_reader :cr_amount, :dr_amount,
:cr_count, :dr_count
def initialize()
#cr_amount=BigDecimal.new("0"); #cr_count=0,
#dr_amount=BigDecimal.new("0"); #dr_count=0
end
def increment(is_a_credit, amount, count=1)
bd_amount = BigDecimal.new(amount)
case is_a_credit
when true
#cr_amount= bd_amount.add(#cr_amount, 14)
#cr_count += count
when false
#dr_amount= bd_amount.add(#dr_amount, 14)
#dr_count = count
end
end
The Pick Ax (p53) book used float for currency as an example but had a foot note explaining that you need to either add .5 cent to when you display the value or use Big Decimal.
Thanks for are your help!

Related

rand(Range) - no implicit conversion of Range into Integer

A follow up on the question How to create a random time between a range
.
Kernel#rand works with Time range:
require 'time'
rand(Time.parse('9 am')..Time.parse('11:30 am'))
But when I tried with a custom class, I ended up with the error:
`rand': no implicit conversion of Range into Integer (TypeError)
class Int
include Comparable
attr_reader :num
def initialize(num)
#num = num
end
def succ
Int.new(num + 1)
end
def <=>(other)
num <=> other.num
end
def to_s
"Int(#{num})"
end
def to_int
#num
end
alias_method :inspect, :to_s
end
puts rand(Int.new(1)..Int.new(3))
Why? What am I missing in the custom class? Can we use such a custom class in rand(Range)?
I don't know of any documentation for what specifically Kernel#rand expects from a Range argument but we can get a look at what's going on by overriding respond_to? in your class and then watching as things fall apart:
def respond_to?(m)
puts "They want us to support #{m}"
super
end
Doing that tells us that rand wants to call the #- and #+ methods on your Int instances. This does make some sense given that rand(a..b) is designed for working with integers.
So we throw in quick'n'dirty implementations of addition and subtraction:
def -(other)
self.class.new(to_int - other.to_int)
end
def +(other)
self.class.new(to_int + other.to_int)
end
and we start getting rand Ints out of our calls to rand.
I'm not sure where (or if) this is documented so you'll have to excuse a bit of hand waving. I normally spend some time rooting around the Ruby source code to answer this sort of question but I lack the time right now.
To add a bit more to #mu-is-too-short's answer, I checked the source of Random#rand and the following is the current implementation logic for rand(Range):
Get the begin, end, and vmax from the Range object (call range_values), where vmax is computed as (call id_minus):
vmax = end - begin
vmax will be used as the upper bound of the random number generation later.
This requires the custom class to have - method defined.
Generate a random number based on the type of vmax:
If it is not Float and can be coerced to Integer (rb_check_to_int), generate a random Integer less than vmax.
In this case, the - method should either return an Integer, or an object which responds to to_int method.
If it is Numeric and can be converted to Float with to_f, (rb_check_to_float), generate a random Float number less than vmax.
In this case, the - method should return a Numeric number which can be converted to Float with method to_f.
Add the random number to begin to yield the result (call id_add).
This requires the custom class to have + method defined, which accepts the result of the random number generated in step 2 (either Integer, or Float) and returns the final result for rand.
I believe this error is because you are trying to use rand() on objects of your custom class.
`rand': no implicit conversion of Range into Integer (TypeError)
This error message clearly mentions that ruby was unable to convert your range into integer. Based on your code snippet, following works and might be what you are looking for.
puts rand(Int.new(1).to_int..Int.new(3).to_int)

Float not equal after division followed by multiplication

I'm testing a small and simple library I made in Ruby. The goal is to convert from EUR to CNY and vice versa. Simple.
I tested it to be sure everything works but I got an unexpected issue. When I use to_euro followed by to_yuan it should go back to the original amount ; it doesn't happen. I tried to .to_f or round(2) the amount variable which fix some tests, raise new ones, but it's never equal to what I expect globally ; I'm running out of idea to fix this :(
class Currency
attr_reader :amount, :currency
def initialize(amount, currency='EUR')
#amount = amount
#currency = currency
end
def to_yuan
update_currency!('CNY', amount * Settings.instance.exchange_rate_to_yuan)
end
def to_euro
update_currency!('EUR', amount / Settings.instance.exchange_rate_to_yuan)
end
def display
"%.2f #{current_symbol}" % amount
end
private
def current_symbol
if currency == 'EUR'
symbol = Settings.instance.supplier_currency.symbol
elsif currency == 'CNY'
symbol = Settings.instance.platform_currency.symbol
end
end
def update_currency!(new_currency, new_amount)
unless new_currency == currency
#currency = new_currency
#amount = new_amount
end
self
end
end
Tests
describe Currency do
let(:rate) { Settings.instance.exchange_rate_to_yuan.to_f }
context "#to_yuan" do
it "should return Currency object" do
expect(Currency.new(20).to_yuan).to be_a(Currency)
end
it "should convert to yuan" do
expect(Currency.new(20).to_yuan.amount).to eql(20.00 * rate)
end
it "should convert to euro and back to yuan" do
# state data test
currency = Currency.new(150, 'CNY')
expect(currency.to_euro).to be_a(Currency)
expect(currency.to_yuan).to be_a(Currency)
expect(currency.amount).to eql(150.00)
end
end
context "#to_euro" do
it "should convert to euro" do
expect(Currency.new(150, 'CNY').to_euro.amount).to eql(150 / rate)
end
end
context "#display" do
it "should display euros" do
expect(Currency.new(10, 'EUR').display).to eql("10.00 €")
end
it "should display yuan" do
expect(Currency.new(60.50, 'CNY').display).to eql("60.50 ¥")
end
end
end
And here's my RSpec result
I'm pretty sure this problem is very common, any idea how to solve it easily ?
Float isn't an exact number representation, as stated in the ruby docs:
Float objects represent inexact real numbers using the native architecture's double-precision floating point representation.
This not ruby fault, as floats can only be represented by a fixed number of bytes and therefor cannot store decimal numbers correctly.
Alternatively, you can use ruby Rational or BigDecimal
Its is also fairly common to use the money gem when dealing with currency and money conversion.

Find percentage of two small numbers in ruby

I have two small numbers that I'd like to find the percentage of.
First number: 0.683789473684211
Second number: 0.678958333333333
I want to find out what percentage of the number is bigger or smaller. These happen to be small numbers, but they could be bigger. The first number COULD be 250, and the second number could be 0.3443435. What I'm TRYING to do is detect whether the first number is 25% bigger than the second number.
I tried using this:
class Numeric
def percent_of(n)
self.to_f / n.to_f * 100.0
end
end
But it kept saying I was dividing by zero
How would you do it?
Why not shoot straight for what you say you want to do?
class Numeric
def sufficiently_bigger?(n, proportion = 1.25)
self >= proportion * n
end
end
p 5.sufficiently_bigger? 4 # => true
p 5.sufficiently_bigger? 4.00001 # => false
This will default to a 25% larger check, but you can override the proportionality by supplying a different value as the second argument.
It's generally easier and avoids the need for an explicit zero-denominator check if you express ratios in product form rather than using division.
The basic implementation of your code looks correct to me. Can you provide the specific example and expected output that is producing that error?
Just because I was curious I took your code and executed it with a small test suite and had 3 passing tests.
require 'rubygems'
require 'test/unit'
class Numeric
def percent_of(n)
self.to_f / n.to_f * 100.00
end
end
class PercentageTeset < Test::Unit::TestCase
def test_25_is_50_percent_of_50
assert_equal (25.percent_of(50)), 50.0
end
def test_50_is_100_percent_of_50
assert_equal (50.percent_of(50)), 100.0
end
def test_75_is_150_percent_of_50
assert_equal (75.percent_of(50)), 150.0
end
end
class Numeric
def percent_of(n)
self.to_f / n.to_f * 100.0
end
end
p 0.683789473684211.percent_of(0.678958333333333)
--output:--
100.71155181602376
p 250.percent_of(0.3443435)
--output:--
72601.9222084924
p 0.000_001.percent_of(0.000_000_5)
--output:--
200.0
p 0.000_000_000_01.percent_of(0.000_000_000_01)
--output:--
100.0
class Numeric
def percent_of(n)
self.to_f / n.to_f * 100.0
end
end
numbers = [ 0.683789473684211, 0.678958333333333 ]
min_max = {min: numbers.min, max: numbers.max}
puts "%<min>f is #{min_max[:min].percent_of(min_max[:max])} of %<max>f" % min_max
This program has opinions in that it shows what percentage the minimal number is of the maximal number, and shows the numbers.
If you use %d for the String#format method, you will show 0's. Perhaps that was what you were referring to, not sure.
Edit: Using minmax as suggested.
class Numeric
def percent_of(n)
self.to_f / n.to_f * 100.0
end
end
numbers = [ 0.683789473684211, 0.678958333333333 ]
min_max = Hash.new
min_max[:min], min_max[:max] = numbers.minmax
puts "%<min>f is #{min_max[:min].percent_of(min_max[:max])} of %<max>f" % min_max
I like the first version as the hash is built as it is needed, rather than initalized and then built.

How do I round a float to a specified number of significant digits in Ruby?

It would be nice to have an equivalent of R's signif function in Ruby.
For example:
>> (11.11).signif(1)
10
>> (22.22).signif(2)
22
>> (3.333).signif(2)
3.3
>> (4.4).signif(3)
4.4 # It's usually 4.40 but that's OK. R does not print the trailing 0's
# because it returns the float data type. For Ruby we want the same.
>> (5.55).signif(2)
5.6
There is probably better way, but this seems to work fine:
class Float
def signif(signs)
Float("%.#{signs}g" % self)
end
end
(1.123).signif(2) # => 1.1
(11.23).signif(2) # => 11.0
(11.23).signif(1) # => 10.0
Here's an implementation that doesn't use strings or other libraries.
class Float
def signif(digits)
return 0 if self.zero?
self.round(-(Math.log10(self).ceil - digits))
end
end
I don't see anything like that in Float. Float is mostly a wrapper for the native double type and given the usual binary/decimal issues, I'm not that surprised that Float doesn't allow you to manipulate the significant digits.
However, BigDecimal in the standard library does understand significant digits but again, I don't see anything that allows you to directly alter the significant digits in a BigDecimal: you can ask for it but you can't change it. But, you can kludge around that by using a no-op version of the mult or add methods:
require 'bigdecimal'
a = BigDecimal.new('11.2384')
a.mult(1, 2) # the result is 0.11E2 (i.e. 11)
a.add(0, 4) # the result is 0.1124E2 (i.e. 11.24)
The second argument to these methods:
If specified and less than the number of significant digits of the result, the result is rounded to that number of digits, according to BigDecimal.mode.
Using BigDecimal will be slower but it might be your only choice if you need fine grained control or if you need to avoid the usual floating point problems.
Some of the previous answers and comments have alluded to this solution but this is what worked for me:
# takes in a float value and returns another float value rounded to
# given significant figures.
def round_to_sig_figs(val, sig_figs)
BigDecimal.new(val, sig_figs).to_f
end
You are probably looking for Ruby's Decimal.
You could then write:
require 'decimal/shortcut'
num = 1.23541764
D.context.precision = 2
num_with_2_significant_digits = +D(num.to_s) # => Decimal('1.2')
num_with_2_significant_digits.to_f # => 1.2000000000000002
Or if you prefer to use the same syntax add this as a function to class Float like this:
class Float
def signif num_digits
require 'decimal/shortcut'
D.context.precision = num_digits
(+D(self.to_s)).to_f
end
end
Usage would then be the same, i.e.
(1.23333).signif 3
# => 1.23
To use it, install the gem
gem install ruby-decimal
#Blou91's answer is nearly there, but it returns a string, instead of a float. This below works for me:
(sprintf "%.2f", 1.23456).to_f
So as a function,
def round(val, sig_figs)
(sprintf "%.#{sig_figs}f", val).to_f
end
Use sprintf if you want to print trailing zeros
2.0.0-p353 :001 > sprintf "%.3f", 500
=> "500.000"
2.0.0-p353 :002 > sprintf "%.4f", 500
=> "500.0000"
2.0.0-p353 :003 >

Why does Ruby give this large precision decimal result?

I want to convert a subtitle time code:
begin="00:00:07.71" dur="00:00:03.67
to pure seconds:
begin=7.1 end=11.38
I wrote a Ruby code:
def to_sec(value)
a = value.split(':')
a[0].to_i*3600+a[1].to_i*60+a[2].to_f
end
which resulted in 11.379999999999999.
Can anybody tell me why this happens?
Is there any Time library that can do this conversion?
It'll probably be easiest for you to represent your underlying datatype as integer hundredths of a second (centiseconds):
def to_csec(value) #if you had CSec < Integer this would be `def self.[](value)`
a = value.split(':')
#tacking on a couple zeros to each
a[0].to_i*360000+a[1].to_i*6000+(a[2].to_f * 100).to_i
end
You could add some helpers for dealing with the durations and pretty printing them as well:
def csec_to_s(csec) #if you had CSec < Integer, this would be `def to_sec`
"%.2f" % (csec.to_f / 100)
end
class SubtitleDuration < Range
def initialize(a,b)
centi_a = to_csec(a)
super(centi_a,to_csec(b) + centi_a)
end
def to_s
"begin=#{csec_to_s(self.begin)} end=#{csec_to_s(self.end) }"
end
end
Then your answer is just:
puts SubtitleDuration.new("00:00:07.71", "00:00:03.67").to_s
#=> begin=7.71 end=11.38
This sort of thing can happen in just about any programming language. It's because of how floating point numbers are represented. They're not stored as decimals under the hood, so sometimes you get odd rounding errors like this.

Resources