Ruby Float#round method behaves incorrectly with round(2) - ruby

I learned that it's recommended to use BigDecimal instead of Float, but this one is either a bug or highlights the esoteric nature of Float. It seems that Float#round(2) has a problem with "1.015", "1.025" and "1.035".
1.015.round(2)
=> 1.01 # => WRONG .. should be 1.02
1.025.round(2)
=> 1.02 # => WRONG .. should be 1.03
1.035.round(2)
=> 1.03 # => WRONG .. should be 1.04
1.045.round(2)
=> 1.05 # => CORRECT
1.016.round(2)
=> 1.02 # => CORRECT
round(3) works fine.
1.0015.round(3)
=> 1.002 # => CORRECT
1.235.round(2)
=> 1.24 # => CORRECT
To monkey patch this in a Rails app, I did this:
config/initializers/float_mp.rb
require 'bigdecimal'
class Float
def round(val=0)
BigDecimal.new(self.to_s).round(val).to_f
end
end
This seems to be a weird and expensive work-around. Could this be a bug in Float#round?

AFAICS the ruby round() works correctly. Presumably it's just a wrapper around the round() function in libm anyway.
So the reason is that your floating point literals cannot be represented exactly in binary. E.g. "1.015" printed with a few more decimals gives "1.0149999999999999"; thus when rounding to two decimal digits, 1.01 is closer to the true value than 1.02. And so on for your other examples as well.
Also keep in mind that the default IEEE 754 rounding mode is "Round to nearest, ties to even" which is not the same as "Round to nearest, ties away from zero" which is what you may be familiar with from school.

Related

Ruby floor gives incorrect decimal points [duplicate]

This question already has answers here:
Is floating point math broken?
(31 answers)
Closed last year.
Running into some weird outputs using floor with params for decimal places.
100.1111.floor(1)
=> 100.1
100.1111.floor(2)
=> 100.11
100.1111.floor(3)
=> 100.111
100.1111.floor(4)
=> 100.111 # why is this even the correct answer
100.1111.floor(5)
=> 100.1111
Why would the floor(4) only give 3 decimal places back and why would floor(5) give the correct response?
I attempted to see if this was an error so I tried a different float.
100.111111.floor(1)
=> 100.1
100.111111.floor(2)
=> 100.11
100.111111.floor(3)
=> 100.111
100.111111.floor(4)
=> 100.1111
100.111111.floor(5)
=> 100.11111
100.111111.floor(6)
=> 100.111111
But it seems that this isnt the case because floor works correctly for other float numbers. I tried with 100.111 and other floats and they all seem to work. Only 100.1111 seems to be giving the issue. Is this a problem with the implementation of Float#floor?
Ruby version is ruby 3.0.3p157 if that helps.
Float point numbers can not represent every number/precision, I suggest you to try the most famous example 0.1 + 0.2 that fails to represent 0.3 in most languages.
If you really care about precision in ruby use BigDecimal
require 'bigdecimal/util'
puts BigDecimal('100.1111').floor(1).to_digits
puts BigDecimal('100.1111').floor(2).to_digits
puts BigDecimal('100.1111').floor(3).to_digits
puts BigDecimal('100.1111').floor(4).to_digits
puts BigDecimal('100.1111').floor(5).to_digits
puts BigDecimal('100.1111').floor(6).to_digits
https://ruby-doc.org/stdlib-3.0.3/libdoc/bigdecimal/rdoc/BigDecimal.html

Convert String to Float::INFINITY

In Ruby Float::INFINITY.to_s results in "Infinity" and "Infinity".to_f results in 0.0:
irb(main):001:0> Float::INFINITY.to_s
=> "Infinity"
irb(main):002:0> "Infinity".to_f
=> 0.0
Why is this the case? Why is the symmetry broken? In the programming language for human beings, I would expect the result of the second statement to be Float::INFINITY. Is there any way to convert a string to Float::INFINITY or -Float::INFINITY with Ruby?
Btw. maybe related: The behavior of to_json in Rails is confusing as well. I'd expect it to raise like ActiveModel::Serializer#as_json does.
irb(main):001:0> {a: Float::INFINITY}.to_json
=> "{\"a\":null}"
The way #to_f works is it tries to find a float at the beginning of the string (ignoring whitespaces). It ignores the rest. If it doesn't find anything "floaty-literal-looking", it defaults to 0.0. That's why:
''.to_f # => 0.0
'∞'.to_f # => 0.0
'foo'.to_f # => 0.0
'foo 1.23'.to_f # => 0.0
' 1.23'.to_f # => 1.23
'1.23foo'.to_f # => 1.23
The string 'Infinity' is no different from any other letter only string in that regard. This is kind of unfortunate as it breaks the symmetry. As you pointed out:
Float::INFINITY.to_s.to_f # => 0.0
But at the same time if 'Infinity' did parse to Float::INFINITY this could have lead to some fairly weird and hard to track bugs in everyday code.
Obviously don't do it, but for completeness' sake - to answer the last part:
Is there any way to convert a string to Float::INFINITY or -Float::INFINITY with Ruby?
eval('Float::INFINITY') # => Float::INFINITY
eval('-Float::INFINITY') # => -Float::INFINITY
On the question of why the design decision was made so that infinity breaks the symmetry:
Float::INFINITY.to_s.to_f # => 0.0
The reason is - it's the only way to have consistency. The promise that #to_f gives you is that it will interpret the float literal in (at the start of) the string.
Infinity isn't a literal. If you try to evaluate it, you will get:
NameError: uninitialized constant Infinity
Float::INFINITY isn't a literal either. It's the constant INFINITY nested under Float.
Why does it have to be only literals then? Well... I can make #to_s return anything:
class Float
def to_s
'foo'
end
end
42.0.to_s # => 'foo'
Obviously it's both impossible and unreasonable to expect 'foo'.to_f to return 42.0.
We can make Float::INFINITY.to_s return other stuff. The string 'Infinity' isn't in any way special. It's not a float literal like -1.23 or 9.999999999999995e+39.
You can look at it the opposite way - most floats when #to_s-ed return a string representing their literal form. It's only a happy coincidence that that's also what you need to #to_f them back. Float::INFINITY doesn't return the string version of the literal form, because it has no literal form.
That's all. While I think an awesome opportunity was missed to make ∞ "the infinity literal", they probably saved themselves a lot of headache by not adding a unicode character as a requirement to have the complete language grammar.
Can you think of any programming language that has literal syntax for infinity?
To answer on the second part of your question (the first part is brilliantly answered by #ndnenkov), and leaving aside if it is a good or bad idea (as pointed out again by #ndnenkov), a way to avoid an eval on a string can be something like this:
class String
SPECIAL_FLOATS = {
Float::INFINITY.to_s => Float::INFINITY,
(-Float::INFINITY).to_s => -Float::INFINITY
}
alias_method :super_to_f, :to_f
def to_f
if String::SPECIAL_FLOATS.key? self
return String::SPECIAL_FLOATS[self]
else
return self.super_to_f
end
end
end
"Infinity".to_f
# => Float::INFINITY
Float::INFINITY.to_s.to_f
# => Float::INFINITY
extending the String class in such a way it can handle the conversion of some special Float literals. If it is a good idea or not is a quite complex topic, and in some way it depends on your project and how you are using strings across your code.
Just for comparison, in ruby 2.5.3 on Windows (and not rails, thus require "JSON"):
{a: Float::INFINITY}.to_json
# Traceback (most recent call last):
# 3: from C:/tools/ruby25/bin/irb.cmd:19:in `<main>'
# 2: from (irb):43
# 1: from (irb):43:in `to_json'
# JSON::GeneratorError (862: Infinity not allowed in JSON)
Is there any way to convert a string to Float::INFINITY or -Float::INFINITY with Ruby?
You can use Object#const_get for it:
:001 > Float::INFINITY
=> Infinity
:002 > string = Float::INFINITY.to_s
=> "Infinity"
:003 > Object.const_get("Float::#{string.upcase}")
=> Infinity

Convert 9999999999999999999999.001 to "9999999999999999999999.001" in ruby

How to convert 9999999999999999999999.001 to "9999999999999999999999.001" in ruby
I have tried
>> 9999999999999999999999.001.to_s
=> "1.0e+22"
>> "%f" % 9999999999999999999999.001
=> "10000000000000000000000.000000"
Truth is you can't. The number you write is 22 digits and floats in ruby have only 15 digits precision. So when you use this variable already part of its value is kind of "lost" as it is of the class Float.
Use BigDecimal from the standard library.
1.8.7 :005 > require 'bigdecimal'
=> true
1.8.7 :006 > BigDecimal('9999999999999999999999.001')
=> #<BigDecimal:7fe0cbcead70,'0.9999999999 9999999999 99001E22',36(36)>
1.8.7 :007 > BigDecimal('9999999999999999999999.001').to_s
=> "0.9999999999999999999999001E22"
Of course, this example only shows that BigDecimal can handle numbers that big. Wherever you're initially getting your 9999999999999999999999.001 number from needs to get it into BigDecimal as soon as it's calculated / inputted.
You can not do that. The reason is simple: from the very beginning the value of the number is not going to be exactly 9999999999999999999999.001. Floats will have just 15 digits of precision.
However, you can use other type to achieve what you want:
require 'bigdecimal'
a = BigDecimal("9999999999999999999999.001")
a.to_s("F")
>> "9999999999999999999999.001"
For BigDecimal the precision is extended with the requests of bigger real numbers - no restrictions are applied.
Float is faster in calculations, because its meant to use the FPU of the processor directly, but because of that comes the restriction in the precision.
EDIT Especially for #izomorphius and his argument, just a very short code sample:
a = "34.101"
b = BigDecimal(a.to_s)
c = b ** 15
c.to_s("F")
>>> 98063348952510709441484.183684987951811295085234607613193907150561501
Now tell me how otherwise you get the last string?

Ruby Big Decimal Rounding with fixed number

I want to round a BigDecimal in ruby. I know I can use the round function but
the round function gives
(3.2).round(2) => 3.2
I want
(3.2).round(2) => 3.20
(3.20).round(2) => 3.20
(3).round(2) => 3.00
(3.578).round(2) => 3.58
I always want to have 2 decimal places, 3.20 not 3.2
any idea how to get this done ?
try this:
'%.2f' % 3.2
=> "3.20"
'%.2f' % 3
=> "3.00"
'%.2f' % 3.578
=> "3.58"
etc.
Unlike, for example, Java's BigDecimal, Ruby's BigDecimal does not have a per-instance precision. The difference between 3.2 and 3.20 in a Ruby BigDecimal is just formatting.

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 >

Resources