Рубин: Почему 1,025.round (2) округляется до 1,02? - PullRequest
11 голосов
/ 29 сентября 2011

Насколько я понимаю, .round() -функция в десятичных числах рубиновых округлений вверх, где последнее значащее число равно 5?

Например 1.5.round(0) # => 2 (ОК)

но почему 1.025.round(2) # => 1.02, а не 1.03, как я ожидал?

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

Что я могу сделать, чтобы обойти это?

Ответы [ 3 ]

21 голосов
/ 29 сентября 2011

Это не имеет ничего общего с последней цифрой 5, а также с преобразованием десятичного значения в значение с плавающей запятой двойной точности.

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

По сути, десятичное число должно быть представлено в ограниченном двоичном формате, который может приближаться только к определенным десятичным значениям, что приводит к потере точности. Это может вызвать странное поведение, как вы видели.

Лучше всего объяснить это, показывая вам ... Marshal.dump(1.025) сбрасывает значение Float и показывает значение немного ближе к тому, что оно есть на самом деле: 1.0249999999999999 . 1.025.to_r предоставит вам дробь, которая представляет двоичное значение. Вы можете использовать произвольно точную десятичную библиотеку BigDecimal, чтобы преобразовать это:

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

Когда определенные десятичные дроби преобразуются в этот «приблизительный» формат двоичных чисел, они будут представлены по-разному, возможно, более точно. Итак, вы могли заметить, что 1.085.round(2) приводит к 1.09 , как и следовало ожидать.

Это отсутствие точности в математике с плавающей запятой означает, что никогда не будет целесообразно использовать значения с плавающей запятой для расчетов в валюте или даже в качестве временных контейнеров для денежных значений. Для данных, связанных с деньгами, следует всегда использовать типы данных произвольной точности.

Как бывший разработчик чрезвычайно крупной финансовой компании, я постоянно был шокирован тем, как редко этот совет учитывается и как часто используется плавающее или двойное число в финансовом программном обеспечении. Большинство программистов в этой отрасли, с которыми я говорил, не знают, что числа с плавающей запятой и двойники никогда не должны хранить денежные ценности. Так что не думайте, что вы слишком за кривой; -)

ТЛ; др

Использовать BigDecimal: BigDecimal.new("1.025").round(2) => "1.03"

4 голосов
/ 29 сентября 2011

Я думаю, это связано с природой чисел с плавающей запятой. Они не являются точными представлениями числа:

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

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

Поэтому, когда вы вводите «1.025», оно представляется на компьютере как число, которое немного меньше значения, которое вы действительно хотели. В большинстве случаев это не проблема, но это может вызвать некоторую странность.

Просто чтобы прояснить: это не проблема с Ruby, это проблема с числами с плавающей запятой на всех языках. Если это вызывает у вас проблемы, взгляните на BigDecimal.

2 голосов
/ 29 сентября 2011

Используя Pry , вы можете посмотреть на базовый код для Float # round

Итак, в типе Pry:

show-method Float#round

, который показывает, что это базовый код C:

From: numeric.c в Ruby Core (метод C): Количество строк: 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);
}

Что показывает, что он использует функцию C round.Что соответствует IEEE-754 .

Если у вас не очень странный край, я бы порекомендовал вам использовать этот тип округления.

...