在快节奏的金融世界中,精确性就是一切。最好避免因舍入/精度误差而造成数百万美元的损失。
本文的出发点是认识到金钱并不是可以用来数篮子里苹果的平均基本数字。当您尝试将 10 欧元乘以 10 美元时,您会得到什么?很难啊……你有没有在旧夹克的口袋里发现过神奇的 1.546 美元?是的,我知道,这也是不可能的。这些愚蠢的例子是为了说明这样一个事实:金钱有其自己的特定规则,不能仅用简单的数字来建模。我向你保证,我不是第一个意识到这一点的人(也许你比我早意识到这一点)。 2002年,程序员Martin Fowler在《企业应用架构模式》中提出了一种用特定属性和操作数规则来表示货币的方法。对他来说,货币数据类型所需的两个最小可行属性是:
amount currency
这个真正基本的表示将是我们构建一个简单但强大的货币模型的起点。
金额绝对是一个特定的数字:它具有固定的精度(同样,你的口袋里不可能有 4.376 美元)。您需要选择一种能够帮助您尊重此约束的表示方式。
剧透警告,如果您不想看到几美分(如果不是美元)消失在浮点数表示的黑暗世界中,这绝对不是一个好主意。
如果您有一些 JavaScript 编码经验,您就会知道,即使是最简单的计算也可能会导致您一开始没有预料到的精度错误。强调这种现象的最明显和最著名的例子是:
0.1 0.2 !== 0.3 // true 0.1 0.2 // 0.30000000000000004
如果这个例子不能完全说服你,我建议你看看这篇文章,它深入探讨了使用 JavaScript 原生数字类型时可能遇到的所有有问题的计算结果......
结果中的这种微小差异可能对您来说似乎无害(幅度约为 10^-16),但是在关键的金融应用程序中,此类错误可能会迅速级联。考虑在数千个账户之间转移资金,其中每笔交易都涉及类似的计算。这些轻微的误差加起来,在你意识到之前,你的财务报表就已经减少了数千美元。老实说,我们都同意,在金钱方面,错误是不允许的:无论是在法律上还是与客户建立信任关系。
当我在一个项目中遇到这个问题时,我问自己的第一个问题是为什么?我发现问题的根源不是 JavaScript,这些不精确性也会影响其他现代编程语言(Java、C、Python……)。
// In C #includeint 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 标准指定的双(或单)精度浮点格式。
在Javascript中,原生类型数字对应的是双精度浮点数,也就是说一个数字用64位进行编码,分为三部分:
然后,您需要使用以下公式将您的位表示形式转换为十进制值:
双精度浮点数表示的示例,近似为 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
这种格式允许我们表示广泛的值,但它不能以绝对精度表示每个可能的数字(仅在 0 和 1 之间,您可以找到无穷多个数字......)。许多数字无法用二进制形式精确表示。循环第一个示例,这就是 0.1 和 0.2 的问题。双点浮点表示为我们提供了这些值的近似值,因此当您将这两个不精确的表示相加时,结果也不精确。
既然您完全相信使用原生 JavaScript 数字类型处理金额是一个坏主意(至少我希望您开始对此产生怀疑),那么 10 亿美元的问题是 您应该如何进行 ?解决方案可能是利用 JavaScript 中提供的一些强大的固定精度算术包。例如 Decimal.js(流行的 ORM Prisma 使用它来表示其 Decimal 数据类型)或 Big.js。
这些包为您提供了特殊的数据类型,允许您执行计算并消除我们上面解释的精度误差。
// 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'
这种方法为您提供了另一个优势,它极大地扩展了可以表示的最大值,例如,当您处理加密货币时,这会变得非常方便。
即使它真的很强大,我也不会选择在我的 Web 应用程序中实现它。我发现应用 Stripe 策略仅处理整数值更容易、更清晰。
我们,Theodo Fintech,重视实用主义!我们喜欢从业内最成功的公司中汲取灵感。 Stripe 是一家专注于支付服务的著名的价值数十亿美元的公司,它选择使用整数而不是浮点数来处理金额。为此,他们使用最小的货币单位来表示货币金额。
// 10 USD are represented by { "amount": 1000, "currency": "USD" }
我想你们很多人已经知道这一点:所有货币都不具有相同数量级的最小单位。其中大多数是“两位小数”货币(欧元、美元、英镑),这意味着它们的最小单位是货币的 1/100。然而,有些是“小数点后三位”货币(KWD),甚至是“小数点零位”货币(日元)。 (您可以通过遵循ISO4217标准找到更多相关信息)。为了处理这些差异,您应该将乘法因子集成到您的货币数据表示中,以将以最小单位表示的金额转换为相应的货币。
我想您已经弄清楚了,您可以使用本机数字、第三方任意精度包或整数,计算可以(并且将会)得出浮点结果,您需要将其四舍五入到您的货币有限精度。举个简单的例子,假设您处理整数值并签订了 16k 美元的贷款合同,利率非常精确,为 8.5413%(哎哟……)。然后您需要退还 16k$ 加上额外的金额
1600000 * 0.085413 // amount in cents //Output in cents: 136660.8
关键是计算后要正确处理金额的四舍五入过程。大多数时候,您最终不得不在三种不同类型的舍入之间进行选择:
说到四舍五入,实际上并没有什么“神奇酱”:您需要根据自己的情况进行仲裁。我建议您在处理新货币和新的舍入用例(转换、资金分割、积分利率等)时始终检查立法。您最好立即遵守规定,以避免进一步的麻烦。例如,在汇率方面,大多数货币都确定了所需精度和舍入规则的规则(您可以在此处查看欧元汇率规则)。
本文并未详尽介绍在 JavaScript 中处理金额的所有现有可能性,也无意为您提供完整/完美的货币数据模型。我试图为您提供足够的提示和指南来实施一种被证明是一致的、有弹性的、由金融科技行业的大人物选择的表示形式。我希望您能够在未来的项目中进行金额计算,而不会忘记口袋里的任何一分钱(否则不要忘记看看您的旧夹克)!
免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。
Copyright© 2022 湘ICP备2022001581号-3