SnakeDoc
SnakeDoc

Reputation: 14371

BigDecimal in JavaScript

I'm very new to JavaScript (I come from a Java background) and I am trying to do some financial calculations with small amounts of money.

My original go at this was:

<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>

This worked well except it always rounded up (Math.ceil). Math.floor has the same issue, and Math.round is also no good for floats.

In Java, I would have avoided the use of floats completely from the get-go, however in JavaScript there does not seem to be a default inclusion of something comparable.

The problem is, all the libraries mentioned are either broken or for a different purpose. The jsfromhell.com/classes/bignumber library is very close to what I need, however I'm having bizarre issues with its rounding and precision... No matter what I set the Round Type to, it seems to decide on its own. So for example, 3.7107 with precision of 2 and round type of ROUND_HALF_UP somehow winds up as 3.72 when it should be 3.71.

I also tried @JasonSmith BigDecimal library (a machined port from Java's BigDecimal), but it seems to be for node.js which I don't have the option of running.

How can I accomplish this using vanilla JavaScript (and be reliable) or is there a modern (ones mentioned above are all years old now) library that I can use that is maintained and is not broken?

Upvotes: 52

Views: 91750

Answers (5)

trincot
trincot

Reputation: 350310

Since we have native support for BigInt, it doesn't require much code any more to implement BigDecimal.

Here is a BigDecimal class based on BigInt with the following characteristics:

  • The number of decimals is configured as a constant, applicable to all instances.
  • Whether excessive digits are truncated or rounded is configured as a boolean constant.
  • An instance stores the decimal number as a BigInt, multiplied by a power of 10 so to include the decimals.
  • All calculations happen with those BigInt values.
  • The arguments passed to add, subtract, multiply and divide can be numeric, string, or instances of BigDecimal
  • These methods return new instances, so a BigDecimal is treated as immutable.
  • The toString method reintroduces the decimal point.
  • A BigDecimal can coerce to a number (via implicit call to toString), but that will obviously lead to loss of precision.

class BigDecimal {
    // Configuration: private constants
    static #DECIMALS = 18; // Number of decimals on all instances
    static #ROUNDED = true; // Numbers are truncated (false) or rounded (true)
    static #SHIFT = 10n ** BigInt(BigDecimal.#DECIMALS); // Derived constant
    static #fromBigInt = Symbol();  // Secret to allow construction with given #n value
    #n; // the BigInt that will hold the BigDecimal's value multiplied by #SHIFT
    constructor(value, convert) {
        if (value instanceof BigDecimal) return value;
        if (convert === BigDecimal.#fromBigInt) { // Can only be used within this class
            this.#n = value;
            return;
        }
        const [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");
    }
    add(num) {
        return new BigDecimal(this.#n + new BigDecimal(num).#n, BigDecimal.#fromBigInt);
    }
    subtract(num) {
        return new BigDecimal(this.#n - new BigDecimal(num).#n, BigDecimal.#fromBigInt);
    }
    static #divRound(dividend, divisor) {
        return new BigDecimal(dividend / divisor 
            + (BigDecimal.#ROUNDED ? dividend * 2n / divisor % 2n : 0n),
               BigDecimal.#fromBigInt);
    }
    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() {
        let s = this.#n.toString().replace("-", "").padStart(BigDecimal.#DECIMALS+1, "0");
        s = (s.slice(0, -BigDecimal.#DECIMALS) + "." + s.slice(-BigDecimal.#DECIMALS))
               .replace(/(\.0*|0+)$/, "");
        return this.#n < 0 ? "-" + s : s;
    }
}

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

Upvotes: 36

Leslie Wong
Leslie Wong

Reputation: 149

Wrapped @trincot 's great implementation of BigDecimal into an NPM module, combined with the BigInt polyfill JSBI and Reverse Polish notation algorithm.

With this module, it is quite intuitive to perform arbitrary arithmetic computation in JS now, even compatible with IE11.

npm install jsbi-calculator

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'

This is the link to the npm page. https://www.npmjs.com/package/jsbi-calculator.

Thanks once again for @trincot 's inspiration.

Upvotes: 0

noreply
noreply

Reputation: 917

Big.js is great, but too bulky for me.

I'm currently using the following which uses BigInt for arbitrary-precision. Only supports add, subtract, multiply, and divide. Calling set_precision(8); sets precision to 8 decimals.

Rounding mode is 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

Upvotes: 2

laffuste
laffuste

Reputation: 17095

There are several implementations of BigDecimal in js:

The last 3 come from the same author: see the differences.

Upvotes: 15

John Strickler
John Strickler

Reputation: 25421

I like using accounting.js for number, money and currency formatting.

Homepage - https://openexchangerates.github.io/accounting.js/

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

Upvotes: 10

Related Questions