Rails портит BigDecimals, добавляя логику с плавающей запятой

Я немного поигрался с рельсами и нашел кое-что странное. Для хранения денежного значения я использую типичный десятичный тип данных, который активная запись преобразует в BigDecimal. Я посчитал это точным и решил избежать странного поведения математики с плавающей запятой. Но когда я сохраняю 99,99 в БД, все работает нормально, но когда записи загружаются активной записью, она теряет точность и преобразуется во что-то вроде 99,9899999999. Это похоже на проблему с плавающей запятой.

Я сделал несколько тестов и обнаружил, что создание BigDecimal, подобное этому, b = BigDecimal.new("99.99") приводит к "чистой" переменной, но создание ее таким образом b = BigDecimal.new(99.99) приводит к "нечистой" версии которого я хочу избежать.

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

Версия Ruby 1.9.3p0 Rails 3.2.9 Sqlite 3.7.9


person user331471    schedule 11.12.2012    source источник
comment
(отказ от ответственности: я не работал с Active Record). Вместо того, чтобы сообщать активной записи о сохранении объекта, просто дайте ей строку со всей точностью, которую вы хотите, и восстановите объект на другом конце. Не так сложно.   -  person Linuxios    schedule 12.12.2012
comment
Используйте деньги для обработки денег. Он работает с целыми числами и упрощает работу с деньгами. Как указывали другие, использование десятичных типов - очень и очень плохая идея.   -  person Chris Heald    schedule 12.12.2012


Ответы (2)


Ваша проблема в том, что вы используете SQLite, а SQLite не имеет встроенной поддержки типов данных numeric(m,n). Из отличного руководства:

1.0 Классы хранения и типы данных

Каждое значение, хранящееся в базе данных SQLite (или управляемое механизмом базы данных), имеет один из следующих классов хранения:

  • NULL. Значение равно NULL.
  • INTEGER. Значение представляет собой целое число со знаком, хранящееся в 1, 2, 3, 4, 6 или 8 байтах в зависимости от величины значения.
  • REAL. Значение представляет собой значение с плавающей запятой, сохраненное как 8-байтовое число с плавающей запятой IEEE.
  • ТЕКСТ. Значение представляет собой текстовую строку, сохраненную с использованием кодировки базы данных (UTF-8, UTF-16BE или UTF-16LE).
  • BLOB. Значение представляет собой большой двоичный объект данных, сохраненный точно так же, как он был введен.

Читайте дальше на этой странице, чтобы увидеть, как работает система типов SQLite.

Ваше 99,99 может быть BigDecimal.new('99.99') в вашем коде Ruby, но это почти наверняка РЕАЛЬНОЕ значение 99.99 (т. е. восьмибайтовое значение с плавающей запятой IEEE) внутри SQLite, и это соседство.

Так что переключитесь на лучшую базу данных в вашей среде разработки; в частности, разрабатывайте поверх любой базы данных, которую вы собираетесь развертывать.

person mu is too short    schedule 12.12.2012
comment
дополнительный +1 (если бы я мог) за разработку с той же базой данных - person Mauricio Pasquier Juan; 29.05.2014
comment
@MauricioPasquierJuan: я все еще поражен тем, что люди купились на фантазию о том, что ActiveRecord обеспечивает переносимость базы данных. - person mu is too short; 29.05.2014

Не используйте плавающую точку для денежных значений

Да, именно так, SQLite искажает ваши значения BigDecimal.

Основная проблема заключается в том, что формат FP не может правильно хранить большинство десятичных дробей.

Я полагаю, что у вас есть около четырех вариантов:

  1. Округлите все, скажем, до двух знаков после запятой, чтобы вы никогда не заметили слегка отличающиеся значения.
  2. Сохраните значения BigDecimal в SQLite с классом хранения TEXT или BLOB.
  3. Используйте другую базу данных, которая поддерживает какую-то десятичную строку.
  4. Масштабируйте все до целых значений и используйте класс хранения INTEGER.

Проблема в том, что дроби FP — это рациональные числа вида x/2n. Но десятичные денежные суммы имеют дроби, равные x/(2n * 5m). Представления просто несовместимы. Например, в 0,01...0,99 только 0,25, 0,50 и 0,75 имеют точное двоичное представление.

person DigitalRoss    schedule 12.12.2012
comment
+1. Что произойдет, если вы попытаетесь заказать 100 товаров стоимостью 129,95 долларов США и используете десятичные дроби? > 129.95 * 100 => 12994.999999999998 - person Chris Heald; 12.12.2012