Является ли использование целых чисел в качестве дробных коэффициентов вместо числа с плавающей запятой хорошей идеей для денежного приложения? - PullRequest
0 голосов
/ 07 января 2019

Мое приложение требует дробного количества, умноженного на денежное значение.

Например, $65.50 × 0.55 hours = $36.025 (округлено до $36.03).

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

Что касается дробного коэффициента, моя проблема в том, что 0.55 не имеет 32-битного представления с плавающей запятой. В приведенном выше примере использования 0.55 hours == 33 minutes, поэтому 0.55 является примером определенного значения, которое мое приложение должно будет точно учитывать. Представление с плавающей точкой 0.550000012 недостаточно, потому что пользователь не поймет, откуда взялся дополнительный 0.000000012. Я не могу просто вызвать функцию округления на 0.550000012, потому что она округляется до целого числа.

Решение для умножения

Чтобы решить эту проблему, моя первая идея состояла в том, чтобы сохранить все величины в виде целых чисел и умножить на 1000. Таким образом, 0.55, введенное пользователем, станет 550 (целое число) при сохранении. Все вычисления будут выполняться без значений с плавающей точкой, а затем просто делятся на 1000 (целочисленное деление, а не число с плавающей точкой) при представлении результата пользователю.

  • Я понимаю, что это навсегда ограничит меня до 3 десятичных знаков точность. Если я решу, что 3 достаточно для жизни моего приложение, имеет ли этот подход смысл?

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

  • Есть ли название для этого процесса? РЕДАКТИРОВАТЬ: Как указано @SergGr, это арифметики с фиксированной точкой .

  • Есть ли лучший подход?

EDIT:

Я должен был уточнить, это не зависит от времени. Это для общих величин, таких как 1.256 pounds of flour, 1 sofa или 0.25 hours (например, счета).

Здесь я пытаюсь воспроизвести более точную версию функциональности Postgres extra_float_digits = 0, где, если пользователь вводит 0.55 (float32), база данных хранит 0.550000012, но при запросе результата возвращает 0.55 который выглядит точно так, как набрал пользователь.

Я хочу ограничить точность этого приложения до 3 десятичных знаков (это бизнес, а не наука), так что именно это заставило меня рассмотреть подход × 1000.

Я использую язык программирования Go, но меня интересуют универсальные межъязыковые решения.

Ответы [ 3 ]

0 голосов
/ 07 января 2019

Другое решение для сохранения результата - использование рациональной формы значения. Вы можете объяснить число двумя целочисленными значениями, число которых равно p/q, так что оба значения p и q являются целыми числами. Следовательно, вы можете иметь больше точности для своих чисел и сделать некоторую математику с рациональными числами в формате двух целых чисел.

0 голосов
/ 09 января 2019

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

TL; DR

  1. Да, такой подход имеет смысл, но, скорее всего, это не лучший выбор
  2. Да, есть проблемы с округлением, но неизбежно будут некоторые, независимо от того, какое представление вы используете
  3. То, что вы предлагаете использовать, называется Десятичное число фиксированное значение число точек
  4. Я бы поспорил, да, есть лучший подход, и это использовать некоторые стандартные или популярные десятичные плавающие точечные числа библиотека для вашего языка ( Го не мой родной язык, поэтому я не могу рекомендовать его)

В PostgreSQL лучше использовать Numeric (например, Numeric(15,3)), а не комбинацию float4 / float8 и extra_float_digits. На самом деле это то, что первый пункт в PostgreSQL документации по типам с плавающей точкой предлагает:

  • Если вам требуется точное хранение и расчеты (например, для денежных сумм), используйте вместо этого тип numeric.

Еще несколько подробностей о том, как можно хранить нецелые числа

Прежде всего, существует фундаментальный факт, что в диапазоне [0;1] бесконечно много чисел, поэтому вы, очевидно, не можете хранить каждое число там в любой конечной структуре данных. Это означает, что вы должны пойти на некоторые компромиссы: независимо от того, какой путь вы выберете, будут некоторые цифры, которые вы не сможете сохранить точно, поэтому вам придется округлять.

Другим важным моментом является то, что люди привыкли к системе, основанной на 10, и в этой системе только результаты деления на числа в форме 2^a*5^b могут быть представлены с помощью конечного числа цифр. Для любого другого рационального числа, даже если вы каким-то образом сохраните его в точной форме, вам придется выполнить некоторое усечение и округление на этапе форматирования для использования человеком.

Потенциально существует бесконечно много способов хранения чисел. На практике широко используются лишь немногие:

  • числа с плавающей запятой с двумя основными ветвями двоичного кода (это то, что большинство современных аппаратных средств изначально реализует и поддерживает большинство языков, например float или double) и десятичного . Это формат, в котором хранятся мантисса и показатель степени (может быть отрицательным), поэтому число равно mantissa * base^exponent (я опускаю знак и просто говорю, что это логически часть мантиссы, хотя на практике она обычно хранится отдельно). Двоичные и десятичные значения определяются base. Например, 0.5 будет храниться в двоичном виде в виде пары (1,-1), т.е. 1*2^-1, и в десятичном виде в виде пары (5,-1), т. Е. 5*10^-1. Теоретически вы также можете использовать любую другую базу, но на практике только 2 и 10 имеют смысл в качестве баз.

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

  • формат рациональных чисел, т. Е. Значение сохраняется в виде пары (p, q), которая представляет p/q (и обычно q>0, поэтому знак хранится в p и либо p=0, q=1 для 0, либо gcd(p,q) = 1 для каждого другого номера). Обычно для этого требуется какая-то большая целочисленная арифметика, чтобы быть полезной в первую очередь (здесь пример Go math.big.Rat ). На самом деле это может быть полезным форматом для некоторых проблем, и люди часто забывают об этой возможности, возможно потому, что она часто не является частью стандартной библиотеки. Другой очевидный недостаток заключается в том, что, как я уже говорил, люди не привыкли мыслить рациональными числами (вы можете легко сравнить, что больше 123/456 или 213/789?), Поэтому вам придется преобразовывать конечные результаты в какую-то другую форму. Другим недостатком является то, что если у вас длинная цепочка вычислений, внутренние числа (p и q) могут легко стать очень большими значениями, поэтому вычисления будут медленными. Тем не менее, может быть полезно хранить промежуточные результаты расчетов.

В практическом плане существует также разделение на представления произвольной длины и фиксированной длины. Например:

  • IEEE 754 float или double - двоичные представления с плавающей точкой фиксированной длины,

  • Go math.big.Float - двоичные представления произвольной длины с плавающей точкой

  • .Net decimal - десятичное представление с плавающей запятой фиксированной длины

  • Java BigDecimal - десятичное представление с плавающей точкой произвольной длины

В практическом плане я бы сказал, что лучшее решение для вашей проблемы - это достаточно большие десятичные представления с плавающей точкой достаточно большой длины (например, .Net decimal). Реализация произвольной длины также будет работать. Если вам нужно сделать реализацию с нуля, тогда ваша идея десятичного представления с фиксированной длиной может быть приемлемой, потому что это проще всего реализовать самостоятельно (немного проще, чем в предыдущих альтернативах), но в некоторых случаях это может стать бременем. точка.

0 голосов
/ 07 января 2019

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

Попробуйте использовать Двоичный код с двоичным кодом для хранения ваших значений. Это работает, ограничивая значения, которые могут быть сохранены в байтах от 0 до 9 (включительно), «тратя впустую» остальное. Таким образом, вы можете закодировать десятичное представление числа байтов за байтом. Например, 613 станет

6 -> 0000 0110
1 -> 0000 0001
3 -> 0000 0011

613 -> 0000 0110 0000 0001 0000 0011

Где каждая группа из 4 цифр выше - это «кусочек» байта. На практике используется вариант упакованный , где две десятичные цифры упакованы в байт (по одному на кусочек), чтобы быть менее «расточительными». Затем вы можете реализовать несколько методов для сложения, вычитания, умножения и т. Д. Просто переберите массив байтов и выполните алгоритмы сложения / умножения в классической начальной школе (помните о упакованном варианте, который вам может понадобиться). добавьте ноль, чтобы получить четное количество кусков). Вам просто нужно сохранить переменную для хранения там, где находится десятичная точка, и не забывать переносить, где это необходимо, для сохранения кодировки.

...