Точный финансовый расчет в JavaScript. Какие проблемы?

125

В интересах создания кросс-платформенного кода я хотел бы разработать простое финансовое приложение на JavaScript. Необходимые вычисления включают сложные проценты и относительно длинные десятичные числа. Я хотел бы знать, каких ошибок следует избегать при использовании JavaScript для выполнения этого типа математики - если это вообще возможно!

james_womack
источник

Ответы:

107

Вероятно, вам следует масштабировать десятичные значения на 100 и представлять все денежные значения в центах. Это сделано для того, чтобы избежать проблем с логикой с плавающей запятой и арифметикой . В JavaScript нет десятичного типа данных - единственный числовой тип данных - это числа с плавающей запятой. Поэтому обычно рекомендуется обращаться с деньгами как с 2550центами, а не с 25.50долларами.

Учтите, что в JavaScript:

var result = 1.0 + 2.0;     // (result === 3.0) returns true

Но:

var result = 0.1 + 0.2;     // (result === 0.3) returns false

Выражение 0.1 + 0.2 === 0.3возвращается false, но, к счастью, целочисленная арифметика с плавающей запятой точна, поэтому ошибок десятичного представления можно избежать, масштабируя 1 .

Обратите внимание, что хотя набор действительных чисел бесконечен, только конечное их число (18 437 736 874 454 810 627, если быть точным) может быть точно представлено в формате JavaScript с плавающей запятой. Следовательно, представление других чисел будет приближением к действительному числу 2 .


1 Дуглас Крокфорд: JavaScript: Хорошие части : Приложение A - Ужасные части (стр. 105) .
2 Дэвид Флэнаган: JavaScript: Полное руководство, четвертое издание : 3.1.3 Литералы с плавающей запятой (стр. 31) .

Даниэль Вассалло
источник
3
Напоминаем, что всегда округляйте расчеты до цента и делайте это наименее выгодным для потребителя способом. Т.е. Если вы рассчитываете налоги, округляйте в большую сторону. Если вы рассчитываете проценты, усеките.
Джош
6
@Cirrostratus: вы можете проверить stackoverflow.com/questions/744099 . Если вы продолжите использовать метод масштабирования, в общем, вы захотите масштабировать свое значение на количество десятичных цифр, которое вы хотите сохранить точность. Если вам нужно 2 десятичных разряда, масштабируйте на 100, если вам нужно 4, масштабируйте на 10000.
Даниэль Вассалло
2
... Что касается значения 3000,57, да, если вы храните это значение в переменных JavaScript и собираетесь выполнять с ним арифметические операции, вы можете сохранить его масштабированным до 300057 (количество центов). Потому что 3000.57 + 0.11 === 3000.68возвращается false.
Даниэль Вассалло
7
Подсчет грошей вместо долларов не поможет. При подсчете пенни вы теряете возможность прибавлять 1 к целому числу примерно 10 ^ 16. При подсчете долларов вы теряете возможность прибавить 0,01 к числу 10 ^ 14. В любом случае это одно и то же.
slashingweapon
1
Я только что создал модуль npm и Bower, который, надеюсь, поможет с этой задачей!
Pensierinmusica
18

Решение - масштабирование каждого значения на 100. Делать это вручную, вероятно, бесполезно, поскольку вы можете найти библиотеки, которые сделают это за вас. Я рекомендую moneysafe, который предлагает функциональный API, хорошо подходящий для приложений ES6:

const { in$, $ } = require('moneysafe');
console.log(in$($(10.5) + $(.3)); // 10.8

https://github.com/ericelliott/moneysafe

Работает как в Node.js, так и в браузере.

Франсуа Занинотто
источник
2
Upvoted. Пункт «Масштаб на 100» уже включен в принятый ответ, однако хорошо, что вы добавили вариант программного пакета с современным синтаксисом JavaScript. FWIW in$, $ имена значений неоднозначны для тех, кто раньше не использовал пакет. Я знаю, что это был выбор Эрика называть вещи таким образом, но я все же считаю, что это достаточно большая ошибка, что я, вероятно, переименую их в операторе import / destructured require.
james_womack
4
Масштабирование на 100 помогает только до тех пор, пока вы не захотите делать что-то вроде вычисления процентов (по сути, выполнять деление).
Pointy
2
Хотел бы я проголосовать за комментарий несколько раз. Масштабирования на 100 просто недостаточно. Единственный числовой тип данных в JavaScript по-прежнему является типом данных с плавающей запятой, и вы все равно столкнетесь со значительными ошибками округления.
Craig
1
И еще один на один из ридх: Money$afe has not yet been tested in production at scale.. Просто указываю на это, чтобы каждый мог подумать, подходит ли это для его
Нобита
7

Нет такой вещи, как «точный» финансовый расчет, потому что всего две десятичные дроби, но это более общая проблема.

В JavaScript вы можете масштабировать каждое значение на 100 и использовать Math.round() каждый раз, когда может появиться дробь.

Вы можете использовать объект для хранения чисел и включить округление в его valueOf()метод прототипов . Как это:

sys = require('sys');

var Money = function(amount) {
        this.amount = amount;
    }
Money.prototype.valueOf = function() {
    return Math.round(this.amount*100)/100;
}

var m = new Money(50.42355446);
var n = new Money(30.342141);

sys.puts(m.amount + n.amount); //80.76569546
sys.puts(m+n); //80.76

Таким образом, каждый раз, когда вы используете объект Money, он будет округлен до двух знаков после запятой. Неокругленное значение по-прежнему доступно через m.amount.

Вы можете встроить свой собственный алгоритм округления Money.prototype.valueOf(), если хотите.

selfawaresoup
источник
Мне нравится этот объектно-ориентированный подход, тот факт, что объект Money содержит оба значения, очень полезен. Это именно тот тип функциональности, который мне нравится создавать в моих пользовательских классах Objective-C.
james_womack
4
Его недостаточно точно для округления.
Генри Ценг
3
Не должно sys.puts (m + n); //80.76 собственно чтение sys.puts (m + n); //80.77? Я полагаю, вы забыли округлить 0,5 вверх.
Дэйв Л.
2
У такого подхода есть ряд тонких проблем, которые могут возникнуть. Например, вы не реализовали безопасные методы сложения, вычитания, умножения и так далее, поэтому вы, вероятно, столкнетесь с ошибками округления при объединении денежных сумм
1800 ИНФОРМАЦИЯ
2
Проблема здесь в том, что, например, это Money(0.1)означает, что лексер JavaScript считывает строку «0.1» из источника и затем преобразует ее в двоичную числа с плавающей запятой, а затем вы уже выполнили непреднамеренное округление. Проблема заключается в представлении (двоичное против десятичного), а не в точности .
mgd
3

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

просто используйте его во всех своих операциях.

https://github.com/MikeMcl/decimal.js/

tlejmi
источник
2

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

Решение ниже, объяснение следует:

Вам нужно подумать о математике, стоящей за этим, чтобы понять это. Реальные числа, такие как 1/3, не могут быть представлены в математике с десятичными значениями, поскольку они бесконечны (например, - .333333333333333 ...). Некоторые числа в десятичном формате не могут быть правильно представлены в двоичном формате. Например, 0,1 не может быть правильно представлено в двоичном формате с ограниченным количеством цифр.

Более подробное описание смотрите здесь: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Взгляните на реализацию решения: http://floating-point-gui.de/languages/javascript/

Генри Ценг
источник
1

Из-за двоичного характера их кодирования некоторые десятичные числа не могут быть представлены с идеальной точностью. Например

var money = 600.90;
var price = 200.30;
var total = price * 3;

// Outputs: false
console.log(money >= total);

// Outputs: 600.9000000000001
console.log(total);

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

var money = 60090;
var price = 20030;
var total = price * 3;

// Outputs: true
console.log(money >= total);

// Outputs: 60090
console.log(total);

Как избежать проблем с десятичной математикой в ​​JavaScript

Есть специальная библиотека для финансовых расчетов с отличной документацией. Finance.js

Али Рехман
источник
Мне нравится, что в Finance.js есть примеры приложений
james_womack
0

К сожалению, все ответы до сих пор игнорируют тот факт, что не все валюты имеют 100 подъединиц (например, цент - это подединица доллара США (USD)). Такие валюты, как иракский динар (IQD), состоят из 1000 субединиц: иракский динар состоит из 1000 филсов. Японская йена (JPY) не имеет дополнительных единиц. Так что «умножить на 100 для выполнения целочисленной арифметики» - не всегда правильный ответ.

Дополнительно для денежных расчетов вам также необходимо отслеживать валюту. Вы не можете добавить доллар США (USD) к индийской рупии (INR) (без предварительного преобразования одного в другой).

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

В денежных расчетах вы также должны иметь в виду, что деньги имеют конечную точность (обычно 0–3 десятичных знака) и округление должно выполняться определенными способами (например, «обычное» округление по сравнению с округлением банкира). Тип округления также может зависеть от юрисдикции / валюты.

Как обращаться с деньгами в javascript есть очень хорошее обсуждение соответствующих моментов.

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

markvgti
источник