Ruby: Почему 1.025.round(2) округляется до 1.02?

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

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

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

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

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

Ответ 1

Это не имеет ничего общего с последней цифрой 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, как и следовало ожидать.

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

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

TL;DR

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

Ответ 2

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

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.

Ответ 3

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

Итак, в режиме Pry:

show-method Float#round

которые показывают базовый код C:

От: 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-раунда. Что соответствует IEEE-754.

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