JavaScript 中的 BigDecimal

IT技术 javascript floating-point bigdecimal
2021-03-09 07:23:37

我对 JavaScript 非常陌生(我来自 Java 背景),我正在尝试用少量资金进行一些财务计算。

我最初的做法是:

<script type="text/javascript">
    var normBase = ("[price]").replace("$", "");
    var salesBase = ("[saleprice]").replace("$", "");
    var base;
    if (salesBase != 0) {
        base = salesBase;
    } else {
        base = normBase;
    }
    var per5  = (base - (base * 0.05));
    var per7  = (base - (base * 0.07));
    var per10 = (base - (base * 0.10));
    var per15 = (base - (base * 0.15));
    document.write
        (
        '5% Off: $'  + (Math.ceil(per5  * 100) / 100).toFixed(2) + '<br/>' +
        '7% Off: $'  + (Math.ceil(per7  * 100) / 100).toFixed(2) + '<br/>' +
        '10% Off: $' + (Math.ceil(per10 * 100) / 100).toFixed(2) + '<br/>' +
        '15% Off: $' + (Math.ceil(per15 * 100) / 100).toFixed(2) + '<br/>'
    );
</script>

这很有效,只是它总是四舍五入 ( Math.ceil)。Math.floor有同样的问题,Math.round也不适合花车。

在 Java 中,我会从一开始就完全避免使用浮点数,但是在 JavaScript 中似乎没有默认包含类似的东西。

问题是,所有提到的库要么坏了,要么用于不同的目的。jsfromhell.com/classes/bignumber库非常接近我需要的东西,但是我在它的舍入和精度方面遇到了奇怪的问题......无论我将 Round Type 设置为什么,它似乎都可以自行决定。因此,例如,精度为 2 且圆形类型ROUND_HALF_UP为 3.72 的3.7107 以某种方式结束,而本应为 3.71。

我也尝试过@JasonSmith BigDecimal 库(Java 的 BigDecimal 的一个机器端口),但它似乎是用于 node.js 的,我没有运行的选项。

我如何使用 vanilla JavaScript(并且可靠)来实现这一点,或者是否有我可以使用的现代(上面提到的那些现在已经有好几年了)库被维护并且没有损坏?

5个回答

由于我们对 有本机支持BigInt,因此不再需要太多代码来实现BigDecimal

这是一个BigDecimal基于BigInt以下特征的类:

  • 小数位数配置为常数,适用于所有实例。
  • 过多的数字是否被截断或舍入被配置为布尔常量。
  • 一个实例将十进制数存储为 a BigInt,乘以 10 的幂以包含小数。
  • 所有计算都使用这些BigInt值进行。
  • 该参数传递给addsubtractmultiply并且divide可以是数字,字符串或实例BigDecimal
  • 这些方法返回新实例,因此 aBigDecimal被视为不可变的。
  • toString方法重新引入小数点。
  • ABigDecimal可以强制转换为数字(通过隐式调用toString),但这显然会导致精度损失。

class BigDecimal {
    // Configuration: constants
    static DECIMALS = 18; // number of decimals on all instances
    static ROUNDED = true; // numbers are truncated (false) or rounded (true)
    static SHIFT = BigInt("1" + "0".repeat(BigDecimal.DECIMALS)); // derived constant
    constructor(value) {
        if (value instanceof BigDecimal) return value;
        let [ints, decis] = String(value).split(".").concat("");
        this._n = BigInt(ints + decis.padEnd(BigDecimal.DECIMALS, "0")
                                     .slice(0, BigDecimal.DECIMALS)) 
                  + BigInt(BigDecimal.ROUNDED && decis[BigDecimal.DECIMALS] >= "5");
    }
    static fromBigInt(bigint) {
        return Object.assign(Object.create(BigDecimal.prototype), { _n: bigint });
    }
    add(num) {
        return BigDecimal.fromBigInt(this._n + new BigDecimal(num)._n);
    }
    subtract(num) {
        return BigDecimal.fromBigInt(this._n - new BigDecimal(num)._n);
    }
    static _divRound(dividend, divisor) {
        return BigDecimal.fromBigInt(dividend / divisor 
            + (BigDecimal.ROUNDED ? dividend  * 2n / divisor % 2n : 0n));
    }
    multiply(num) {
        return BigDecimal._divRound(this._n * new BigDecimal(num)._n, BigDecimal.SHIFT);
    }
    divide(num) {
        return BigDecimal._divRound(this._n * BigDecimal.SHIFT, new BigDecimal(num)._n);
    }
    toString() {
        const s = this._n.toString().padStart(BigDecimal.DECIMALS+1, "0");
        return s.slice(0, -BigDecimal.DECIMALS) + "." + s.slice(-BigDecimal.DECIMALS)
                .replace(/\.?0+$/, "");
    }
}

// Demo
var a = new BigDecimal("123456789123456789876");
var b = a.divide("10000000000000000000");
var c = b.add("9.000000000000000004");
console.log(b.toString());
console.log(c.toString());
console.log(+c); // loss of precision when converting to number

嘿,特林科特...是否可以在这个类中包含第 n 个数字的根?
2021-05-06 07:23:37
当然,为什么不呢?试一试吧。
2021-05-07 07:23:37
@trincot 这是一个很好的答案,您是否考虑将其作为module发布到 npm 上?
2021-05-09 07:23:37

我喜欢accounting.js用于数字、货币和货币格式。

主页- https://openexchangerates.github.io/accounting.js/

Github - https://github.com/openexchangerates/accounting.js

像魅力一样工作,完全符合预期!再次感谢你。(现在让 JavaScript 默认包含这样的东西!)
2021-04-15 07:23:37
继续我的评论:我的小脚本基本上只是在客户根据购物车总数将商品添加到购物车之前向我们网站上的客户提供报价。所以就我而言,这个库提供的“精度”(在它的四舍五入魔法等中)是可以接受的。
2021-04-20 07:23:37
@SnakeDoc 这如何解决您摆脱浮点数以保证精度的问题?我查看了accounting.js 代码和演示,看起来它只擅长格式化数字。
2021-05-03 07:23:37
我没有:(但是accounting.js 库最终按照我的预期处理了我的计算(即输出)。我主要用它来纠正javascript 内置的错误浮点数学,而不是用美元符号和然而,我仍在寻找一个好的和工作的 BigDecimal 端口到 javascript。
2021-05-14 07:23:37

BigDecimal 在js中有几种​​实现:

最后 3 个来自同一作者:查看差异

你推荐使用哪个?
2021-04-22 07:23:37

Big.js 很棒,但对我来说太笨重了。

我目前正在使用以下使用 BigInt 的任意精度。仅支持加、减、乘、除。调用set_precision(8);将精度设置为 8 位小数。

舍入模式为 ROUND_DOWN。


class AssertionError extends Error {

  /**
   * @param {String|void} message
   */
  constructor (message) {
    super(message);
    this.name = 'AssertionError';
    if (Error.captureStackTrace instanceof Function) {
      Error.captureStackTrace(this, AssertionError);
    }
  }

  toJSON () {
    return { name: this.name, message: this.message, stack: this.stack };
  }

  /**
   * @param {Boolean} value
   * @param {String|void} message
   */
  static assert (value, message) {
    if (typeof value !== 'boolean') {
      throw new Error('assert(value, message?), "value" must be a boolean.');
    }
    if (message !== undefined && typeof message !== 'string') {
      throw new Error('assert(value, message?), "message" must be a string.');
    }
    if (value === false) {
      throw new AssertionError(message);
    }
  }
}

module.exports = AssertionError;
const AssertionError = require('./AssertionError');

let precision = 2;
let precision_multiplier = 10n ** BigInt(precision);
let max_safe_integer = BigInt(Number.MAX_SAFE_INTEGER) * precision_multiplier;

/**
 * @param {Number} value
 */
const set_precision = (value) => {
  AssertionError.assert(typeof value === 'number');
  AssertionError.assert(Number.isFinite(value) === true);
  AssertionError.assert(Number.isInteger(value) === true);
  AssertionError.assert(value >= 0 === true);
  precision = value;
  precision_multiplier = 10n ** BigInt(precision);
  max_safe_integer = BigInt(Number.MAX_SAFE_INTEGER) * precision_multiplier;
};

/**
 * @param {Number} value
 */
const to_bigint = (value) => {
  AssertionError.assert(typeof value === 'number');
  AssertionError.assert(Number.isFinite(value) === true);
  return BigInt(value.toFixed(precision).replace('.', ''));
};

/**
 * @param {BigInt} value
 * @param {Number} decimal_places
 */
const to_number = (value) => {
  AssertionError.assert(typeof value === 'bigint');
  AssertionError.assert(value <= max_safe_integer);
  const value_string = value.toString().padStart(2 + precision, '0');
  const whole = value_string.substring(0, value_string.length - precision);
  const decimal = value_string.substring(value_string.length - precision, value_string.length);
  const result = Number(`${whole}.${decimal}`);
  return result;
};

/**
 * @param  {Number[]} values
 */
const add = (...values) => to_number(values.reduce((previous, current) => previous === null ? to_bigint(current) : previous + to_bigint(current), null));
const subtract = (...values) => to_number(values.reduce((previous, current) => previous === null ? to_bigint(current) : previous - to_bigint(current), null));
const multiply = (...values) => to_number(values.reduce((previous, current) => previous === null ? to_bigint(current) : (previous * to_bigint(current)) / precision_multiplier, null));
const divide = (...values) => to_number(values.reduce((previous, current) => previous === null ? to_bigint(current) : (previous * precision_multiplier) / to_bigint(current), null));

const arbitrary = { set_precision, add, subtract, multiply, divide };

module.exports = arbitrary;

const arbitrary = require('./arbitrary');

arbitrary.set_precision(2);
const add = arbitrary.add;
const subtract = arbitrary.subtract;
const multiply = arbitrary.multiply;
const divide = arbitrary.divide;

console.log(add(75, 25, 25)); // 125
console.log(subtract(75, 25, 25)); // 25
console.log(multiply(5, 5)); // 25
console.log(add(5, multiply(5, 5))); // 30
console.log(divide(125, 5, 5)); // 5
console.log(divide(1000, 10, 10)); // 10
console.log(divide(1000, 8.86)); // 112.86681715
console.log(add(Number.MAX_SAFE_INTEGER, 0)); // 9007199254740991
console.log(subtract(Number.MAX_SAFE_INTEGER, 1)); // 9007199254740990
console.log(multiply(Number.MAX_SAFE_INTEGER, 0.5)); // 4503599627370495.5
console.log(divide(Number.MAX_SAFE_INTEGER, 2)); // 4503599627370495.5
console.log(multiply(Math.PI, Math.PI)); // 9.86960437
console.log(divide(Math.PI, Math.PI)); // 1
console.log(divide(1, 12)); // 0.08333333
console.log(add(0.1, 0.2)); // 0.3
console.log(multiply(1.500, 1.3)); // 1.95
console.log(multiply(0, 1)); // 0
console.log(multiply(0, -1)); // 0
console.log(multiply(-1, 1)); // -1
console.log(divide(1.500, 1.3)); // 1.15384615
console.log(divide(0, 1)); // 0
console.log(divide(0, -1)); // 0
console.log(divide(-1, 1)); // -1
console.log(multiply(5, 5, 5, 5)); // 625
console.log(multiply(5, 5, 5, 123, 123, 5)); // 9455625

将 @trincot 对 BigDecimal 的出色实现包装到 NPM module中,并结合 BigInt polyfill JSBI 和反向波兰符号算法。

有了这个module,现在可以很直观的在JS中进行任意算术计算,甚至兼容IE11。

npm 安装 jsbi 计算器

import JBC from "jsbi-calculator";

const { calculator } = JBC;

const expressionOne = "((10 * (24 / ((9 + 3) * (-2)))) + 17) + 5";
const resultOne = calculator(expressionOne);
console.log(resultOne);
// -> '12'

const max = String(Number.MAX_SAFE_INTEGER);
console.log(max);
// -> '9007199254740991'
const expressionTwo = `${max} + 2`;
const resultTwo = calculator(expressionTwo);
console.log(resultTwo);
// -> '9007199254740993'

这是 npm 页面的链接。https://www.npmjs.com/package/jsbi-calculator

再次感谢@trincot 的灵感。