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

2

5 Answers

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

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

Homepage -

Github -

4

There are several implementations of BigDecimal in js:

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

1

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 

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

Thanks once again for @trincot 's inspiration.

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy