当浮点精度不够好时,如何在 JavaScript 中处理精确的十进制计算
曾经尝试过在 JavaScript 中进行精确计算吗?这些问题可能会导致严重的问题,尤其是在处理金钱、百分比或任何需要精确的计算时。
// Basic arithmetic issues
0.1 + 0.2; // 0.30000000000000004
1.0 - 0.9; // 0.09999999999999998
0.7 + 0.1; // 0.7999999999999999
// Division problems
1 / 3; // 0.3333333333333333
1 / 6; // 0.16666666666666666
// Multiplication can be surprising
0.3 / 0.1; // 2.9999999999999996
(1 / 98) * 98; // 0.9999999999999999
// Money calculations break
19.99 * 0.1; // 1.9989999999999999
这不是一个 JavaScriptbug。这是计算机存储十进制数的一个基本限制,它影响了几乎所有的编程语言。要理解为什么我们需要 Fraction. js,我们首先需要了解幕后到底发生了什么。
计算机以二进制(以 2 为底)存储数字。虽然这对整数非常有效,但它会给小数带来问题。
在以 10 为底(我们的十进制系统)中,我们可以将 1/10 简单地表示为 0.1。但是在二进制中,0.1 是一个重复的分数——就像试图用十进制表示 1/3(0.333333…)。当它以 JavaScript 的 64 位浮点格式(IEEE 754)存储时,它必须被截断,从而导致微小的舍入错误。
你可能认为 BigInt 可以解决这个问题,但它有自己的局限性:
// BigInt only works with whole numbers
BigInt(1) / BigInt(3) // Error: Cannot divide BigInts
BigInt(0.1) // TypeError: Cannot convert decimal to BigInt
BigInt 解决了整数的精度问题,但根本不能处理小数。它专为不同的用例而设计,例如密码学或非常大的整数计算。
一个常见的解决方法是将所有内容乘以 100 以完全避免小数:
// Common attempt: multiply to remove decimals
const dollars = 19.99 * 100 // 1999 cents
const tax = 8.5 * 100 // 850 basis points
// Still breaks down
console.log((dollars * tax) / 10000) // 169.91499999999996 - More rounding errors
这种方法看起来很简单,但在复杂的计算或多个操作中会失败。
使用分数而不是小数
让我们使用Fraction. js:来解决这些精度问题,这是一个将数字作为分数而不是浮点小数处理的库。就像我们在学校学习数学一样:1/3 比 0.333333 更精确。
Terminal window
➜ npm i fraction.js
Fraction. js 对分子和分母都使用 BigInt 表示,确保最小的性能开销,同时最大限度地提高准确性。它的设计针对精度进行了优化,使其成为其他数学工具(如多项式.js和Math.js)的基础库的理想选择
import Fraction from 'fraction.js';
// Basic arithmetic is now exact
new Fraction(0.1).add(0.2).toString(2) // "0.30"
new Fraction(1.0).sub(0.9).toString(2) // "0.10"
new Fraction(0.7).add(0.1).toString(1) // "0.8"
// Division with controlled precision
new Fraction(1, 3).toString(2) // "0.33"
new Fraction('1/6').toString(2) // "0.17"
// Money calculations stay precise
const price = new Fraction('19.99')
const tax = new Fraction('0.085') // 8.5% tax
const total = price.add(price.mul(tax))
total.toString(2) // "21.69"
// For fractions, we might want the actual fraction representation
new Fraction('0.333333').toFraction() // "1/3"
new Fraction('1.4166666').toFraction() // "17/12"
Fraction. js 没有尝试用二进制表示小数,这会导致我们之前看到的舍入误差,而是将每个数字存储为整数的比率。例如,0.1 在内部变为 1/10。这意味着我们的计算保持精确,就像在纸上一样。
了解限制
Fraction. js 功能强大,但不是魔法。它适用于有理数——可以表示为整数比率的数字(如 3/4 或 22/7)。这使得它非常适合货币计算或简单分数,但对于 π 或 √2 等无理数来说就不那么好了。
虽然它在内部使用**BigInt**,但您仍在边缘使用 JavaScript 的数字系统。
这意味着你得到的精度比常规小数高,但不是无限精度。有些操作也有局限性。平方根给出近似值而不是精确值,并且不支持像 sin 或 cos 这样的超越函数。复数也不在桌面上。
即使有这些限制,Fraction. js 仍然非常有用。你可以用它提前计算精确的值,处理没有舍入错误的金钱计算,构建处理分数的教育软件,或者解决任何十进制精度真正重要的问题。关键是知道什么时候使用它,什么时候其他东西可能更合适。