@freemework/common
Version:
Common library of the Freemework Project.
521 lines (444 loc) • 18.4 kB
text/typescript
import { FException } from "../exception/f_exception.js";
import { FExceptionArgument } from "../exception/f_exception_argument.js";
import { FExceptionInvalidOperation } from "../exception/f_exception_invalid_operation.js";
export abstract class FDecimal {
public static readonly REGEXP: RegExp = /^([+-]?)(0|[1-9][0-9]*)(\.([0-9]+))?$/;
private static _cfg: { readonly backend: FDecimalBackend; readonly settings: FDecimalSettings; } | null = null;
private static get cfg(): { readonly backend: FDecimalBackend; readonly settings: FDecimalSettings; } {
const cfg: { readonly backend: FDecimalBackend; readonly settings: FDecimalSettings; } | null = this._cfg;
if (cfg !== null) {
return cfg;
}
throw new FExceptionInvalidOperation(`${FDecimal.name} is not configured. Did you call ${FDecimal.name}.configure()?`);
}
private static get backend(): FDecimalBackend { return FDecimal.cfg.backend; }
public static get settings(): FDecimalSettings { return FDecimal.cfg.settings; }
public static configure(backend: FDecimalBackend): void {
if (FDecimal._cfg !== null) {
throw new FExceptionInvalidOperation(`Cannot ${FDecimal.name}.configure() twice. By design you have to call ${FDecimal.name}.configure() once.`);
}
this._cfg = Object.freeze({
backend,
settings: Object.freeze({
...backend.settings
})
});
}
/**
* Analog of Math.abs()
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs
*/
public static abs(value: FDecimal): FDecimal { return FDecimal.backend.abs(value); }
public static add(left: FDecimal, right: FDecimal): FDecimal { return FDecimal.backend.add(left, right); }
public static divide(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal { return FDecimal.backend.divide(left, right, roundMode); }
public static equals(left: FDecimal, right: FDecimal): boolean { return FDecimal.backend.equals(left, right); }
public static fromFloat(value: number, roundMode?: FDecimalRoundMode): FDecimal { return FDecimal.backend.fromFloat(value, roundMode); }
public static fromInt(value: number): FDecimal { return FDecimal.backend.fromInt(value); }
public static gt(left: FDecimal, right: FDecimal): boolean { return FDecimal.backend.gt(left, right); }
public static gte(left: FDecimal, right: FDecimal): boolean { return FDecimal.backend.gte(left, right); }
public static inverse(value: FDecimal): FDecimal { return FDecimal.backend.inverse(value); }
public static isDecimal(test: any): test is FDecimal { return FDecimal.backend.isDecimal(test); }
public static isNegative(test: FDecimal): boolean { return FDecimal.backend.isNegative(test); }
public static isPositive(test: FDecimal): boolean { return FDecimal.backend.isPositive(test); }
public static isZero(test: FDecimal): boolean { return FDecimal.backend.isZero(test); }
public static lt(left: FDecimal, right: FDecimal): boolean { return FDecimal.backend.lt(left, right); }
public static lte(left: FDecimal, right: FDecimal): boolean { return FDecimal.backend.lte(left, right); }
/**
* Analog of Math.max()
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max
*/
public static max(left: FDecimal, right: FDecimal): FDecimal { return FDecimal.backend.max(left, right); }
/**
* Analog of Math.min()
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min
*/
public static min(left: FDecimal, right: FDecimal): FDecimal { return FDecimal.backend.min(left, right); }
public static mod(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal { return FDecimal.backend.mod(left, right, roundMode); }
public static multiply(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal { return FDecimal.backend.multiply(left, right, roundMode); }
public static parse(value: string): FDecimal { return FDecimal.backend.parse(value); }
public static round(value: FDecimal, fractionDigits: FDecimalFractionDigits, roundMode?: FDecimalRoundMode): FDecimal { return FDecimal.backend.round(value, fractionDigits, roundMode); }
public static subtract(left: FDecimal, right: FDecimal): FDecimal { return FDecimal.backend.subtract(left, right); }
public abstract abs(): FDecimal;
public abstract add(value: FDecimal): FDecimal;
public abstract divide(value: FDecimal, roundMode?: FDecimalRoundMode): FDecimal;
public abstract equals(value: FDecimal): boolean;
public abstract inverse(): FDecimal;
public abstract isNegative(): boolean;
public abstract isPositive(): boolean;
public abstract isZero(): boolean;
public abstract mod(value: FDecimal, roundMode?: FDecimalRoundMode): FDecimal;
public abstract multiply(value: FDecimal, roundMode?: FDecimalRoundMode): FDecimal;
public abstract round(fractionDigits: FDecimalFractionDigits, roundMode?: FDecimalRoundMode): FDecimal;
public abstract subtract(value: FDecimal): FDecimal;
public abstract toNumber(): number;
public abstract toString(): string;
public abstract toJSON(): string;
}
export type FDecimalFractionDigits = number;
export function isDecimalFraction(test: number): test is FDecimalFractionDigits {
return Number.isSafeInteger(test) && test >= 0;
}
export function verifyDecimalFraction(test: FDecimalFractionDigits): asserts test is FDecimalFractionDigits {
if (!isDecimalFraction(test)) {
throw new FExceptionArgument("Wrong argument fraction. Expected integer >= 0");
}
}
export enum FDecimalRoundMode {
/**
* Round to the smallest Financial greater than or equal to a given Financial.
*
* In other words: Round UP
*
* Example of Ceil to fraction:2
* * 0.595 -> 0.60
* * 0.555 -> 0.56
* * 0.554 -> 0.56
* * -0.595 -> -0.59
* * -0.555 -> -0.55
* * -0.554 -> -0.55
*/
Ceil = "Ceil",
/**
* Round to the largest Financial less than or equal to a given Financial.
*
* In other words: Round DOWN
*
* Example of Floor to fraction:2
* * 0.595 -> 0.59
* * 0.555 -> 0.55
* * 0.554 -> 0.55
* * -0.595 -> -0.60
* * -0.555 -> -0.56
* * -0.554 -> -0.56
*/
Floor = "Floor",
/**
* Round to the Financial rounded to the nearest Financial.
*
* In other words: Round classic
*
* Example of Round to fraction:2
* * 0.595 -> 0.60
* * 0.555 -> 0.56
* * 0.554 -> 0.55
* * -0.595 -> -0.60
* * -0.555 -> -0.55
* * -0.554 -> -0.55
*/
Round = "Round",
/**
* Round to the Financial by removing fractional digits.
*
* Works same as Floor in positive range.
*
* Works same as Ceil in negative range
*
* Example of Trunc to fraction:2
* * 0.595 -> 0.59
* * 0.555 -> 0.55
* * 0.554 -> 0.55
* * -0.595 -> -0.59
* * -0.555 -> -0.55
* * -0.554 -> -0.55
*/
Trunc = "Trunc"
}
export class FDecimalRoundModeUnreachableException extends FException {
public constructor(roundMode: never) {
super(`Unsupported round mode: ${roundMode}`);
}
}
export interface FDecimalBackend {
readonly settings: FDecimalSettings;
/**
* Analog of Math.abs()
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs
*/
abs(value: FDecimal): FDecimal;
add(left: FDecimal, right: FDecimal): FDecimal;
divide(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal;
equals(left: FDecimal, right: FDecimal): boolean;
fromFloat(value: number, roundMode?: FDecimalRoundMode): FDecimal;
fromInt(value: number): FDecimal;
gt(left: FDecimal, right: FDecimal): boolean;
gte(left: FDecimal, right: FDecimal): boolean;
inverse(value: FDecimal): FDecimal;
isDecimal(test: any): test is FDecimal;
isNegative(test: FDecimal): boolean;
isPositive(test: FDecimal): boolean;
isZero(test: FDecimal): boolean;
lt(left: FDecimal, right: FDecimal): boolean;
lte(left: FDecimal, right: FDecimal): boolean;
/**
* Analog of Math.max()
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max
*/
max(left: FDecimal, right: FDecimal): FDecimal;
/**
* Analog of Math.min()
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min
*/
min(left: FDecimal, right: FDecimal): FDecimal;
mod(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal;
multiply(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal;
parse(value: string): FDecimal;
round(value: FDecimal, fractionDigits: FDecimalFractionDigits, roundMode?: FDecimalRoundMode): FDecimal;
subtract(left: FDecimal, right: FDecimal): FDecimal;
toNumber(value: FDecimal): number;
toString(value: FDecimal): string;
}
export interface FDecimalSettings {
readonly decimalSeparator: string;
readonly fractionalDigits: number;
readonly roundMode: FDecimalRoundMode;
}
export namespace FDecimal {
/**
* @deprecated Use FDecimalFraction instead
*/
export type FractionDigits = FDecimalFractionDigits;
/**
* @deprecated Use FDecimalFraction instead
*/
export namespace FractionDigits {
/**
* @deprecated Use isDecimalFraction instead
*/
export const isDecimalFractionDigits: (test: number) => test is FDecimalFractionDigits = isDecimalFraction;
/**
* @deprecated Use verifyDecimalFraction instead
*/
export const verifyFraction: (test: FDecimalFractionDigits) => asserts test is FDecimalFractionDigits = verifyDecimalFraction;
}
/**
* @deprecated Use FDecimalBackend instead
*/
export type Backend = FDecimalBackend;
/**
* @deprecated Use FDecimalRoundMode instead
*/
export type RoundMode = FDecimalRoundMode;
/**
* Use FDecimalSettings instead
*/
export type Settings = FDecimalSettings;
/**
* @deprecated Use FDecimalRoundModeUnreachableException instead
*/
export type UnreachableRoundMode = FDecimalRoundModeUnreachableException;
}
export class FDecimalBase<TInstance, TBackend extends FDecimalBackend> implements FDecimal {
private readonly _instance: TInstance;
private readonly _backend: TBackend;
public constructor(instance: TInstance, backend: TBackend) {
this._instance = instance;
this._backend = backend;
}
public get instance(): TInstance {
return this._instance;
}
public abs(): FDecimal {
return this._backend.abs(this);
}
public add(value: FDecimal): FDecimal {
return this._backend.add(this, value);
}
public divide(value: FDecimal, roundMode?: FDecimalRoundMode): FDecimal {
return this._backend.divide(this, value, roundMode);
}
public equals(value: FDecimal): boolean {
return this._backend.equals(this, value);
}
public inverse(): FDecimal {
return this._backend.inverse(this);
}
public isNegative(): boolean {
return this._backend.isNegative(this);
}
public isPositive(): boolean {
return this._backend.isPositive(this);
}
public isZero(): boolean {
return this._backend.isZero(this);
}
public mod(value: FDecimal): FDecimal {
return this._backend.mod(this, value);
}
public multiply(value: FDecimal, roundMode?: FDecimalRoundMode): FDecimal {
return this._backend.multiply(this, value, roundMode);
}
public round(fractionDigits: FDecimalFractionDigits, roundMode?: FDecimalRoundMode): FDecimal {
return this._backend.round(this, fractionDigits, roundMode);
}
public subtract(value: FDecimal): FDecimal {
return this._backend.subtract(this, value);
}
public toNumber(): number {
return this._backend.toNumber(this);
}
public toString(): string {
return this._backend.toString(this);
}
public toJSON(): string {
return this.toString();
}
protected get backend(): TBackend { return this._backend; }
}
export class FDecimalBackendNumber implements FDecimalBackend {
private static verifyInstance(test: FDecimal): asserts test is _FDecimalNumber {
if (test instanceof _FDecimalNumber) { 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 > 20) {
throw new FExceptionArgument("Range 0..20 overflow", "fractionalDigits");
}
this.settings = {
decimalSeparator: ".",
fractionalDigits,
roundMode
};
}
public abs(value: FDecimal): FDecimal {
FDecimalBackendNumber.verifyInstance(value);
return new _FDecimalNumber(Math.abs(value.instance), this);
}
public add(left: FDecimal, right: FDecimal): FDecimal {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return new _FDecimalNumber(left.instance + right.instance, this);
}
public divide(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode | undefined): FDecimal {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
const divideValueInstance: number = left.instance / right.instance;
const roundedDivideValueInstance = this._round(divideValueInstance, this.settings.fractionalDigits, roundMode);
return new _FDecimalNumber(roundedDivideValueInstance, this);
}
public equals(left: FDecimal, right: FDecimal): boolean {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return left.instance === right.instance;
}
public fromFloat(value: number, _?: FDecimalRoundMode | undefined): FDecimal {
return new _FDecimalNumber(value, this);
}
public fromInt(value: number): FDecimal {
return new _FDecimalNumber(value, this);
}
public gt(left: FDecimal, right: FDecimal): boolean {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return left.instance > right.instance;
}
public gte(left: FDecimal, right: FDecimal): boolean {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return left.instance >= right.instance;
}
public inverse(value: FDecimal): FDecimal {
FDecimalBackendNumber.verifyInstance(value);
return new _FDecimalNumber(value.instance * -1, this);
}
public isDecimal(test: any): test is FDecimal {
return test instanceof _FDecimalNumber;
}
public isNegative(test: FDecimal): boolean {
FDecimalBackendNumber.verifyInstance(test);
return test.instance < 0;
}
public isPositive(test: FDecimal): boolean {
FDecimalBackendNumber.verifyInstance(test);
return test.instance > 0;
}
public isZero(test: FDecimal): boolean {
FDecimalBackendNumber.verifyInstance(test);
return test.instance == 0;
}
public lt(left: FDecimal, right: FDecimal): boolean {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return left.instance < right.instance;
}
public lte(left: FDecimal, right: FDecimal): boolean {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return left.instance <= right.instance;
}
public max(left: FDecimal, right: FDecimal): FDecimal {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return new _FDecimalNumber(Math.max(left.instance, right.instance), this);
}
public min(left: FDecimal, right: FDecimal): FDecimal {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return new _FDecimalNumber(Math.min(left.instance, right.instance), this);
}
public mod(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode): FDecimal {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
const modValueInstance: number = left.instance % right.instance;
const roundedModValueInstance = this._round(modValueInstance, this.settings.fractionalDigits, roundMode);
return new _FDecimalNumber(roundedModValueInstance, this);
}
public multiply(left: FDecimal, right: FDecimal, roundMode?: FDecimalRoundMode | undefined): FDecimal {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
const multiplyValueInstance: number = left.instance * right.instance;
const roundedMultiplyValueInstance = this._round(multiplyValueInstance, this.settings.fractionalDigits, roundMode);
return new _FDecimalNumber(roundedMultiplyValueInstance, this);
}
public parse(value: string): FDecimal {
return new _FDecimalNumber(Number.parseFloat(value), this);
}
public round(value: FDecimal, fractionDigits: number, roundMode?: FDecimalRoundMode | undefined): FDecimal {
FDecimalBackendNumber.verifyInstance(value);
const roundedValueInstance = this._round(value.instance, fractionDigits, roundMode);
return new _FDecimalNumber(roundedValueInstance, this);
}
private _round(valueInstance: number, fractionDigits: number, roundMode?: FDecimalRoundMode | undefined): number {
class UnreachableRoundModeException extends FExceptionInvalidOperation {
public constructor(_: never) { super(); }
}
if (roundMode === undefined) {
roundMode = this.settings.roundMode;
}
const powValue = Math.pow(10, fractionDigits + 1);
const powedValueInstance = valueInstance * powValue;
let powedRoundedValueInstance: number;
switch (roundMode) {
case FDecimalRoundMode.Ceil:
powedRoundedValueInstance = Math.ceil(powedValueInstance); break;
case FDecimalRoundMode.Floor:
powedRoundedValueInstance = Math.floor(powedValueInstance); break;
case FDecimalRoundMode.Round:
powedRoundedValueInstance = Math.round(powedValueInstance); break;
case FDecimalRoundMode.Trunc:
powedRoundedValueInstance = Math.trunc(powedValueInstance); break;
default:
throw new UnreachableRoundModeException(roundMode);
}
const roundedValueInstance: number = powedRoundedValueInstance / powValue;
return roundedValueInstance;
}
public subtract(left: FDecimal, right: FDecimal): FDecimal {
FDecimalBackendNumber.verifyInstance(left);
FDecimalBackendNumber.verifyInstance(right);
return new _FDecimalNumber(left.instance - right.instance, this);
}
public toNumber(value: FDecimal): number {
FDecimalBackendNumber.verifyInstance(value);
return value.instance;
}
public toString(value: FDecimal): string {
FDecimalBackendNumber.verifyInstance(value);
return value.instance.toFixed(this.settings.fractionalDigits);
}
}
class _FDecimalNumber extends FDecimalBase<number, FDecimalBackendNumber> { }