UNPKG

bigint-money

Version:

A Money class for high precision calculations using the ESnext bigint type.

229 lines 8.39 kB
import { IncompatibleCurrencyError } from './errors.js'; import { bigintToFixed, divide, moneyValueToBigInt, PRECISION, PRECISION_I, PRECISION_M, Round, } from './util.js'; export class Money { currency; value; round; constructor(value, currency, round = Round.HALF_TO_EVEN) { this.currency = currency; this.round = round; this.value = moneyValueToBigInt(value, this.round); } /** * Return a string representation of the money value. * * Precision is a number of decimals that was requested. The decimals are * always returned, e.g.: new Money(1, 'USD').toFixed(2) returns '1.00'. * * This function rounds to even, a.k.a. it uses bankers rounding. */ toFixed(precision) { return bigintToFixed(this.value, precision, this.round); } add(val) { if (val instanceof Money && val.currency !== this.currency) { throw new IncompatibleCurrencyError('You cannot add Money from different currencies. Convert first'); } const addVal = moneyValueToBigInt(val, this.round); const r = Money.fromSource(addVal + this.value, this.currency, this.round); return r; } subtract(val) { if (val instanceof Money && val.currency !== this.currency) { throw new IncompatibleCurrencyError('You cannot subtract Money from different currencies. Convert first'); } const subVal = moneyValueToBigInt(val, this.round); return Money.fromSource(this.value - subVal, this.currency, this.round); } /** * Divide the current number with the specified number. * * This function returns a new Money object with the result. * * Unlike add, subtract, divide and multiply do accept mismatching * currencies. When calling divide, the currency of _this_ Money object will * be used for the resulting object. */ divide(val) { // Even though val1 was already in 'bigint' format, we run this // again as otherwise we will lose precision. // // This means for an original of $1 this would now be $1 * 10**24. const val1 = moneyValueToBigInt(this.value, this.round); // Converting the dividor. const val2 = moneyValueToBigInt(val, this.round); return Money.fromSource(divide(val1, val2, this.round), this.currency, this.round); } /** * Multiply * * Unlike add, subtract, divide and multiply do accept mismatching * currencies. When calling multiply, the currency of _this_ Money object will * be used for the resulting object. */ multiply(val) { const valBig = moneyValueToBigInt(val, this.round); // Converting the dividor. const resultBig = valBig * this.value; return Money.fromSource(divide(resultBig, PRECISION_M, this.round), this.currency, this.round); } /** * Pow returns the current value to it's exponent. * * pow currently only supports whole numbers. */ pow(exponent) { if (typeof exponent === 'number' && !Number.isInteger(exponent)) { throw new Error('You can currently only use pow() with whole numbers'); } if (exponent > 1) { const resultBig = this.value ** BigInt(exponent); return Money.fromSource(divide(resultBig, PRECISION_M ** (BigInt(exponent) - 1n), this.round), this.currency, this.round); } else if (exponent < 0) { return new Money(1, this.currency, this.round).divide(this.pow(-exponent)); } else if (exponent === 1) { return this; } else { return new Money(1, this.currency, this.round); } } /** * Returns the absolute value. */ abs() { return this.multiply(this.sign()); } /** * Return -1 if the value is less than zero, 0 if zero, and 1 if more than zero. */ sign() { return this.compare(0); } /** * Returns true if this Money object is _less_ than the passed value */ isLesserThan(val) { return this.compare(val) === -1; } /** * Returns true if this Money object is _more_ than the passed value */ isGreaterThan(val) { return this.compare(val) === 1; } /** * Returns true if this Money object is _more_ than the passed value */ isEqual(val) { return this.compare(val) === 0; } /** * Returns true if this Money object is _more_ than the passed value */ isLesserThanOrEqual(val) { return this.compare(val) < 1; } /** * Returns true if this Money object is _more_ than the passed value */ isGreaterThanOrEqual(val) { return this.compare(val) > -1; } /** * Compares this Money object with another value. * * If the values are equal, 0 is returned. * If this object is considered to be lower, -1 is returned. * If this object is considered to be higher, 1 is returned. */ compare(val) { if (val instanceof Money && val.currency !== this.currency) { throw new IncompatibleCurrencyError('You cannot compare different currencies.'); } const bigVal = moneyValueToBigInt(val, this.round); if (bigVal === this.value) { return 0; } return this.value < bigVal ? -1 : 1; } /** * Allocate this value to different parts. * * This is useful in cases no money can be lost when splitting in different * parts. For example, when splitting $1 between 3 people, this function will * return 3.34, 3.33, 3.33. * * The remainder of the split will be added round-robin to the results, * starting with the first group. * * The reason precision must be specified, is because under the hood this * library uses 12 digits for precision. But when splitting a whole dollar, * you might only be interested in cents (precision = 2). * * */ allocate(parts, precision) { const bParts = BigInt(parts); // Javascript will round to 0. const fraction = this.value / bParts; const remainder = this.value % bParts; // This value is used for rounding to the desired precision const precisionRounder = BigInt(10) ** (PRECISION - BigInt(precision)); const roundedFraction = (fraction / precisionRounder); const roundedRemainder = fraction % precisionRounder; // We had 2 division operators, and we want to keep remainders for both // of them. const totalRoundedRemainder = ((roundedRemainder + remainder) * bParts) / precisionRounder; const result = Array(parts).fill(roundedFraction); // Figure out how many spare 'cents' we need to distribute. If the number // is negative, we need to spread debt instead. const add = BigInt(totalRoundedRemainder > 0 ? 1 : -1); for (let i = 0; i < Math.abs(Number(totalRoundedRemainder)); i++) { result[i] += add; } return result.map(item => { return Money.fromSource(item * precisionRounder, this.currency, this.round); }); } /** * Returns the underlying bigint value. * * This is the current value of the object, multiplied by 10 ** 12. */ toSource() { return this.value; } /** * A factory function to construct a Money object a 'source' value. * * The source value is just the underlying bigint used in the Money * class and can be obtained by calling Money.getSource(). */ static fromSource(val, currency, round = Round.HALF_TO_EVEN) { const m = new Money(0, currency, round); m.value = val; return m; } /** * This function creates custom output in console.log statements. */ [Symbol.for('nodejs.util.inspect.custom')]() { return this.format() + ' ' + this.currency; } /** * A default output for serializing to JSON */ toJSON() { return [this.format(), this.currency]; } /** * This function will return a string with all irrelevant 0's removed. */ format() { return this.toFixed(PRECISION_I).replace(/\.?0+$/, ''); } } //# sourceMappingURL=money.js.map