Reputation: 14371
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
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:
BigInt
, multiplied by a power of 10 so to include the decimals.BigInt
values.add
, subtract
, multiply
and divide
can be numeric, string, or instances of BigDecimal
BigDecimal
is treated as immutable.toString
method reintroduces the decimal point.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
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
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
Reputation: 17095
There are several implementations of BigDecimal in js:
The last 3 come from the same author: see the differences.
Upvotes: 15
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