decimal128
Version:
Partial implementation of IEEE 754 Decimal128 decimal floating-point numbers
862 lines (861 loc) • 27.7 kB
JavaScript
/**
* 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 { 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);
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, preferredQuantum) {
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) {
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.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) {
if (x === "NaN" || x === "Infinity" || x === "-Infinity") {
return x; // no further validation needed
}
let val = x;
let v = val.cohort;
let q = val.quantum;
let d = new Decimal({ cohort: v, quantum: q });
return adjustDecimal128(d);
}
function handleDecimalNotation(s) {
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 {
constructor(n) {
this.d = undefined;
this._isNaN = false;
this._isFinite = true;
this._isNegative = false;
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;
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;
}
}
isNaN() {
return this._isNaN;
}
isFinite() {
return this._isFinite;
}
isNegative() {
return this._isNegative;
}
cohort() {
let d = this.d;
return d.cohort;
}
quantum() {
let d = this.d;
return d.quantum;
}
isZero() {
if (this.isNaN()) {
return false;
}
if (!this.isFinite()) {
return false;
}
let v = this.cohort();
return v === "0" || v === "-0";
}
exponent() {
let mantissa = this.mantissa();
let mantissaQuantum = mantissa.quantum();
let ourQuantum = this.quantum();
return ourQuantum - mantissaQuantum;
}
mantissa() {
if (this.isZero()) {
throw new RangeError("Zero does not have a mantissa");
}
if (this.isNegative()) {
return this.negate().mantissa().negate();
}
let x = 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;
}
scale10(n) {
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();
return new Decimal128(new Decimal({ cohort: v.scale10(n), quantum: q + n }));
}
coefficient() {
let d = this.d;
return d.coefficient();
}
emitExponential() {
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;
}
emitDecimal() {
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() {
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) {
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) {
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) {
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;
}
isInteger() {
let s = this.toString();
let [_, rhs] = s.split(/[.]/);
if (rhs === undefined) {
return true;
}
return !!rhs.match(/^0+$/);
}
toBigInt() {
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() {
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) {
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();
let theirCohort = x.cohort();
return ourCohort.cmp(theirCohort);
}
abs() {
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) {
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();
let theirCohort = x.cohort();
let ourQuantum = this.quantum();
let theirQuantum = x.quantum();
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) {
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();
let theirCohort = x.cohort();
let ourExponent = this.quantum();
let theirExponent = x.quantum();
let difference = 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) {
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();
let theirCohort = x.cohort();
let ourQuantum = this.quantum();
let theirQuantum = x.quantum();
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,
}));
}
clone() {
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) {
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 = 0, mode = ROUNDING_MODE_HALF_EVEN) {
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();
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() {
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.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) {
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() {
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() {
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() {
if (this.isZero() || this.isSubnormal()) {
return NORMAL_EXPONENT_MIN;
}
return this.exponent();
}
scaledSignificand() {
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();
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");
};