ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript の財務精度: 1 セントも失わずにお金を扱う

JavaScript の財務精度: 1 セントも失わずにお金を扱う

WBOY
WBOYオリジナル
2024-08-10 06:45:02836ブラウズ

Financial Precision in JavaScript: Handle Money Without Losing a Cent

ペースの速い金融の世界では、正確さがすべてです。四捨五入や精度の誤差が原因で 100 万ドルの損失が発生するのは常に避けた方が良いです。

お金に関して言えば、データ型について考えてみましょう

この記事の出発点は、お金はかごの中のリンゴを数えるのに使用できる平均的な基本的な数字ではないという認識です。 10ユーロと10ドルを掛けてみると何が得られますか?大変ですね…古いジャケットのポケットから奇跡の 1.546 ドルを見つけたことがありますか?はい、わかっています、これも実際には不可能です。これらの愚かな例は、お金には独自の特別なルールがあり、単純な数字だけでモデル化することはできないという事実を説明するためにここに挙げたものです。安心してください、私が最初にそのことに気づいたわけではありません(おそらくあなたは私よりずっと前に気づいていたでしょう)。 2002 年、プログラマーの Martin Fowler は、「エンタープライズ アプリケーション アーキテクチャのパターン」の中で、特定の属性とオペランド ルールを使用してお金を表現する方法を提案しました。彼にとって、お金のデータ型に必要な最小限の実行可能な属性は次の 2 つでした。

amount
currency

この非常に基本的な表現は、シンプルだが堅牢な通貨モデルを構築するための出発点となります。

お金の量、その表現方法

金額は間違いなく特定の数値です。精度は固定されています (繰り返しますが、ポケットに 4.376 ドルを入れることはできません)。この制約を尊重できるような表現方法を選択する必要があります。

素朴なアプローチ、ネイティブ数値 JavaScript データ型

ネタバレに注意してください。浮動小数点数表現の暗い世界で数セント (ドルではないとしても) が消えていくのを見たくないのであれば、これは決して良い考えではありません。

コストのかかる精度エラー

JavaScript でのコーディングの経験がある方は、最も単純な計算でも、最初は予期しない精度の誤差が生じる可能性があることをご存知でしょう。この現象を強調する最も明白でよく知られた例は次のとおりです:

0.1 + 0.2 !== 0.3 // true
0.1 + 0.2 // 0.30000000000000004

この例で十分に納得できない場合は、JavaScript のネイティブ数値型を使用する際に発生する可能性のあるすべての問題のある計算結果についてもう少し詳しく説明しているこの記事を参照することをお勧めします…

結果のこのわずかなデルタは、ユーザーにとっては無害に見えるかもしれません (大きさは約 10^-16) が、重要な金融アプリケーションでは、このようなエラーは急速に連鎖する可能性があります。各取引に同様の計算が含まれる、数千のアカウント間で資金を移動することを検討してください。わずかな誤差が積み重なり、気が付けば財務諸表が数千ドルもずれてしまいます。そして正直に言うと、お金のことに関しては、法的にも、顧客との信頼関係を築くためにも、間違いは許されないということには誰もが同意できます。

なぜこのようなエラーが発生するのでしょうか?

プロジェクトの 1 つで問題に遭遇したときに私が自問した最初の質問は、なぜ? ということでした。問題の原因は JavaScript ではなく、これらの不正確さは他の最新のプログラミング言語 (Java、C、Python など) にも影響を与えることがわかりました。

// In C
#include <stdio.h>

int main() {
    double a = 0.1;
    double b = 0.2;
    double sum = a + b;
    if (sum == 0.3) {
        printf("Equal\n");
    } else {
        printf("Not Equal\n"); // This block is executed
    }
    return 0;
}

// > Not equal
// In Java
public class doublePrecision {
    public static void main(String[] args) {

        double total = 0;
        total += 5.6;
        total += 5.8;
        System.out.println(total);
    }
}

// > 11.399999999999

実際、根本的な原因は、浮動小数点数を表すためにこれらの言語で使用されている標準、つまり IEEE 754 標準で指定されている倍精度 (または単精度) 浮動小数点形式にあります。

IEEE 754 標準: ビット表現の物語

JavaScript では、ネイティブ型の数値は倍精度浮動小数点数に対応します。これは、数値が 64 ビットでエンコードされ、3 つの部分に分割されることを意味します。

  • 符号用の 1 ビット
  • 指数用の 11 ビット
  • 0 から 1 の範囲の仮数 (または小数部) の 52 ビット

Financial Precision in JavaScript: Handle Money Without Losing a Cent

Financial Precision in JavaScript: Handle Money Without Losing a Cent

次に、次の式を使用してビット表現を 10 進数値に変換する必要があります。

Financial Precision in JavaScript: Handle Money Without Losing a Cent

倍精度浮動小数点数表現の例、1/3 の近似:

0 01111111101 0101010101010101010101010101010101010101010101010101

= (-1)^0 x (1 + 2^-2 + 2^-4 + 2^-6 + ... + 2^-50 + 2^-52) x 2^(1021-1023)
= 0.333333333333333314829616256247390992939472198486328125
~ 1/3

This format allows us to represent a vast range of values, but it can't represent every possible number with absolute precision (just between 0 and 1 you can find an infinity of numbers…). Many numbers cannot be represented exactly in binary form. To loop on the first example, that's the issue with 0.1 and 0.2. Double-point floating representation gives us an approximation of these value, so when you add these two imprecise representations, the result is also not exact.

A possible solution: Arbitrary Decimal Arithmetic

Now that you are fully convinced that handling money amounts with native JavaScript number type is a bad idea (at least I hope you begin to have doubts about it), the 1Billion$ question is how should you proceed ? A solution could be to make use of some of the powerful fixed-precision arithmetic packages available in JavaScript. For example Decimal.js (which is used by the popular ORM Prisma to represent its Decimal datatype) or Big.js.

These packages provide you with special datatypes that allow you to perform calculations with getting rid of precision errors we explained above.

// Example using Decimal.js
const Decimal = require('decimal.js');
const a = new Decimal('0.1');
const b = new Decimal('0.2');
const result = a.plus(b);

console.log(result.toString()); // Output: '0.3'

This approach provides you with another advantage, it drastically extends the maximum value that can be represented, which can become pretty handy when you are dealing with cryptocurrencies for example.
Even if it is really robust, that it not the one I prefer to choose to implement in my web applications. I find it easier and clearer to apply Stripe strategy to only deal with integer values.

Learn from Masters : Stripe, a No-Floating Points Strategy

We, at Theodo Fintech, value pragmatism! We love to take inspiration from the most succeeding companies in the industry. Stripe, the well-known billions$ company specialized in payment services, made the choice to handle money amounts without floating numbers but with integers. To do that, they use the smallest unit of the currency to represent a monetary amount.

// 10 USD are represented by
{
 "amount": 1000,
 "currency": "USD"
}

Currency Minimal Units… Are Not Consistent!

I guess that many of you already know this: all currencies don't have the smallest unit of the same magnitude. Most of them are "two-decimal" currencies (EUR, USD, GBP) which means that their smallest unit is 1/100th of the currency. However, some are "three-decimal" currencies (KWD) or even "zero-decimal" currencies (JPY). (You can find more information about it by following the ISO4217 standard). To handle these disparities, you should integrate to your money data representation the multiplicative factor to convert an amount represented in the smallest unit into the corresponding currency.

Financial Precision in JavaScript: Handle Money Without Losing a Cent

I Chose a Way to Represent Money Amount… OK, But Now, How Do I Round Them?

I guess you already figured it out, you can either work with native number , third party arbitrary-precision packages or integers, calculations can (and will) lead you to floating-point results that you will need to round to your monetary finite precision. As a quick example is never to much, let's say that you handle integer values and contracted a loan of 16k$ with a really precise interest rate of 8.5413% (ouch…). You then need to refund 16k$ plus an additional amount of

1600000 * 0.085413 // amount in cents
//Output in cents: 136660.8

The crux is to properly handle the rounding process of money amounts after calculations. Most of the time, you end up having to choose between three different kinds of rounding:

  • Classic rounding: Round to the closest value and if halfway between two values, round up
  • Banker rounding: Round to the closest value and if halfway between two values, round down if the number is even, and round up if the number is odd (this gives you numerical stability when you have to perform lots of rounding)
  • Custom rounding: based on particular legislation for the currency you use and the use case you are handling

When it comes to roundings, there is not really any "magic sauce": you need to arbitrate depending on your situation. I recommend you always check legislation when you deal with a new currency and a new rounding use case (conversion, money split, interest rates for credits, …). You better follow regulations right away to avoid further troubles. For example, when it comes to conversion rates, most currencies have determined rules about needed precision and rounding rules (you can have a look at the EUR conversion rate rules here).

結論

この記事は、JavaScript で金額を処理するための既存の可能性をすべて網羅しているわけではなく、また、完全/完璧な金額データ モデルを提供することを目的としたものでもありません。私は、一貫性と回復力が証明され、フィンテック業界の大手企業によって選ばれた表現を実装するための十分なヒントとガイドラインを提供しようと努めました。今後のプロジェクトで、ポケットに 1 セントも忘れずに金額計算を実行できるようになることを願っています (そうでない場合は、古いジャケットを確認することを忘れないでください)。

以上がJavaScript の財務精度: 1 セントも失わずにお金を扱うの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。