UNPKG

decimal128

Version:

Partial implementation of IEEE 754 Decimal128 decimal floating-point numbers

1,188 lines (939 loc) 30.3 kB
/** * decimal128.js -- Decimal128 implementation in JavaScript * * The purpose of this module is to provide a userland implementation of * IEEE 758 Decimal128, which are exact decimal floating point numbers fit into * 128 bits. This library provides basic arithmetic operations (addition, multiplication). * It's main purpose is to help gather data and experience about using Decimal128 * in JavaScript programs. Speed is not a concern; the main goal is to simply * make Decimal128 values available in some form in JavaScript. In the future, * JavaScript may get exact decimal numbers as a built-in data type, which will * surely be much faster than what this library can provide. * * @author Jesse Alama <jesse@igalia.com> */ import JSBI from "jsbi"; import { RoundingMode, ROUNDING_MODES, ROUNDING_MODE_HALF_EVEN, ROUNDING_MODE_TRUNCATE, } from "./common.mjs"; import { Rational } from "./Rational.mjs"; import { Decimal } from "./Decimal.mjs"; const EXPONENT_MIN = -6176; const NORMAL_EXPONENT_MIN = -6143; const EXPONENT_MAX = 6111; const NORMAL_EXPONENT_MAX = 6144; const MAX_SIGNIFICANT_DIGITS = 34; const bigTen = JSBI.BigInt(10); type NaNValue = "NaN"; type InfiniteValue = "Infinity" | "-Infinity"; type FiniteValue = Decimal; type Decimal128Value = NaNValue | InfiniteValue | FiniteValue; const NAN = "NaN"; const POSITIVE_INFINITY = "Infinity"; const NEGATIVE_INFINITY = "-Infinity"; const TEN_MAX_EXPONENT = JSBI.exponentiate( bigTen, JSBI.BigInt(MAX_SIGNIFICANT_DIGITS) ); function pickQuantum( d: "0" | "-0" | Rational, preferredQuantum: number ): number { if (preferredQuantum < EXPONENT_MIN) { return EXPONENT_MIN; } if (preferredQuantum > EXPONENT_MAX) { return EXPONENT_MAX; } if (d === "0" || d === "-0") { return preferredQuantum; } return preferredQuantum; } function adjustDecimal128(d: Decimal): Decimal128Value { let v = d.cohort; let q = d.quantum; if (v === "0" || v === "-0") { return new Decimal({ cohort: v, quantum: pickQuantum(v, q) }); } if (d.isNegative()) { let adjusted = adjustDecimal128(d.negate()); if (adjusted === "Infinity") { return "-Infinity"; } return (adjusted as Decimal).negate(); } let coef = d.coefficient(); if ( JSBI.LE(coef, TEN_MAX_EXPONENT) && JSBI.LE(EXPONENT_MIN, q) && JSBI.LE(q, EXPONENT_MAX) ) { return d; } let renderedCohort = v.toFixed(Infinity); let [integerPart, _] = renderedCohort.split(/[.]/); if (integerPart === "0") { integerPart = ""; } if (integerPart.length > MAX_SIGNIFICANT_DIGITS) { return "Infinity"; } let scaledSig = v.scale10(MAX_SIGNIFICANT_DIGITS - integerPart.length); let rounded = scaledSig.round(0, "halfEven"); let rescaled = rounded.scale10( 0 - MAX_SIGNIFICANT_DIGITS + integerPart.length ); if (rescaled.isZero()) { return new Decimal({ cohort: "0", quantum: pickQuantum("0", q) }); } let rescaledAsString = rescaled.toFixed(Infinity); return new Decimal(rescaledAsString); } function validateConstructorData(x: Decimal128Value): Decimal128Value { if (x === "NaN" || x === "Infinity" || x === "-Infinity") { return x; // no further validation needed } let val = x as FiniteValue; let v = val.cohort; let q = val.quantum; let d = new Decimal({ cohort: v, quantum: q }); return adjustDecimal128(d); } function handleDecimalNotation(s: string): Decimal128Value { if (s.match(/^[+]/)) { return handleDecimalNotation(s.substring(1)); } if (s.match(/_/)) { return handleDecimalNotation(s.replace(/_/g, "")); } if ("" === s) { throw new SyntaxError("Empty string not permitted"); } if ("." === s) { throw new SyntaxError("Lone decimal point not permitted"); } if ("-" === s) { throw new SyntaxError("Lone minus sign not permitted"); } if ("-." === s) { throw new SyntaxError("Lone minus sign and period not permitted"); } if (s === "NaN") { return "NaN"; } if (s.match(/^-?Infinity$/)) { return s.match(/^-/) ? "-Infinity" : "Infinity"; } return new Decimal(s); } export class Decimal128 { private readonly d: Decimal | undefined = undefined; private readonly _isNaN: boolean = false; private readonly _isFinite: boolean = true; private readonly _isNegative: boolean = false; constructor(n: string | number | bigint | JSBI | Decimal) { let data; if ("object" === typeof n) { if (n instanceof JSBI) { // Note: when using babel-plugin-transform-jsbi-to-bigint, // the condition above may get transpiled into "typeof n === 'bigint'", // causing the condition above to always be false. // This is fine as we will be hanlding bigint below. data = handleDecimalNotation(n.toString()); } else { data = n; } } else { let s: string; if ("number" === typeof n) { s = Object.is(n, -0) ? "-0" : n.toString(); } else if ("bigint" === typeof n) { s = n.toString(); } else { s = n; } data = handleDecimalNotation(s); } data = validateConstructorData(data); if (data == "NaN") { this._isNaN = true; } else if (data == "Infinity") { this._isFinite = false; } else if (data == "-Infinity") { this._isFinite = false; this._isNegative = true; } else { let v = data.cohort; if (v === "-0") { this._isNegative = true; } else if (v === "0") { this._isNegative = false; } else { this._isNegative = v.isNegative; } this.d = data; } } public isNaN(): boolean { return this._isNaN; } public isFinite(): boolean { return this._isFinite; } public isNegative(): boolean { return this._isNegative; } private cohort(): "0" | "-0" | Rational { let d = this.d as Decimal; return d.cohort; } private quantum(): number { let d = this.d as Decimal; return d.quantum; } private isZero(): boolean { if (this.isNaN()) { return false; } if (!this.isFinite()) { return false; } let v = this.cohort(); return v === "0" || v === "-0"; } public exponent(): number { let mantissa = this.mantissa(); let mantissaQuantum = mantissa.quantum(); let ourQuantum = this.quantum(); return ourQuantum - mantissaQuantum; } public mantissa(): Decimal128 { if (this.isZero()) { throw new RangeError("Zero does not have a mantissa"); } if (this.isNegative()) { return this.negate().mantissa().negate(); } let x: Decimal128 = this; let decimalOne = new Decimal128("1"); let decimalTen = new Decimal128("10"); while (JSBI.LE(0, x.cmp(decimalTen))) { x = x.scale10(-1); } while (x.cmp(decimalOne) === -1) { x = x.scale10(1); } return x; } public scale10(n: number): Decimal128 { if (this.isNaN()) { throw new RangeError("NaN cannot be scaled"); } if (!this.isFinite()) { throw new RangeError("Infinity cannot be scaled"); } if (!Number.isInteger(n)) { throw new TypeError("Argument must be an integer"); } if (n === 0) { return this.clone(); } let v = this.cohort(); if (v === "0" || v === "-0") { return this.clone(); } let q = this.quantum() as number; return new Decimal128( new Decimal({ cohort: v.scale10(n), quantum: q + n }) ); } private coefficient(): JSBI { let d = this.d as Decimal; return d.coefficient(); } private emitExponential(): string { let v = this.cohort(); let q = this.quantum(); let p = this._isNegative ? "-" : ""; if (v === "0" || v === "-0") { return v + "e" + (q < 0 ? "-" : "+") + Math.abs(q); } let m = this.mantissa(); let e = this.exponent(); let mAsString = m.toFixed({ digits: Infinity }); let expPart = (e < 0 ? "-" : "+") + Math.abs(e); return p + mAsString + "e" + expPart; } private emitDecimal(): string { let v = this.cohort(); let q = this.quantum(); if (v === "0") { if (q < 0) { return "0" + "." + "0".repeat(0 - q); } return "0"; } if (v === "-0") { if (q < 0) { return "-0" + "." + "0".repeat(0 - q); } return "-0"; } let c = v.scale10(0 - q); let s = c.numerator.toString(); let p = this._isNegative ? "-" : ""; if (q > 0) { return p + s + "0".repeat(q); } if (q === 0) { return p + s; } if (s.length < Math.abs(q)) { let numZeroesNeeded = Math.abs(q) - s.length; return p + "0." + "0".repeat(numZeroesNeeded) + s; } let integerPart = s.substring(0, s.length + q); let fractionalPart = s.substring(s.length + q); if (integerPart === "") { integerPart = "0"; } return p + integerPart + "." + fractionalPart; } /** * Returns a digit string representing this Decimal128. */ toString(): string { if (this.isNaN()) { return NAN; } if (!this.isFinite()) { return (this.isNegative() ? "-" : "") + POSITIVE_INFINITY; } let asDecimalString = this.emitDecimal(); if (asDecimalString.match(/[.]/)) { asDecimalString = asDecimalString.replace(/0+$/, ""); if (asDecimalString.match(/[.]$/)) { asDecimalString = asDecimalString.substring( 0, asDecimalString.length - 1 ); } } return asDecimalString; } toFixed(opts?: { digits?: number }): string { if (undefined === opts) { return this.toString(); } if ("object" !== typeof opts) { throw new TypeError("Argument must be an object"); } if (undefined === opts.digits) { return this.toString(); } let n = opts.digits; if (n < 0) { throw new RangeError("Argument must be greater than or equal to 0"); } if (n === Infinity) { return this.emitDecimal(); } if (!Number.isInteger(n)) { throw new RangeError( "Argument must be an integer or positive infinity" ); } if (this.isNaN()) { return NAN; } if (!this.isFinite()) { return this.isNegative() ? "-" + POSITIVE_INFINITY : POSITIVE_INFINITY; } let rounded = this.round(n); let roundedRendered = rounded.emitDecimal(); if (roundedRendered.match(/[.]/)) { let [lhs, rhs] = roundedRendered.split(/[.]/); return lhs + "." + rhs.substring(0, n); } return roundedRendered; } toPrecision(opts?: { digits?: number }): string { if (undefined === opts) { return this.toString(); } if ("object" !== typeof opts) { throw new TypeError("Argument must be an object"); } if (undefined === opts.digits) { return this.toString(); } let n = opts.digits; if (JSBI.LE(n, 0)) { throw new RangeError("Argument must be positive"); } if (!Number.isInteger(n)) { throw new RangeError("Argument must be an integer"); } if (this.isNaN()) { return "NaN"; } if (!this.isFinite()) { return (this.isNegative() ? "-" : "") + "Infinity"; } let s = this.abs().emitDecimal(); let [lhs, rhs] = s.split(/[.]/); let p = this.isNegative() ? "-" : ""; if (JSBI.LE(n, lhs.length)) { if (lhs.length === n) { return p + lhs; } return p + s.substring(0, n) + "e+" + `${lhs.length - n + 1}`; } if (JSBI.LE(n, lhs.length + rhs.length)) { let rounded = this.round(n - lhs.length); return rounded.emitDecimal(); } return p + lhs + "." + rhs + "0".repeat(n - lhs.length - rhs.length); } toExponential(opts?: { digits?: number }): string { if (this.isNaN()) { return "NaN"; } if (!this.isFinite()) { return (this.isNegative() ? "-" : "") + "Infinity"; } if (undefined === opts) { return this.emitExponential(); } if ("object" !== typeof opts) { throw new TypeError("Argument must be an object"); } if (undefined === opts.digits) { return this.emitExponential(); } let n = opts.digits; if (JSBI.LE(n, 0)) { throw new RangeError("Argument must be positive"); } if (!Number.isInteger(n)) { throw new RangeError("Argument must be an integer"); } let s = this.abs().emitExponential(); let [lhs, rhsWithEsign] = s.split(/[.]/); let [rhs, exp] = rhsWithEsign.split(/[eE]/); let p = this.isNegative() ? "-" : ""; if (JSBI.LE(rhs.length, n)) { return p + lhs + "." + rhs + "0".repeat(n - rhs.length) + "e" + exp; } return p + lhs + "." + rhs.substring(0, n) + "e" + exp; } private isInteger(): boolean { let s = this.toString(); let [_, rhs] = s.split(/[.]/); if (rhs === undefined) { return true; } return !!rhs.match(/^0+$/); } toBigInt(): JSBI { if (this.isNaN()) { throw new RangeError("NaN cannot be converted to a BigInt"); } if (!this.isFinite()) { throw new RangeError("Infinity cannot be converted to a BigInt"); } if (!this.isInteger()) { throw new RangeError( "Non-integer decimal cannot be converted to a BigInt" ); } return JSBI.BigInt(this.toString()); } toNumber(): number { if (this.isNaN()) { return NaN; } if (!this.isFinite()) { if (this.isNegative()) { return -Infinity; } return Infinity; } return Number(this.toString()); } /** * Compare two values. Return * * * NaN if either argument is a decimal NaN * + -1 if the mathematical value of this decimal is strictly less than that of the other, * + 0 if the mathematical values are equal, and * + 1 otherwise. * * @param x */ cmp(x: Decimal128): number { if (this.isNaN() || x.isNaN()) { return NaN; } if (!this.isFinite()) { if (!x.isFinite()) { if (this.isNegative() === x.isNegative()) { return 0; } return this.isNegative() ? -1 : 1; } if (this.isNegative()) { return -1; } return 1; } if (!x.isFinite()) { return x.isNegative() ? 1 : -1; } if (this.isZero()) { if (x.isZero()) { return 0; } return x.isNegative() ? 1 : -1; } let ourCohort = this.cohort() as Rational; let theirCohort = x.cohort() as Rational; return ourCohort.cmp(theirCohort); } abs(): Decimal128 { if (this.isNaN()) { return new Decimal128(NAN); } if (!this.isFinite()) { if (this.isNegative()) { return this.negate(); } return this.clone(); } if (this.isNegative()) { return this.negate(); } return this.clone(); } /** * Add this Decimal128 value to one or more Decimal128 values. * * @param x */ add(x: Decimal128): Decimal128 { if (this.isNaN() || x.isNaN()) { return new Decimal128(NAN); } if (!this.isFinite()) { if (!x.isFinite()) { if (this.isNegative() === x.isNegative()) { return x.clone(); } return new Decimal128(NAN); } return this.clone(); } if (!x.isFinite()) { return x.clone(); } if (this.isNegative() && x.isNegative()) { return this.negate().add(x.negate()).negate(); } if (this.isZero()) { return x.clone(); } if (x.isZero()) { return this.clone(); } let ourCohort = this.cohort() as Rational; let theirCohort = x.cohort() as Rational; let ourQuantum = this.quantum() as number; let theirQuantum = x.quantum() as number; let sum = Rational.add(ourCohort, theirCohort); let preferredQuantum = Math.min(ourQuantum, theirQuantum); if (sum.isZero()) { if (this._isNegative) { return new Decimal128("-0"); } return new Decimal128("0"); } return new Decimal128( new Decimal({ cohort: sum, quantum: pickQuantum(sum, preferredQuantum), }) ); } /** * Subtract another Decimal128 value from one or more Decimal128 values. * * @param x */ subtract(x: Decimal128): Decimal128 { if (this.isNaN() || x.isNaN()) { return new Decimal128(NAN); } if (!this.isFinite()) { if (!x.isFinite()) { if (this.isNegative() === x.isNegative()) { return new Decimal128(NAN); } return this.clone(); } return this.clone(); } if (!x.isFinite()) { return x.negate(); } if (x.isNegative()) { return this.add(x.negate()); } if (this.isZero()) { return x.negate(); } if (x.isZero()) { return this.clone(); } let ourCohort = this.cohort() as Rational; let theirCohort = x.cohort() as Rational; let ourExponent = this.quantum() as number; let theirExponent = x.quantum() as number; let difference: "0" | "-0" | Rational = Rational.subtract( ourCohort, theirCohort ); let preferredQuantum = Math.min(ourExponent, theirExponent); if (difference.isZero()) { difference = "0"; } return new Decimal128( new Decimal({ cohort: difference, quantum: pickQuantum(difference, preferredQuantum), }) ); } /** * Multiply this Decimal128 value by an array of other Decimal128 values. * * If no arguments are given, return this value. * * @param x */ multiply(x: Decimal128): Decimal128 { if (this.isNaN() || x.isNaN()) { return new Decimal128(NAN); } if (!this.isFinite()) { if (x.isZero()) { return new Decimal128(NAN); } if (this.isNegative() === x.isNegative()) { return new Decimal128(POSITIVE_INFINITY); } return new Decimal128(NEGATIVE_INFINITY); } if (!x.isFinite()) { if (this.isZero()) { return new Decimal128(NAN); } if (this.isNegative() === x.isNegative()) { return new Decimal128(POSITIVE_INFINITY); } return new Decimal128(NEGATIVE_INFINITY); } if (this.isNegative()) { return this.negate().multiply(x).negate(); } if (x.isNegative()) { return this.multiply(x.negate()).negate(); } let ourCohort = this.cohort() as Rational; let theirCohort = x.cohort() as Rational; let ourQuantum = this.quantum() as number; let theirQuantum = x.quantum() as number; let preferredQuantum = ourQuantum + theirQuantum; if (this.isZero()) { return new Decimal128( new Decimal({ cohort: this.cohort(), quantum: preferredQuantum, }) ); } if (x.isZero()) { return new Decimal128( new Decimal({ cohort: x.cohort(), quantum: preferredQuantum, }) ); } let product = Rational.multiply(ourCohort, theirCohort); let actualQuantum = pickQuantum(product, preferredQuantum); return new Decimal128( new Decimal({ cohort: product, quantum: actualQuantum, }) ); } private clone(): Decimal128 { if (this.isNaN()) { return new Decimal128(NAN); } if (!this.isFinite()) { return new Decimal128( this.isNegative() ? NEGATIVE_INFINITY : POSITIVE_INFINITY ); } return new Decimal128( new Decimal({ cohort: this.cohort(), quantum: this.quantum() }) ); } /** * Divide this Decimal128 value by another Decimal128 value. * * @param x */ divide(x: Decimal128): Decimal128 { if (this.isNaN() || x.isNaN()) { return new Decimal128(NAN); } if (x.isZero()) { return new Decimal128(NAN); } if (this.isZero()) { return this.clone(); } if (!this.isFinite()) { if (!x.isFinite()) { return new Decimal128(NAN); } if (this.isNegative() === x.isNegative()) { return new Decimal128(POSITIVE_INFINITY); } if (this.isNegative()) { return this.clone(); } return new Decimal128(NEGATIVE_INFINITY); } if (!x.isFinite()) { if (this.isNegative() === x.isNegative()) { return new Decimal128("0"); } return new Decimal128("-0"); } if (this.isNegative()) { return this.negate().divide(x).negate(); } if (x.isNegative()) { return this.divide(x.negate()).negate(); } let adjust = 0; let dividendCoefficient = this.coefficient(); let divisorCoefficient = x.coefficient(); if (JSBI.notEqual(dividendCoefficient, JSBI.BigInt(0))) { while (JSBI.LT(dividendCoefficient, divisorCoefficient)) { dividendCoefficient = JSBI.multiply( dividendCoefficient, JSBI.BigInt(10) ); adjust++; } } while ( JSBI.GT( dividendCoefficient, JSBI.multiply(divisorCoefficient, JSBI.BigInt(10)) ) ) { divisorCoefficient = JSBI.multiply( divisorCoefficient, JSBI.BigInt(10) ); adjust--; } let resultCoefficient = JSBI.BigInt(0); let done = false; while (!done) { while (JSBI.LE(divisorCoefficient, dividendCoefficient)) { dividendCoefficient = JSBI.subtract( dividendCoefficient, divisorCoefficient ); resultCoefficient = JSBI.add(resultCoefficient, JSBI.BigInt(1)); } if ( (JSBI.equal(dividendCoefficient, JSBI.BigInt(0)) && adjust >= 0) || resultCoefficient.toString().length > MAX_SIGNIFICANT_DIGITS ) { done = true; } else { resultCoefficient = JSBI.multiply( resultCoefficient, JSBI.BigInt(10) ); dividendCoefficient = JSBI.multiply( dividendCoefficient, JSBI.BigInt(10) ); adjust++; } } let ourExponent = this.quantum(); let theirExponent = x.quantum(); let resultExponent = ourExponent - (theirExponent + adjust); return new Decimal128(`${resultCoefficient}E${resultExponent}`); } /** * * @param numDecimalDigits * @param {RoundingMode} mode (default: ROUNDING_MODE_DEFAULT) */ round( numDecimalDigits: number = 0, mode: RoundingMode = ROUNDING_MODE_HALF_EVEN ): Decimal128 { if (!ROUNDING_MODES.includes(mode)) { throw new RangeError(`Invalid rounding mode "${mode}"`); } if (this.isNaN() || !this.isFinite()) { return this.clone(); } if (this.isZero()) { return this.clone(); } let v = this.cohort() as Rational; let roundedV = v.round(numDecimalDigits, mode); if (roundedV.isZero()) { return new Decimal128( new Decimal({ cohort: v.isNegative ? "-0" : "0", quantum: 0 - numDecimalDigits, }) ); } return new Decimal128( new Decimal({ cohort: roundedV, quantum: 0 - numDecimalDigits }) ); } negate(): Decimal128 { if (this.isNaN()) { return this.clone(); } if (!this.isFinite()) { return new Decimal128( this.isNegative() ? POSITIVE_INFINITY : NEGATIVE_INFINITY ); } let v = this.cohort(); if (v === "0") { return new Decimal128( new Decimal({ cohort: "-0", quantum: this.quantum() }) ); } if (v === "-0") { return new Decimal128( new Decimal({ cohort: "0", quantum: this.quantum() }) ); } return new Decimal128( new Decimal({ cohort: (v as Rational).negate(), quantum: this.quantum(), }) ); } /** * Return the remainder of this Decimal128 value divided by another Decimal128 value. * * @param d * @throws RangeError If argument is zero */ remainder(d: Decimal128): Decimal128 { if (this.isNaN() || d.isNaN()) { return new Decimal128(NAN); } if (this.isNegative()) { return this.negate().remainder(d).negate(); } if (d.isNegative()) { return this.remainder(d.negate()); } if (!this.isFinite()) { return new Decimal128(NAN); } if (!d.isFinite()) { return this.clone(); } if (d.isZero()) { return new Decimal128(NAN); } if (this.cmp(d) === -1) { return this.clone(); } let q = this.divide(d).round(0, ROUNDING_MODE_TRUNCATE); return this.subtract(d.multiply(q)); } isNormal(): boolean { if (this.isNaN()) { throw new RangeError("Cannot determine whether NaN is normal"); } if (!this.isFinite()) { throw new RangeError( "Only finite numbers can be said to be normal or not" ); } if (this.isZero()) { throw new RangeError( "Only non-zero numbers can be said to be normal or not" ); } let exp = this.exponent(); return exp >= NORMAL_EXPONENT_MIN && exp <= NORMAL_EXPONENT_MAX; } isSubnormal(): boolean { if (this.isNaN()) { throw new RangeError("Cannot determine whether NaN is subnormal"); } if (!this.isFinite()) { throw new RangeError( "Only finite numbers can be said to be subnormal or not" ); } let exp = this.exponent(); return exp < NORMAL_EXPONENT_MIN; } truncatedExponent(): number { if (this.isZero() || this.isSubnormal()) { return NORMAL_EXPONENT_MIN; } return this.exponent(); } scaledSignificand(): JSBI { if (this.isNaN()) { throw new RangeError("NaN does not have a scaled significand"); } if (!this.isFinite()) { throw new RangeError("Infinity does not have a scaled significand"); } if (this.isZero()) { return JSBI.BigInt(0); } let v = this.cohort() as Rational; let te = this.truncatedExponent(); let ss = v.scale10(MAX_SIGNIFICANT_DIGITS - 1 - te); return ss.numerator; } } Decimal128.prototype.valueOf = function () { throw TypeError("Decimal128.prototype.valueOf throws unconditionally"); };