@freemework/decimal.bignumberjs
Version:
decimal.bignumberjs library of the Freemework Project.
387 lines (317 loc) • 13.2 kB
text/typescript
// This module provide a wrapper over https://www.npmjs.com/package/bignumber.js
import {
FDecimal,
FDecimalBase,
FDecimalBackend,
FDecimalRoundMode,
FDecimalSettings,
FException,
FExceptionArgument,
FExceptionInvalidOperation,
FDecimalFractionDigits
} from "@freemework/common";
import { BigNumber } from "bignumber.js";
// See https://mikemcl.github.io/bignumber.js/#decimal-places
const DECIMAL_PLACES_MAP = new Map<BigNumber.Config["DECIMAL_PLACES"], typeof BigNumber>();
function getBigNumberImpl(DECIMAL_PLACES: BigNumber.Config["DECIMAL_PLACES"]): typeof BigNumber {
const existentBN = DECIMAL_PLACES_MAP.get(DECIMAL_PLACES);
if (existentBN !== undefined) { return existentBN; }
const newBN = BigNumber.clone();
newBN.config({ DECIMAL_PLACES: DECIMAL_PLACES! });
DECIMAL_PLACES_MAP.set(DECIMAL_PLACES, newBN);
return newBN;
}
class _FDecimalBigNumber extends FDecimalBase<BigNumber, FDecimalBackendBigNumber> {
public override get backend(): FDecimalBackendBigNumber { return super.backend; }
}
export class FDecimalBackendBigNumber implements FDecimalBackend {
private static verifyText(test: string): void {
if (!FDecimal.REGEXP.test(test)) {
throw new FExceptionArgument(`Bad FDecimal value: ${test}`);
}
}
private verifyInstance(test: FDecimal): asserts test is _FDecimalBigNumber {
if (test instanceof _FDecimalBigNumber && test.backend === this) { return; }
throw new FExceptionInvalidOperation(`Mixed '${FDecimal.name}' implementations detected.`);
}
public readonly settings: FDecimalSettings;
/**
*
* @param roundMode Default value is FDecimalRoundMode.Round
*/
public constructor(fractionalDigits: number, roundMode: FDecimalRoundMode) {
if (fractionalDigits < 0 || fractionalDigits > 32) {
throw new FExceptionArgument("Range 0..32 overflow", "fractionalDigits");
}
this.settings = {
decimalSeparator: ".",
fractionalDigits,
roundMode
};
}
public abs(value: FDecimal): FDecimal {
this.verifyInstance(value);
const unwrapValue: BigNumber = value.instance;
const result: BigNumber = unwrapValue.abs();
return new _FDecimalBigNumber(result, this);
}
public add(left: FDecimal, right: FDecimal): FDecimal {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: BigNumber = unwrapLeftValue.plus(unwrapRightValue);
return new _FDecimalBigNumber(result, this);
}
public divide(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const BN = getBigNumberImpl(this.settings.fractionalDigits + 2); // Why +2 ?
if (roundMode === undefined) {
roundMode = this.settings.roundMode;
}
const result: BigNumber = new BN(unwrapLeftValue).div(unwrapRightValue);
const bigNumberRoundMode = FDecimalBackendBigNumber.convertRoundMode(roundMode, result.isPositive());
const roundedResult: BigNumber = result.decimalPlaces(this.settings.fractionalDigits, bigNumberRoundMode);
return new _FDecimalBigNumber(roundedResult, this);
}
public equals(left: FDecimal, right: FDecimal): boolean {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: boolean = unwrapLeftValue.isEqualTo(unwrapRightValue);
return result;
}
public fromFloat(value: number, _?: FDecimalRoundMode): FDecimal {
const fractionalDigits: FDecimalFractionDigits = this.settings.fractionalDigits;
const BN = getBigNumberImpl(fractionalDigits);
const raw = new BN(value);
if (fractionalDigits < raw.decimalPlaces()!) {
let rawRoundMode: BigNumber.RoundingMode;
const roundMode: FDecimalRoundMode = this.settings.roundMode;
switch (roundMode) {
case FDecimalRoundMode.Ceil:
rawRoundMode = BigNumber.ROUND_CEIL;
break;
case FDecimalRoundMode.Floor:
rawRoundMode = BigNumber.ROUND_FLOOR;
break;
case FDecimalRoundMode.Round:
rawRoundMode = BigNumber.ROUND_HALF_EVEN;
break;
case FDecimalRoundMode.Trunc:
rawRoundMode = BigNumber.ROUND_DOWN;
break;
default:
throw new FDecimalBackendBigNumber.UnreachableRoundMode(roundMode);
}
const correctedRaw: BigNumber = raw.decimalPlaces(fractionalDigits, rawRoundMode);
return new _FDecimalBigNumber(correctedRaw, this);
}
return new _FDecimalBigNumber(raw, this);
}
public fromInt(value: number): FDecimal {
if (!Number.isSafeInteger(value)) {
throw new FExceptionArgument(`Wrong value ${value}. Expected safe integer.`, "value");
}
const CustomBigNumber = getBigNumberImpl(this.settings.fractionalDigits);
const raw: BigNumber = new CustomBigNumber(value);
return new _FDecimalBigNumber(raw, this);
}
public gt(left: FDecimal, right: FDecimal): boolean {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: boolean = unwrapLeftValue.gt(unwrapRightValue);
return result;
}
public gte(left: FDecimal, right: FDecimal): boolean {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: boolean = unwrapLeftValue.gte(unwrapRightValue);
return result;
}
public inverse(value: FDecimal): FDecimal {
this.verifyInstance(value);
const unwrapValue: BigNumber = value.instance;
const result: BigNumber = unwrapValue.negated();
return new _FDecimalBigNumber(result, this);
}
public isDecimal(test: any): test is FDecimal {
return test instanceof _FDecimalBigNumber;
}
public isNegative(test: FDecimal): boolean {
this.verifyInstance(test);
const unwrapValue: BigNumber = test.instance;
const result: boolean = unwrapValue.isNegative() && !unwrapValue.isZero();
return result;
}
public isPositive(test: FDecimal): boolean {
this.verifyInstance(test);
const unwrapValue: BigNumber = test.instance;
const result: boolean = unwrapValue.isPositive() && !unwrapValue.isZero();
return result;
}
public isZero(test: FDecimal): boolean {
this.verifyInstance(test);
const unwrapValue: BigNumber = test.instance;
const result: boolean = unwrapValue.isZero();
return result;
}
public lt(left: FDecimal, right: FDecimal): boolean {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: boolean = unwrapLeftValue.lt(unwrapRightValue);
return result;
}
public lte(left: FDecimal, right: FDecimal): boolean {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: boolean = unwrapLeftValue.lte(unwrapRightValue);
return result;
}
public max(left: FDecimal, right: FDecimal): FDecimal {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: BigNumber = BigNumber.max(unwrapLeftValue, unwrapRightValue);
return new _FDecimalBigNumber(result, this);
}
public min(left: FDecimal, right: FDecimal): FDecimal {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: BigNumber = BigNumber.min(unwrapLeftValue, unwrapRightValue);
return new _FDecimalBigNumber(result, this);
}
public mod(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const BN = getBigNumberImpl(this.settings.fractionalDigits + 2); // Why +2 ?
if (roundMode === undefined) {
roundMode = this.settings.roundMode;
}
const result: BigNumber = new BN(unwrapLeftValue).mod(unwrapRightValue);
const bigNumberRoundMode = FDecimalBackendBigNumber.convertRoundMode(roundMode, result.isPositive());
const roundedResult: BigNumber = result.decimalPlaces(this.settings.fractionalDigits, bigNumberRoundMode);
return new _FDecimalBigNumber(roundedResult, this);
}
public multiply(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const BN = getBigNumberImpl(unwrapLeftValue.decimalPlaces()! + unwrapRightValue.decimalPlaces()!);
if (roundMode === undefined) {
roundMode = this.settings.roundMode;
}
const result: BigNumber = new BN(unwrapLeftValue).multipliedBy(unwrapRightValue);
const bigNumberRoundMode = FDecimalBackendBigNumber.convertRoundMode(roundMode, result.isPositive());
const roundedResult: BigNumber = result.decimalPlaces(this.settings.fractionalDigits, bigNumberRoundMode);
return new _FDecimalBigNumber(roundedResult, this);
}
public parse(value: string): FDecimal {
FDecimalBackendBigNumber.verifyText(value); // raise error if wrong value
const fractionalDigits = this.settings.fractionalDigits;
const CustomBigNumber = getBigNumberImpl(fractionalDigits);
const raw: BigNumber = new CustomBigNumber(value);
const decimalPlaces: number = raw.decimalPlaces()!;
if (fractionalDigits < decimalPlaces) {
const bigNumberRoundMode = FDecimalBackendBigNumber.convertRoundMode(this.settings.roundMode, raw.isPositive());
const roundedRaw = raw.decimalPlaces(fractionalDigits, bigNumberRoundMode);
return new _FDecimalBigNumber(roundedRaw, this);
} else {
let rawStr = raw.toString(10);
if (rawStr.length < value.length) {
if (decimalPlaces > 0) {
// Workaround for X.XXX00000
rawStr = rawStr.padEnd(value.length, "0");
} else if (decimalPlaces === 0 && (rawStr.length + 1) < value.length) {
// Workaround for X.00000
rawStr += ".";
rawStr = rawStr.padEnd(value.length, "0");
}
}
if (rawStr !== value) {
throw new FExceptionArgument(`The value '${value}' cannot be represented in backend '${FDecimalBackendBigNumber.name}'`);
}
return new _FDecimalBigNumber(raw, this);
}
}
public round(value: FDecimal, fractionDigits: number, roundMode?: FDecimalRoundMode): FDecimal {
FractionDigitsGuard.verifyFraction(fractionDigits);
this.verifyInstance(value);
const unwrapValue: BigNumber = value.instance;
if (fractionDigits < unwrapValue.decimalPlaces()!) {
if (roundMode === undefined) {
roundMode = this.settings.roundMode;
}
const bigNumberRoundMode = FDecimalBackendBigNumber.convertRoundMode(roundMode, unwrapValue.isPositive());
const roundedResult = unwrapValue.decimalPlaces(fractionDigits, bigNumberRoundMode);
return new _FDecimalBigNumber(roundedResult, this);
} else {
return value;
}
}
public subtract(left: FDecimal, right: FDecimal): FDecimal {
this.verifyInstance(left);
this.verifyInstance(right);
const unwrapLeftValue: BigNumber = left.instance;
const unwrapRightValue: BigNumber = right.instance;
const result: BigNumber = unwrapLeftValue.minus(unwrapRightValue);
return new _FDecimalBigNumber(result, this);
}
public toNumber(value: FDecimal): number {
this.verifyInstance(value);
const unwrapValue: BigNumber = value.instance;
const result: number = unwrapValue.toNumber();
return result;
}
public toString(value: FDecimal): string {
this.verifyInstance(value);
const unwrapValue: BigNumber = value.instance;
const result: string = unwrapValue.toString(10);
return result;
}
private static convertRoundMode(roundMode: FDecimalRoundMode, isPositive: boolean): BigNumber.RoundingMode {
switch (roundMode) {
case FDecimalRoundMode.Ceil: return isPositive === true ? BigNumber.ROUND_UP : BigNumber.ROUND_DOWN;
case FDecimalRoundMode.Floor: return isPositive === true ? BigNumber.ROUND_DOWN : BigNumber.ROUND_UP;
case FDecimalRoundMode.Round: return isPositive === true ? BigNumber.ROUND_HALF_UP : BigNumber.ROUND_HALF_DOWN;
case FDecimalRoundMode.Trunc: return BigNumber.ROUND_DOWN;
default:
throw new FDecimalBackendBigNumber.UnreachableRoundMode(roundMode);
}
}
}
export namespace FDecimalBackendBigNumber {
export class UnreachableRoundMode extends FException {
public constructor(roundMode: never) {
super(`Unsupported round mode: ${roundMode}`);
}
}
}
namespace FractionDigitsGuard {
export function isFraction(test: number): test is FDecimalFractionDigits {
return Number.isSafeInteger(test) && test >= 0;
}
export function verifyFraction(test: FDecimalFractionDigits): asserts test is FDecimalFractionDigits {
if (!isFraction(test)) {
throw new FExceptionArgument("Wrong argument fraction. Expected integer >= 0");
}
}
}