Почему BigDecimal возвращает странное значение?

Я пишу код, который будет иметь дело с валютами, сборами и т. д. Я собираюсь использовать класс BigDecimal для математики и хранения, но мы столкнулись с чем-то странным.

такое заявление:

1876.8 == BigDecimal('1876.8')

возвращает false.

если я запускаю эти значения через строку форматирования "%.13f" Я:

"%.20f" % 1876.8 => 1876.8000000000000
"%.20f" % BigDecimal('1876.8') => 1876.8000000000002

отметить лишнее 2 из BigDecimal в последней десятичной дроби.

Я думал, что BigDecimal должен был противостоять неточности хранения вещественных чисел непосредственно в собственной плавающей точке компьютера. Где это 2 идет?

6 ответов


вы правы, BigDecimal должен хранить его правильно, мое лучшее предположение:

  • BigDecimal сохраняет значение правильно
  • при передаче функции форматирования строки BigDecimal отбрасывается как значение с плавающей запятой более низкой точности, создавая ...02.
  • при сравнении непосредственно с поплавком поплавок имеет дополнительное десятичное место далеко за пределами 20, которые вы видите (классические поплавки не могут быть сравнены behavoir).

либо таким образом, вы вряд ли получите точные результаты, сравнивая float с BigDecimal.


это не даст вам столько контроля над количеством десятичных знаков, но обычный механизм формата для BigDecimal выглядит так:

a.to_s('F')

Если вам нужен больший контроль, рассмотрите возможность использования Money gem, предполагая, что ваша проблема домена в основном связана с валютой.

gem install money

Не сравнивайте FPU десятичные строковые дроби для равенства

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

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

чтобы ответить на ваш точный вопрос,2 исходит из немного другого преобразование десятичной Строковой дроби в . Поскольку дробь не может быть представлена точно, возможно, что два вычисления будут учитывать разные суммы точности в промежуточных вычислениях и в конечном итоге округляют результат до 52-разрядной мантиссы двойной точности IEEE 754 по-разному. Это вряд ли имеет значение, потому что там is нет точного представления в любом случае, но один, вероятно, более ошибочен, чем другой.

в частности, ваш 1876.8 не может быть точно представлен объектом FP, на самом деле, между 0.01 и 0.99, только 0.25, 0.50 и 0.75 имеют точные двоичные представления. Все остальные, включая 1876.8, повторяются навсегда и округляются до 52 бит. Это примерно половина причины, по которой BigDecimal даже существует. (Другая половина причины-фиксированная точность данных FP: иногда вам нужно больше.)

Итак, результат, который вы получаете при сравнении фактического значения машины с десятичной Строковой константой зависит от каждого бита в двоичной дробью ... до 1/252 ... и даже тогда, требует округления.

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

можно даже сделать аргумент, что сравнение должны всегда терпеть неудачу, потому что никакой формат FPU IEEE не может даже представить это число точно. Они действительно не равны, хотя и выглядят так. Слева ваша десятичная строка была преобразована в двоичную строку, и большинство чисел просто не преобразуются точно. Справа все еще десятичная строка.

поэтому не смешивайте поплавки с BigDecimal, просто сравните один BigDecimal с другим BigDecimal. (Даже когда оба операнда are поплавки, проверки на равенство требует большой осторожности или нечеткого теста. Кроме того, не доверяйте каждая отформатированная цифра: форматирование вывода будет нести остатки с правой стороны фракции, поэтому вы обычно не начинаете видеть нули, вы просто увидите значения мусора.)


*проблема: номера машины x / 2n, но десятичные константы x / (2n * 5m). Ваше значение как знака, экспоненты и мантиссы-бесконечно повторяющееся 0 10000001001 1101010100110011001100110011001100110011001100110011... По иронии судьбы, арифметика FP совершенно точна и сравнение равенства отлично работает, когда значение не имеет дроби.


Как сказал Дэвид, BigDecimal хранит его правильно

 p (BigDecimal('1876.8') * 100000000000000).to_i

возвращает 187680000000000000

Итак, да, форматирование строки разрушает его


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


на Mac OS X я запускаю ruby 1.8.7 (2008-08-11 patchlevel 72) [i686-darwin9]

irb(main):004:0> 1876.8 == BigDecimal('1876.8') => true

однако, будучи Ruby, я думаю, вы должны думать с точки зрения сообщений, отправленных объектам. Что это возвращает вам:

BigDecimal('1876.8') == 1876.8

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

по той же причине я не думаю, что форматирование BigDecimal путем отправки формата сообщение в строку формата также является правильным подходом.