开发者

Ruby: Why is 1.025.round(2) rounded to 1.02?

As far as I understand the .round()-functionality in ruby rounds decimals upwards where the last significant number is 5?

For example 1.5.round(0) # => 2 (OK)

but why does 1.025.round(2) # => 1.02, and not 1.03 as I would expect?

irb(main):037:0> 1.025.round(2)
=> 1.02

Wha开发者_如何转开发t can I do to go around this?


This has nothing to do with the last digit being 5 and everything to do with conversion of a decimal value to a double precision floating point value.

http://en.wikipedia.org/wiki/Double_precision_floating-point_format

Basically, the decimal number has to be represented in limited binary format which can only approximate certain decimal values, leading to loss of precision. This can cause some weird behavior, as you have seen.

Best to explain this by showing you... Marshal.dump(1.025) dumps the Float value and shows the value a bit closer to what it really is: 1.0249999999999999. 1.025.to_r will provide you with the fraction which represents the binary value. You can use the arbitrarily precise decimal library, BigDecimal to convert this:

ruby-1.9.2-p180 :060 > (BigDecimal.new("2308094809027379.0") / BigDecimal.new("2251799813685248.0")).to_s('F')
=> "1.024999999999999911182158029987476766"

When certain decimals are converted to this "approximate" binary number format they will be represented differently, possibly more precisely. So, you might have noticed that 1.085.round(2) results in 1.09 as you'd expect.

This lack of precision with floating point math means it's never, ever appropiate to use floating point values for currency calculations, or even as temporary containers for money values. Arbitrary precision data types should be used at all times for anything involving money.

As a former developer for an extremely large financial company I was constantly shocked by how rarely this advice is heeded and how common the use of floats or doubles is in financial software. Most programmers in that industry I have talked to are not aware that floats and doubles should never store money values. So, don't feel like you are too behind the curve ;-)

tl;dr

Use BigDecimal: BigDecimal.new("1.025").round(2) => "1.03"


I think this is due to the nature of floating point numbers. They're not exact representations of a number:

printf("%f", 1.025)     # rounded to 6 decimal places
=> 1.025000

printf("%.16f", 1.025)  # rounded to 16 decimal places
=> 1.0249999999999999

So when you enter "1.025" it's represented in the computer as a number that is fractionally less than the value you really wanted. Most of the time it's not a problem but it can throw up the occasional bit of strangeness.

Just to be clear: this isn't a problem with Ruby, it's a problem with floating point numbers in all languages. If it's causing you trouble have a look at BigDecimal.


Using Pry you can look at the underlying code for Float#round

So in Pry type:

show-method Float#round

which show's the underlying C code:

From: numeric.c in Ruby Core (C Method): Number of lines: 36

static VALUE
flo_round(int argc, VALUE *argv, VALUE num)
{
    VALUE nd;
    double number, f;
    int ndigits = 0, i;
    long val;

    if (argc > 0 && rb_scan_args(argc, argv, "01", &nd) == 1) {
    ndigits = NUM2INT(nd);
    }
    number  = RFLOAT_VALUE(num);
    f = 1.0;
    i = abs(ndigits);
    while  (--i >= 0)
    f = f*10.0;

    if (isinf(f)) {
    if (ndigits < 0) number = 0;
    }
    else {
    if (ndigits < 0) number /= f;
    else number *= f;
    number = round(number);
    if (ndigits < 0) number *= f;
    else number /= f;
    }

    if (ndigits > 0) return DBL2NUM(number);

    if (!FIXABLE(number)) {
    return rb_dbl2big(number);
    }
    val = (long)number;
    return LONG2FIX(val);
}

Which show's it's using the C round function. Which is complying to IEEE-754.

Unless you have a very strange edge-case I would recommend that you keep with this type of rounding.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜