为什么会出现精度丢失?
由于 JavaScript 使用 IEEE 754 标准的 64 位双精度浮点型来表示数字,这种表示方法在处理小数时会出现精度问题。
方法一:使用decimal.js
decimal.js适用于精度问题比较多的项目,长期性解决精度问题。
特点
- 整数和浮点数
- 简单但功能齐全的 API
- 复制 JavaScript 和对象的许多方法Number.prototypeMath
- 还处理十六进制、二进制和八进制值
- 比 Java 的 BigDecimal 的 JavaScript 版本更快、更小,并且可能更易于使用
- 无依赖项
- 广泛的平台兼容性:仅使用 JavaScript 1.5 (ECMAScript 3) 功能
- 全面的文档和测试集
- 在 math.js 的后台使用
- 包括 TypeScript 声明文件:decimal.d.ts
安装decimal.js
npm install decimal.js
浮点数计算:加减乘除
// import Decimal from "decimal.js";
const Decimal = require('decimal.js')
let num1 = new Decimal('0.1');
let num2 = new Decimal('0.2');
// 加法
let sum = num1.plus(num2); // "0.3"
console.log(sum.toString()); // "0.3"
// 减法
let difference = num1.minus(num2); // "-0.1"
console.log(difference.toString()); // "-0.1"
// 乘法
let product = num1.times(num2); // "0.02"
console.log(product.toString()); // "0.02"
// 除法
let quotient = num1.dividedBy(num2); // "0.5"
console.log(quotient.toString()); // "0.5"
可以封装一个方法来便于调用
// import Decimal from "decimal.js";
const Decimal = require('decimal.js')
function calculate(a, b, operator) {
const decimalA = new Decimal(a);
const decimalB = new Decimal(b);
switch (operator) {
case '+':
return decimalA.plus(decimalB).toString();
case '-':
return decimalA.minus(decimalB).toString();
case '*':
return decimalA.times(decimalB).toString();
case '/':
if (b === '0' || b === 0) {
throw new Error('除数不能为零');
}
return decimalA.dividedBy(decimalB).toString();
default:
throw new Error('无效的操作符');
}
}
console.log(calculate('0.1', '0.2', '+')); // "0.3"
console.log(calculate('1.0', '0.1', '-')); // "0.9"
console.log(calculate('1.1', '2.2', '*')); // "2.42"
console.log(calculate('1.0', '3', '/')); // "0.33333333333333333333"
浮点数比较
直接使用浮点数比较可能会因为精度问题导致错误的结果,而 decimal.js 提供了精确的比较方法。
let amount1 = new Decimal('0.1').plus('0.2'); // "0.3"
let amount2 = new Decimal('0.3');
// 比较是否相等
console.log(amount1.equals(amount2)); // true
// 比较大小
console.log(amount1.greaterThan(amount2)); // false
console.log(amount1.lessThan(amount2)); // false
精度设置
// 全局设置精度5位(默认20位)
Decimal.set({ precision: 5 });
let result = new Decimal('1').dividedBy('3'); // 1除以3
console.log(result.toString()); // "0.33333"
// 局部设置精度(3位)
let localResult = new Decimal('1').dividedBy('3').toDecimalPlaces(3); // 1除以3
console.log(localResult.toString()); // "0.333"
方法二:封装简单的方法来实现金额计算
可以先获取要计算的两个数的小数位数最大值,确定需要放大多少倍才能将两个数都转为整数,之后以整数进行计算,计算完成后还原相应倍数。
如果小数位过多(15位以上)时,还是建议使用三方库来完成计算。
// 获取两个数中小数位数的最大值
function getMaxDecimalPlaces(num1, num2) {
const getDecimalPlaces = (num) => {
if (!isFinite(num)) return 0;
const match = ('' + num).match(/\.(\d+)$/);
return match ? match[1].length : 0;
};
return Math.max(getDecimalPlaces(num1), getDecimalPlaces(num2));
}
// 统一的浮点数计算方法
function calculate(a, b, operator) {
if (b === 0 && operator === '/') {
throw new Error('除数不能为零');
}
const decimalPlaces = getMaxDecimalPlaces(a, b);
const factor = Math.pow(10, decimalPlaces);
switch (operator) {
case '+':
return (Math.round(a * factor) + Math.round(b * factor)) / factor;
case '-':
return (Math.round(a * factor) - Math.round(b * factor)) / factor;
case '*':
const totalDecimalPlaces = getMaxDecimalPlaces(a, 0) + getMaxDecimalPlaces(b, 0);
const productFactor = Math.pow(10, totalDecimalPlaces);
return Math.round(a * b * productFactor) / productFactor;
case '/':
const decimalPlacesA = getMaxDecimalPlaces(a, 0);
const decimalPlacesB = getMaxDecimalPlaces(b, 0);
const divideFactorA = Math.pow(10, decimalPlacesA);
const divideFactorB = Math.pow(10, decimalPlacesB);
return Math.round((a * divideFactorA) / (b * divideFactorB)) / Math.pow(10, decimalPlacesA - decimalPlacesB);
default:
throw new Error('无效的操作符');
}
}
console.log(calculate(0.1, 0.2, '+')); // 0.3
console.log(calculate(0.3, 0.2, '-')); // 0.1
console.log(calculate(0.1, 0.2, '*')); // 0.02
console.log(calculate(0.3, 0.2, '/')); // 1.5