diff --git a/src/decimal128.mts b/src/decimal128.mts index be307c9..9b7858c 100644 --- a/src/decimal128.mts +++ b/src/decimal128.mts @@ -1,165 +1,702 @@ -import { RationalDecimal128 } from "./rationalDecimal128.mjs"; - -/** - * Is this Decimal128 actually an integer? That is: is there nothing after the decimal point? - */ -function isInteger(d: string): boolean { - return new RationalDecimal128(d).isInteger(); -} - /** - * Return the absolute value of this Decimal128 value. + * 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. * - * @param d + * @author Jesse Alama */ -function abs(d: string): string { - return new RationalDecimal128(d).abs().toString(); -} + +import { countSignificantDigits, Digit } from "./common.mjs"; +import { Rational } from "./rational.mjs"; + +const EXPONENT_MIN = -6143; +const EXPONENT_MAX = 6144; +const MAX_SIGNIFICANT_DIGITS = 34; + +const bigTen = BigInt(10); +const bigOne = BigInt(1); +const bigZero = BigInt(0); +const bigTwenty = BigInt(20); /** - * Return a digit string where the digits of this number are cut off after - * a certain number of digits. Rounding may be performed, in case we always round up. + * Normalize a digit string. This means: + * + * + removing any initial zeros + * + removing any trailing zeros + * + rewriting -0 to 0 + * + rewriting 0.0 to 0 + * + * @param s A digit string * - * @param d - * @param n + * @example normalize("000123.456000") // => "123.456" + * @example normalize("000000.000000") // => "0" + * @example normalize("000000.000001") // => "0.000001" + * @example normalize("000000.100000") // => "0.1" */ -function toDecimalPlaces(d: string, n: number): string { - return new RationalDecimal128(d).toDecimalPlaces(n).toString(); +function normalize(s: string): string { + if (s.match(/^-/)) { + let n = normalize(s.substring(1)); + if ("0" === n) { + return "0"; + } + return "-" + n; + } + + let a = s.replace(/^0+/, ""); + let b = a.match(/[.]/) ? a.replace(/0+$/, "") : a; + + if (b.match(/^[.]/)) { + b = "0" + b; + } + + if (b.match(/[.]$/)) { + b = b.substring(0, b.length - 1); + } + + if ("" === b) { + b = "0"; + } + + return b; } -function toExponentialString(d: string): string { - return new RationalDecimal128(d).toExponentialString(); +function shiftDecimalPointLeft(s: string): string { + if (s.match(/^-/)) { + return "-" + shiftDecimalPointLeft(s.substring(1)); + } + + let [lhs, rhs] = s.split(/[.]/); + return lhs + rhs.substring(0, 1) + "." + rhs.substring(1); } -/** - * Return the ceiling of this number. That is: the smallest integer greater than or equal to this number. - */ -function ceil(d: string): string { - return new RationalDecimal128(d).ceil().toString(); +function shiftDecimalPointRight(s: string): string { + if (s.match(/^-/)) { + return "-" + shiftDecimalPointRight(s.substring(1)); + } + + return s.substring(0, s.length - 1) + "." + s.substring(s.length - 1); } -/** - * Return the floor of this number. That is: the largest integer less than or equal to this number. - * - * @param d A Decimal128 value. - */ -function floor(d: string): string { - return new RationalDecimal128(d).floor().toString(); +function roundDigitStringTiesToEven(s: string, n: number): string { + let [lhs, rhs] = s.split("."); + + if (undefined === rhs) { + return lhs; + } + + if (n === 0) { + let digit = parseInt(lhs.substring(lhs.length - 1, lhs.length)); + let nextDigit = nthSignificantDigit("0." + rhs, 0); + + if (nextDigit > 5) { + return propagateCarryFromRight(lhs); + } + + if (nextDigit === 5) { + if (0 === digit % 2) { + // round to even + return lhs; + } + + return propagateCarryFromRight(lhs); + } + + return lhs; + } + + let timesTen = normalize(shiftDecimalPointLeft(s)); + + if (!timesTen.match(/[.]/)) { + return roundDigitStringTiesToEven(s, n - 1); + } + + return shiftDecimalPointRight(roundDigitStringTiesToEven(timesTen, n - 1)); } /** - * Compare two values. Return + * Return the significand of a digit string, assumed to be normalized. + * The returned value is a digit string that has no decimal point, even if the original + * digit string had one. * - * + -1 if this value is strictly less than the other, - * + 0 if they are equal, and - * + 1 otherwise. + * @param s * - * @param x - * @param y + * @example significand("123.456") // => "123456" + * @example significand("0.000123") // => "123" */ -function cmp(x: string, y: string): -1 | 0 | 1 { - return new RationalDecimal128(x).cmp(new RationalDecimal128(y)); +function significand(s: string): string { + if (s.match(/^-/)) { + return significand(s.substring(1)); + } else if (s.match(/^0[.]/)) { + return significand(s.substring(2)); + } else if (s.match(/[.]/)) { + return significand(s.replace(/[.]/, "")); + } else if (s.match(/^0+/)) { + return significand(s.replace(/^0+/, "")); + } else if (s.match(/0+$/)) { + return significand(s.replace(/0+$/, "")); + } else { + return s; + } } /** - * Truncate the decimal part of this number (if any), returning an integer. + * Get the n-th significant digit of a digit string, assumed to be normalized. * - * @param d A Decimal128 value. - * @return {RationalDecimal128} An integer (as a Decimal128 value). + * @param s digit string (assumed to be normalized) + * @param n non-negative integer */ -function truncate(d: string): string { - return new RationalDecimal128(d).truncate().toString(); +function nthSignificantDigit(s: string, n: number): number { + return parseInt(significand(s).charAt(n)); } -/** - * Add this Decimal128 value to one or more Decimal128 values. - * - * @param x - * @param y - */ -function add(x: string, y: string): string { - return new RationalDecimal128(x).add(new RationalDecimal128(y)).toString(); +function cutoffAfterSignificantDigits(s: string, n: number): string { + if (s.match(/^-/)) { + return "-" + cutoffAfterSignificantDigits(s.substring(1), n); + } + + if (s.match(/^0[.]/)) { + return s.substring(0, n + 2); + } + + return s.substring(0, n + 1); } -/** - * Subtract another Decimal128 value from one or more Decimal128 values. - * - * @param x - * @param y - */ -function subtract(x: string, y: string): string { - return new RationalDecimal128(x) - .subtract(new RationalDecimal128(y)) - .toString(); +function propagateCarryFromRight(s: string): string { + let [left, right] = s.split(/[.]/); + + if (undefined === right) { + let lastDigit = parseInt(left.charAt(left.length - 1)); + if (lastDigit === 9) { + if (1 === left.length) { + return "10"; + } + + return ( + propagateCarryFromRight(left.substring(0, left.length - 1)) + + "0" + ); + } + return left.substring(0, left.length - 1) + `${lastDigit + 1}`; + } + + let len = right.length; + + if (1 === len) { + return propagateCarryFromRight(left) + ".0"; + } else { + let finalDigit = parseInt(right.charAt(len - 1)); + + if (9 === finalDigit) { + return ( + propagateCarryFromRight( + left + "." + right.substring(0, len - 1) + ) + "0" + ); + } + + return ( + left + + "." + + right.substring(0, len - 1) + + `${parseInt(right.charAt(len - 1)) + 1}` + ); + } } /** - * Multiply this Decimal128 value by an array of other Decimal128 values. - * - * If no arguments are given, return this value. + * Return the exponent of a digit string, assumed to be normalized. It is the number of digits + * to the left or right that the significand needs to be shifted to recover the original (normalized) + * digit string. * - * @param x - * @param y + * @param s string of digits (assumed to be normalized) */ -function multiply(x: string, y: string): string { - return new RationalDecimal128(x) - .multiply(new RationalDecimal128(y)) - .toString(); +function exponent(s: string): number { + if (s.match(/^-/)) { + return exponent(s.substring(1)); + } else if (s.match(/[.]/)) { + let rhs = s.split(".")[1]; + return 0 - rhs.length; + } else if (s === "0") { + return 0; + } else { + let m = s.match(/0+$/); + if (m) { + return m[0].length; + } else { + return 0; + } + } } -/** - * Divide this Decimal128 value by an array of other Decimal128 values. - * - * Association is to the left: 1/2/3 is (1/2)/3 - * - * If only one argument is given, just return the first argument. - * - * @param x - * @param y - */ -function divide(x: string, y: string): string { - return new RationalDecimal128(x) - .divide(new RationalDecimal128(y)) - .toString(); +type DigitPair = [Digit, Digit]; + +function prepareLeftHandSideForSquareRoot(s: string): DigitPair[] { + let [lhs] = s.split("."); + let numDigits = lhs.length; + + let digitPairs: DigitPair[] = []; + + if (numDigits % 2 === 1) { + let firstDigit = parseInt(lhs.charAt(0)) as Digit; + digitPairs.push([0, firstDigit]); + numDigits--; + } + + for (let i = 0; i < numDigits / 2; i++) { + let d1 = parseInt(lhs.charAt(2 * i)) as Digit; + let d2 = parseInt(lhs.charAt(2 * i + 1)) as Digit; + digitPairs.push([d1, d2]); + } + + return digitPairs; } -function round(x: string, n: number = 0): string { - return new RationalDecimal128(x).round(n).toString(); +function prepareRightHandSideForSquareRoot(s: string): DigitPair[] { + let [_, rhs] = s.split("."); + + if (undefined === rhs) { + return []; + } + + let numDigits = rhs.length; + + let digitPairs: DigitPair[] = []; + + for (let i = 0; i < (numDigits - 1) / 2; i++) { + let d1 = parseInt(rhs.charAt(2 * i)) as Digit; + let d2 = parseInt(rhs.charAt(2 * i + 1)) as Digit; + digitPairs.push([d1, d2]); + } + + if (numDigits % 2 === 1) { + let lastDigit = parseInt(rhs.charAt(numDigits - 1)) as Digit; + digitPairs.push([0, lastDigit]); + } + + return digitPairs; } -/** - * Return the remainder of this Decimal128 value divided by another Decimal128 value. - * - * @param n - * @param d - * @throws RangeError If argument is zero - */ -function remainder(n: string, d: string): string { - return new RationalDecimal128(n) - .remainder(new RationalDecimal128(d)) - .toString(); -} - -function multiplyAndAdd(x: string, y: string, z: string): string { - return new RationalDecimal128(x) - .multiplyAndAdd(new RationalDecimal128(y), new RationalDecimal128(z)) - .toString(); -} - -export const Decimal128 = { - isInteger: isInteger, - abs: abs, - toDecimalPlaces: toDecimalPlaces, - toExponentialString: toExponentialString, - ceil: ceil, - floor: floor, - round: round, - cmp: cmp, - truncate: truncate, - add: add, - subtract: subtract, - multiply: multiply, - divide: divide, - remainder: remainder, - multiplyAndAdd: multiplyAndAdd, -}; +function valueOfDigitPair(digitPair: DigitPair): bigint { + let [d1, d2] = digitPair; + return BigInt(`${d1}${d2}`); +} + +function nextDigit(p: bigint, c: DigitPair, r: bigint): bigint { + let x: bigint = 0n; + let v: bigint = 100n * r + valueOfDigitPair(c); + while (x * (20n * p + x) <= v) { + x = x + 1n; + } + return x - 1n; +} + +interface Decimal128Constructor { + significand: string; + exponent: bigint; + isNegative: boolean; +} + +function isInteger(x: Decimal128Constructor): boolean { + return x.exponent >= bigZero; +} + +function validateConstructorData(x: Decimal128Constructor): void { + let numSigDigits = countSignificantDigits(x.significand); + + if (isInteger(x) && numSigDigits > MAX_SIGNIFICANT_DIGITS) { + throw new RangeError("Integer too large"); + } + + if (x.exponent > EXPONENT_MAX) { + throw new RangeError(`Exponent too big (${exponent})`); + } + + if (x.exponent < EXPONENT_MIN) { + throw new RangeError(`Exponent too small (${exponent})`); + } +} + +function handleExponentialNotation(s: string): Decimal128Constructor { + let [sg, exp] = s.match(/e/) ? s.split("e") : s.split("E"); + + let isNegative = false; + if (sg.match(/^-/)) { + isNegative = true; + sg = sg.substring(1); + } + + if (exp.match(/^[+]/)) { + exp = exp.substring(1); + } + + return { + significand: sg, + exponent: BigInt(exp), + isNegative: isNegative, + }; +} + +function handleDecimalNotation(s: string): Decimal128Constructor { + let normalized = normalize(s.replace(/_/g, "")); + let isNegative = !!normalized.match(/^-/); + let sg = significand(normalized); + let exp = exponent(normalized); + let numSigDigits = countSignificantDigits(normalized); + let isInteger = exp >= 0; + + if (!isInteger && numSigDigits > MAX_SIGNIFICANT_DIGITS) { + let lastDigit = parseInt(sg.charAt(MAX_SIGNIFICANT_DIGITS)); + let penultimateDigit = parseInt(sg.charAt(MAX_SIGNIFICANT_DIGITS - 1)); + if (lastDigit === 5) { + if (penultimateDigit % 2 === 0) { + let rounded = cutoffAfterSignificantDigits( + normalized, + MAX_SIGNIFICANT_DIGITS - 1 + ); + sg = significand(rounded); + exp = exponent(rounded); + } else if (9 === penultimateDigit) { + let rounded = + cutoffAfterSignificantDigits( + propagateCarryFromRight( + cutoffAfterSignificantDigits( + normalized, + MAX_SIGNIFICANT_DIGITS - 1 + ) + ), + MAX_SIGNIFICANT_DIGITS - 2 + ) + "0"; + sg = significand(rounded); + exp = exponent(rounded); + } else { + let rounded = + cutoffAfterSignificantDigits( + normalized, + MAX_SIGNIFICANT_DIGITS - 2 + ) + `${penultimateDigit + 1}`; + sg = significand(rounded); + exp = exponent(rounded); + } + } else if (lastDigit > 5) { + let cutoff = normalize( + cutoffAfterSignificantDigits(normalized, MAX_SIGNIFICANT_DIGITS) + ); + let rounded = normalize(propagateCarryFromRight(cutoff)); + sg = significand(rounded); + exp = exponent(rounded); + } else { + let rounded = normalize( + cutoffAfterSignificantDigits(normalized, MAX_SIGNIFICANT_DIGITS) + ); + sg = significand(rounded); + exp = exponent(rounded); + } + } + + return { + significand: sg, + exponent: BigInt(exp), + isNegative: isNegative, + }; +} + +export class Decimal128 { + public readonly significand: string; + public readonly exponent: number; + public readonly isNegative: boolean; + private readonly digitStrRegExp = + /^-?[0-9]+(?:_?[0-9]+)*(?:[.][0-9](_?[0-9]+)*)?$/; + private readonly exponentRegExp = /^-?[1-9][0-9]*[eE][-+]?[1-9][0-9]*$/; + private readonly rat; + + constructor(n: string) { + let data = undefined; + + if (n.match(this.exponentRegExp)) { + data = handleExponentialNotation(n); + } else if (n.match(this.digitStrRegExp)) { + data = handleDecimalNotation(n); + } else { + throw new SyntaxError(`Illegal number format "${n}"`); + } + + validateConstructorData(data); + + this.significand = data.significand; + this.exponent = parseInt(data.exponent.toString()); // safe because the min & max are less than 10000 + this.isNegative = data.isNegative; + + if ("1" === this.significand) { + // power of ten + if (this.exponent < 0) { + this.rat = new Rational( + bigOne, + BigInt( + (this.isNegative ? "-" : "") + + "1" + + "0".repeat(0 - this.exponent) + ) + ); + } else if (this.exponent === 0) { + this.rat = new Rational( + BigInt(this.isNegative ? -1 : 1), + bigOne + ); + } else { + this.rat = new Rational( + BigInt( + (this.isNegative ? "-" : "") + + "1" + + "0".repeat(this.exponent) + ), + bigOne + ); + } + } else if (this.exponent < 0) { + this.rat = new Rational( + BigInt((this.isNegative ? "-" : "") + this.significand), + bigTen ** BigInt(0 - this.exponent) + ); + } else if (this.exponent === 1) { + this.rat = new Rational( + BigInt((this.isNegative ? "-" : "") + this.significand + "0"), + bigOne + ); + } else { + this.rat = new Rational( + BigInt((this.isNegative ? "-" : "") + this.significand), + bigTen ** BigInt(this.exponent) + ); + } + } + + /** + * Returns a digit string representing this Decimal128. + */ + toString(): string { + return this.rat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS); + } + + /** + * Returns an exponential string representing this Decimal128. + * + */ + toExponentialString(): string { + return ( + (this.isNegative ? "-" : "") + + (this.significand === "" ? "0" : this.significand) + + "E" + + this.exponent + ); + } + + /** + * Is this Decimal128 actually an integer? That is: is there nothing after the decimal point? + */ + isInteger(): boolean { + return this.exponent >= 0; + } + + /** + * Return the absolute value of this Decimal128 value. + * + */ + abs(): Decimal128 { + if (this.isNegative) { + return new Decimal128(this.toString().substring(1)); + } + + return this; + } + + /** + * Return a digit string where the digits of this number are cut off after + * a certain number of digits. Rounding may be performed, in case we always round up. + * + * @param n + */ + toDecimalPlaces(n: number): Decimal128 { + if (!Number.isInteger(n)) { + throw new TypeError("Argument must be an integer"); + } + + if (n < 0) { + throw new RangeError("Argument must be non-negative"); + } + + let s = this.toString(); + let [lhs, rhs] = s.split("."); + + if (undefined === rhs || 0 === n) { + return new Decimal128(lhs); + } + + if (rhs.length <= n) { + return new Decimal128(s); + } + + let penultimateDigit = parseInt(rhs.charAt(n - 1)); + + return new Decimal128( + lhs + "." + rhs.substring(0, n - 1) + `${penultimateDigit + 1}` + ); + } + + /** + * Return the ceiling of this number. That is: the smallest integer greater than or equal to this number. + */ + ceil(): Decimal128 { + if (this.isInteger()) { + return this; + } + + if (this.isNegative) { + return this.truncate(); + } + + return this.add(new Decimal128("1")).truncate(); + } + + /** + * Return the floor of this number. That is: the largest integer less than or equal to this number. + * + */ + floor(): Decimal128 { + return this.truncate(); + } + + /** + * Compare two values. Return + * + * + -1 if this value is strictly less than the other, + * + 0 if they are equal, and + * + 1 otherwise. + * + * @param x + */ + cmp(x: Decimal128): -1 | 0 | 1 { + return this.rat.cmp(x.rat); + } + + /** + * Truncate the decimal part of this number (if any), returning an integer. + * + * @return {Decimal128} An integer (as a Decimal128 value). + */ + truncate(): Decimal128 { + let [lhs] = this.toString().split("."); + return new Decimal128(lhs); + } + + /** + * Add this Decimal128 value to one or more Decimal128 values. + * + * @param x + */ + add(x: Decimal128): Decimal128 { + let resultRat = Rational.add(this.rat, x.rat); + return new Decimal128( + resultRat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS + 1) + ); + } + + /** + * Subtract another Decimal128 value from one or more Decimal128 values. + * + * Association is to the left: `a.subtract(b, c, d)` is the same as + * `((a.subtract(b)).subtract(c)).subtract(d)`, and so one for any number + * of arguments. + * + * @param x + */ + subtract(x: Decimal128): Decimal128 { + return new Decimal128( + Rational.subtract(this.rat, x.rat).toDecimalPlaces( + MAX_SIGNIFICANT_DIGITS + 1 + ) + ); + } + + /** + * 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 { + let resultRat = Rational.multiply(this.rat, x.rat); + return new Decimal128( + resultRat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS + 1) + ); + } + + /** + * Divide this Decimal128 value by an array of other Decimal128 values. + * + * Association is to the left: 1/2/3 is (1/2)/3 + * + * If only one argument is given, just return the first argument. + * + * @param x + */ + divide(x: Decimal128): Decimal128 { + return new Decimal128( + Rational.divide(this.rat, x.rat).toDecimalPlaces( + MAX_SIGNIFICANT_DIGITS + 1 + ) + ); + } + + round(n: number = 0): Decimal128 { + if (!Number.isInteger(n)) { + throw new TypeError("Argument must be an integer"); + } + + if (n < 0) { + throw new RangeError("Argument must be non-negative"); + } + + return new Decimal128(roundDigitStringTiesToEven(this.toString(), n)); + } + + negate(): Decimal128 { + let s = this.toString(); + + if (s.match(/^-/)) { + return new Decimal128(s.substring(1)); + } + + return new Decimal128("-" + s); + } + + /** + * 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.isNegative) { + return this.negate().remainder(d).negate(); + } + + if (d.isNegative) { + return this.remainder(d.negate()); + } + + let q = this.divide(d).round(); + return this.subtract(d.multiply(q)).abs(); + } + + multiplyAndAdd(x: Decimal128, y: Decimal128): Decimal128 { + return this.multiply(x).add(y); + } +} diff --git a/src/rationalDecimal128.mts b/src/rationalDecimal128.mts deleted file mode 100644 index 89cf1b2..0000000 --- a/src/rationalDecimal128.mts +++ /dev/null @@ -1,723 +0,0 @@ -/** - * 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 - */ - -import { countSignificantDigits, Digit } from "./common.mjs"; -import { Rational } from "./rational.mjs"; - -const EXPONENT_MIN = -6143; -const EXPONENT_MAX = 6144; -const MAX_SIGNIFICANT_DIGITS = 34; - -const bigTen = BigInt(10); -const bigOne = BigInt(1); -const bigZero = BigInt(0); -const bigTwenty = BigInt(20); - -/** - * Normalize a digit string. This means: - * - * + removing any initial zeros - * + removing any trailing zeros - * + rewriting -0 to 0 - * + rewriting 0.0 to 0 - * - * @param s A digit string - * - * @example normalize("000123.456000") // => "123.456" - * @example normalize("000000.000000") // => "0" - * @example normalize("000000.000001") // => "0.000001" - * @example normalize("000000.100000") // => "0.1" - */ -function normalize(s: string): string { - if (s.match(/^-/)) { - let n = normalize(s.substring(1)); - if ("0" === n) { - return "0"; - } - return "-" + n; - } - - let a = s.replace(/^0+/, ""); - let b = a.match(/[.]/) ? a.replace(/0+$/, "") : a; - - if (b.match(/^[.]/)) { - b = "0" + b; - } - - if (b.match(/[.]$/)) { - b = b.substring(0, b.length - 1); - } - - if ("" === b) { - b = "0"; - } - - return b; -} - -function shiftDecimalPointLeft(s: string): string { - if (s.match(/^-/)) { - return "-" + shiftDecimalPointLeft(s.substring(1)); - } - - let [lhs, rhs] = s.split(/[.]/); - return lhs + rhs.substring(0, 1) + "." + rhs.substring(1); -} - -function shiftDecimalPointRight(s: string): string { - if (s.match(/^-/)) { - return "-" + shiftDecimalPointRight(s.substring(1)); - } - - return s.substring(0, s.length - 1) + "." + s.substring(s.length - 1); -} - -function roundDigitStringTiesToEven(s: string, n: number): string { - let [lhs, rhs] = s.split("."); - - if (undefined === rhs) { - return lhs; - } - - if (n === 0) { - let digit = parseInt(lhs.substring(lhs.length - 1, lhs.length)); - let nextDigit = nthSignificantDigit("0." + rhs, 0); - - if (nextDigit > 5) { - return propagateCarryFromRight(lhs); - } - - if (nextDigit === 5) { - if (0 === digit % 2) { - // round to even - return lhs; - } - - return propagateCarryFromRight(lhs); - } - - return lhs; - } - - let timesTen = normalize(shiftDecimalPointLeft(s)); - - if (!timesTen.match(/[.]/)) { - return roundDigitStringTiesToEven(s, n - 1); - } - - return shiftDecimalPointRight(roundDigitStringTiesToEven(timesTen, n - 1)); -} - -/** - * Return the significand of a digit string, assumed to be normalized. - * The returned value is a digit string that has no decimal point, even if the original - * digit string had one. - * - * @param s - * - * @example significand("123.456") // => "123456" - * @example significand("0.000123") // => "123" - */ -function significand(s: string): string { - if (s.match(/^-/)) { - return significand(s.substring(1)); - } else if (s.match(/^0[.]/)) { - return significand(s.substring(2)); - } else if (s.match(/[.]/)) { - return significand(s.replace(/[.]/, "")); - } else if (s.match(/^0+/)) { - return significand(s.replace(/^0+/, "")); - } else if (s.match(/0+$/)) { - return significand(s.replace(/0+$/, "")); - } else { - return s; - } -} - -/** - * Get the n-th significant digit of a digit string, assumed to be normalized. - * - * @param s digit string (assumed to be normalized) - * @param n non-negative integer - */ -function nthSignificantDigit(s: string, n: number): number { - return parseInt(significand(s).charAt(n)); -} - -function cutoffAfterSignificantDigits(s: string, n: number): string { - if (s.match(/^-/)) { - return "-" + cutoffAfterSignificantDigits(s.substring(1), n); - } - - if (s.match(/^0[.]/)) { - return s.substring(0, n + 2); - } - - return s.substring(0, n + 1); -} - -function propagateCarryFromRight(s: string): string { - let [left, right] = s.split(/[.]/); - - if (undefined === right) { - let lastDigit = parseInt(left.charAt(left.length - 1)); - if (lastDigit === 9) { - if (1 === left.length) { - return "10"; - } - - return ( - propagateCarryFromRight(left.substring(0, left.length - 1)) + - "0" - ); - } - return left.substring(0, left.length - 1) + `${lastDigit + 1}`; - } - - let len = right.length; - - if (1 === len) { - return propagateCarryFromRight(left) + ".0"; - } else { - let finalDigit = parseInt(right.charAt(len - 1)); - - if (9 === finalDigit) { - return ( - propagateCarryFromRight( - left + "." + right.substring(0, len - 1) - ) + "0" - ); - } - - return ( - left + - "." + - right.substring(0, len - 1) + - `${parseInt(right.charAt(len - 1)) + 1}` - ); - } -} - -/** - * Return the exponent of a digit string, assumed to be normalized. It is the number of digits - * to the left or right that the significand needs to be shifted to recover the original (normalized) - * digit string. - * - * @param s string of digits (assumed to be normalized) - */ -function exponent(s: string): number { - if (s.match(/^-/)) { - return exponent(s.substring(1)); - } else if (s.match(/[.]/)) { - let rhs = s.split(".")[1]; - return 0 - rhs.length; - } else if (s === "0") { - return 0; - } else { - let m = s.match(/0+$/); - if (m) { - return m[0].length; - } else { - return 0; - } - } -} - -type DigitPair = [Digit, Digit]; - -function prepareLeftHandSideForSquareRoot(s: string): DigitPair[] { - let [lhs] = s.split("."); - let numDigits = lhs.length; - - let digitPairs: DigitPair[] = []; - - if (numDigits % 2 === 1) { - let firstDigit = parseInt(lhs.charAt(0)) as Digit; - digitPairs.push([0, firstDigit]); - numDigits--; - } - - for (let i = 0; i < numDigits / 2; i++) { - let d1 = parseInt(lhs.charAt(2 * i)) as Digit; - let d2 = parseInt(lhs.charAt(2 * i + 1)) as Digit; - digitPairs.push([d1, d2]); - } - - return digitPairs; -} - -function prepareRightHandSideForSquareRoot(s: string): DigitPair[] { - let [_, rhs] = s.split("."); - - if (undefined === rhs) { - return []; - } - - let numDigits = rhs.length; - - let digitPairs: DigitPair[] = []; - - for (let i = 0; i < (numDigits - 1) / 2; i++) { - let d1 = parseInt(rhs.charAt(2 * i)) as Digit; - let d2 = parseInt(rhs.charAt(2 * i + 1)) as Digit; - digitPairs.push([d1, d2]); - } - - if (numDigits % 2 === 1) { - let lastDigit = parseInt(rhs.charAt(numDigits - 1)) as Digit; - digitPairs.push([0, lastDigit]); - } - - return digitPairs; -} - -function valueOfDigitPair(digitPair: DigitPair): bigint { - let [d1, d2] = digitPair; - return BigInt(`${d1}${d2}`); -} - -function nextDigit(p: bigint, c: DigitPair, r: bigint): bigint { - let x: bigint = 0n; - let v: bigint = 100n * r + valueOfDigitPair(c); - while (x * (20n * p + x) <= v) { - x = x + 1n; - } - return x - 1n; -} - -interface Decimal128Constructor { - significand: string; - exponent: bigint; - isNegative: boolean; -} - -function isInteger(x: Decimal128Constructor): boolean { - return x.exponent >= bigZero; -} - -function validateConstructorData(x: Decimal128Constructor): void { - let numSigDigits = countSignificantDigits(x.significand); - - if (isInteger(x) && numSigDigits > MAX_SIGNIFICANT_DIGITS) { - throw new RangeError("Integer too large"); - } - - if (x.exponent > EXPONENT_MAX) { - throw new RangeError(`Exponent too big (${exponent})`); - } - - if (x.exponent < EXPONENT_MIN) { - throw new RangeError(`Exponent too small (${exponent})`); - } -} - -function handleExponentialNotation(s: string): Decimal128Constructor { - let [sg, exp] = s.match(/e/) ? s.split("e") : s.split("E"); - - let isNegative = false; - if (sg.match(/^-/)) { - isNegative = true; - sg = sg.substring(1); - } - - if (exp.match(/^[+]/)) { - exp = exp.substring(1); - } - - return { - significand: sg, - exponent: BigInt(exp), - isNegative: isNegative, - }; -} - -function handleDecimalNotation(s: string): Decimal128Constructor { - let normalized = normalize(s.replace(/_/g, "")); - let isNegative = !!normalized.match(/^-/); - let sg = significand(normalized); - let exp = exponent(normalized); - let numSigDigits = countSignificantDigits(normalized); - let isInteger = exp >= 0; - - if (!isInteger && numSigDigits > MAX_SIGNIFICANT_DIGITS) { - let lastDigit = parseInt(sg.charAt(MAX_SIGNIFICANT_DIGITS)); - let penultimateDigit = parseInt(sg.charAt(MAX_SIGNIFICANT_DIGITS - 1)); - if (lastDigit === 5) { - if (penultimateDigit % 2 === 0) { - let rounded = cutoffAfterSignificantDigits( - normalized, - MAX_SIGNIFICANT_DIGITS - 1 - ); - sg = significand(rounded); - exp = exponent(rounded); - } else if (9 === penultimateDigit) { - let rounded = - cutoffAfterSignificantDigits( - propagateCarryFromRight( - cutoffAfterSignificantDigits( - normalized, - MAX_SIGNIFICANT_DIGITS - 1 - ) - ), - MAX_SIGNIFICANT_DIGITS - 2 - ) + "0"; - sg = significand(rounded); - exp = exponent(rounded); - } else { - let rounded = - cutoffAfterSignificantDigits( - normalized, - MAX_SIGNIFICANT_DIGITS - 2 - ) + `${penultimateDigit + 1}`; - sg = significand(rounded); - exp = exponent(rounded); - } - } else if (lastDigit > 5) { - let cutoff = normalize( - cutoffAfterSignificantDigits(normalized, MAX_SIGNIFICANT_DIGITS) - ); - let rounded = normalize(propagateCarryFromRight(cutoff)); - sg = significand(rounded); - exp = exponent(rounded); - } else { - let rounded = normalize( - cutoffAfterSignificantDigits(normalized, MAX_SIGNIFICANT_DIGITS) - ); - sg = significand(rounded); - exp = exponent(rounded); - } - } - - return { - significand: sg, - exponent: BigInt(exp), - isNegative: isNegative, - }; -} - -export class RationalDecimal128 { - public readonly significand: string; - public readonly exponent: number; - public readonly isNegative: boolean; - private readonly digitStrRegExp = - /^-?[0-9]+(?:_?[0-9]+)*(?:[.][0-9](_?[0-9]+)*)?$/; - private readonly exponentRegExp = /^-?[1-9][0-9]*[eE][-+]?[1-9][0-9]*$/; - private readonly rat; - - constructor(n: string | bigint | number) { - let data = undefined; - - let s: string = ""; - - if (typeof n === "bigint") { - s = n.toString(); - } else if (typeof n === "number") { - if (!Number.isInteger(n)) { - throw new TypeError("Number must be an integer"); - } - if (!Number.isSafeInteger(n)) { - throw new RangeError("Integer is not safe"); - } - s = n.toString(); - } else { - s = n; - } - - if (s.match(this.exponentRegExp)) { - data = handleExponentialNotation(s); - } else if (s.match(this.digitStrRegExp)) { - data = handleDecimalNotation(s); - } else { - throw new SyntaxError(`Illegal number format "${s}"`); - } - - validateConstructorData(data); - - this.significand = data.significand; - this.exponent = parseInt(data.exponent.toString()); // safe because the min & max are less than 10000 - this.isNegative = data.isNegative; - - if ("1" === this.significand) { - // power of ten - if (this.exponent < 0) { - this.rat = new Rational( - bigOne, - BigInt( - (this.isNegative ? "-" : "") + - "1" + - "0".repeat(0 - this.exponent) - ) - ); - } else if (this.exponent === 0) { - this.rat = new Rational( - BigInt(this.isNegative ? -1 : 1), - bigOne - ); - } else { - this.rat = new Rational( - BigInt( - (this.isNegative ? "-" : "") + - "1" + - "0".repeat(this.exponent) - ), - bigOne - ); - } - } else if (this.exponent < 0) { - this.rat = new Rational( - BigInt((this.isNegative ? "-" : "") + this.significand), - bigTen ** BigInt(0 - this.exponent) - ); - } else if (this.exponent === 1) { - this.rat = new Rational( - BigInt((this.isNegative ? "-" : "") + this.significand + "0"), - bigOne - ); - } else { - this.rat = new Rational( - BigInt((this.isNegative ? "-" : "") + this.significand), - bigTen ** BigInt(this.exponent) - ); - } - } - - /** - * Returns a digit string representing this Decimal128. - */ - toString(): string { - return this.rat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS); - } - - /** - * Returns an exponential string representing this Decimal128. - * - */ - toExponentialString(): string { - return ( - (this.isNegative ? "-" : "") + - (this.significand === "" ? "0" : this.significand) + - "E" + - this.exponent - ); - } - - /** - * Is this Decimal128 actually an integer? That is: is there nothing after the decimal point? - */ - isInteger(): boolean { - return this.exponent >= 0; - } - - /** - * Return the absolute value of this Decimal128 value. - * - */ - abs(): RationalDecimal128 { - if (this.isNegative) { - return new RationalDecimal128(this.toString().substring(1)); - } - - return this; - } - - /** - * Return a digit string where the digits of this number are cut off after - * a certain number of digits. Rounding may be performed, in case we always round up. - * - * @param n - */ - toDecimalPlaces(n: number): RationalDecimal128 { - if (!Number.isInteger(n)) { - throw new TypeError("Argument must be an integer"); - } - - if (n < 0) { - throw new RangeError("Argument must be non-negative"); - } - - let s = this.toString(); - let [lhs, rhs] = s.split("."); - - if (undefined === rhs || 0 === n) { - return new RationalDecimal128(lhs); - } - - if (rhs.length <= n) { - return new RationalDecimal128(s); - } - - let penultimateDigit = parseInt(rhs.charAt(n - 1)); - - return new RationalDecimal128( - lhs + "." + rhs.substring(0, n - 1) + `${penultimateDigit + 1}` - ); - } - - /** - * Return the ceiling of this number. That is: the smallest integer greater than or equal to this number. - */ - ceil(): RationalDecimal128 { - if (this.isInteger()) { - return this; - } - - if (this.isNegative) { - return this.truncate(); - } - - return this.add(new RationalDecimal128("1")).truncate(); - } - - /** - * Return the floor of this number. That is: the largest integer less than or equal to this number. - * - */ - floor(): RationalDecimal128 { - return this.truncate(); - } - - /** - * Compare two values. Return - * - * + -1 if this value is strictly less than the other, - * + 0 if they are equal, and - * + 1 otherwise. - * - * @param x - */ - cmp(x: RationalDecimal128): -1 | 0 | 1 { - return this.rat.cmp(x.rat); - } - - /** - * Truncate the decimal part of this number (if any), returning an integer. - * - * @return {RationalDecimal128} An integer (as a Decimal128 value). - */ - truncate(): RationalDecimal128 { - let [lhs] = this.toString().split("."); - return new RationalDecimal128(lhs); - } - - /** - * Add this Decimal128 value to one or more Decimal128 values. - * - * @param x - */ - add(x: RationalDecimal128): RationalDecimal128 { - let resultRat = Rational.add(this.rat, x.rat); - return new RationalDecimal128( - resultRat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS + 1) - ); - } - - /** - * Subtract another Decimal128 value from one or more Decimal128 values. - * - * Association is to the left: `a.subtract(b, c, d)` is the same as - * `((a.subtract(b)).subtract(c)).subtract(d)`, and so one for any number - * of arguments. - * - * @param x - */ - subtract(x: RationalDecimal128): RationalDecimal128 { - return new RationalDecimal128( - Rational.subtract(this.rat, x.rat).toDecimalPlaces( - MAX_SIGNIFICANT_DIGITS + 1 - ) - ); - } - - /** - * Multiply this Decimal128 value by an array of other Decimal128 values. - * - * If no arguments are given, return this value. - * - * @param x - */ - multiply(x: RationalDecimal128): RationalDecimal128 { - let resultRat = Rational.multiply(this.rat, x.rat); - return new RationalDecimal128( - resultRat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS + 1) - ); - } - - /** - * Divide this Decimal128 value by an array of other Decimal128 values. - * - * Association is to the left: 1/2/3 is (1/2)/3 - * - * If only one argument is given, just return the first argument. - * - * @param x - */ - divide(x: RationalDecimal128): RationalDecimal128 { - return new RationalDecimal128( - Rational.divide(this.rat, x.rat).toDecimalPlaces( - MAX_SIGNIFICANT_DIGITS + 1 - ) - ); - } - - round(n: number = 0): RationalDecimal128 { - if (!Number.isInteger(n)) { - throw new TypeError("Argument must be an integer"); - } - - if (n < 0) { - throw new RangeError("Argument must be non-negative"); - } - - return new RationalDecimal128( - roundDigitStringTiesToEven(this.toString(), n) - ); - } - - negate(): RationalDecimal128 { - let s = this.toString(); - - if (s.match(/^-/)) { - return new RationalDecimal128(s.substring(1)); - } - - return new RationalDecimal128("-" + s); - } - - /** - * Return the remainder of this Decimal128 value divided by another Decimal128 value. - * - * @param d - * @throws RangeError If argument is zero - */ - remainder(d: RationalDecimal128): RationalDecimal128 { - if (this.isNegative) { - return this.negate().remainder(d).negate(); - } - - if (d.isNegative) { - return this.remainder(d.negate()); - } - - let q = this.divide(d).round(); - return this.subtract(d.multiply(q)).abs(); - } - - multiplyAndAdd( - x: RationalDecimal128, - y: RationalDecimal128 - ): RationalDecimal128 { - return this.multiply(x).add(y); - } -} diff --git a/tests/abs.test.js b/tests/abs.test.js index 6f5f904..80be7d8 100644 --- a/tests/abs.test.js +++ b/tests/abs.test.js @@ -2,9 +2,9 @@ import { Decimal128 } from "../src/decimal128.mjs"; describe("absolute value", function () { test("simple positive case", () => { - expect(Decimal128.abs("123.456")).toStrictEqual("123.456"); + expect(new Decimal128("123.456").abs().toString()).toStrictEqual("123.456"); }); test("simple negative case", () => { - expect(Decimal128.abs("-123.456")).toStrictEqual("123.456"); + expect(new Decimal128("-123.456").abs().toString()).toStrictEqual("123.456"); }); }); diff --git a/tests/add.test.js b/tests/add.test.js index 82522c5..d795fff 100644 --- a/tests/add.test.js +++ b/tests/add.test.js @@ -3,39 +3,43 @@ import { Decimal128 } from "../src/decimal128.mjs"; const MAX_SIGNIFICANT_DIGITS = 34; const bigDigits = "9".repeat(MAX_SIGNIFICANT_DIGITS); +const zero = new Decimal128("0"); +const one = new Decimal128("1"); +const minusOne = new Decimal128("-1"); +const two = new Decimal128("2"); + describe("addition" + "", () => { test("one plus one equals two", () => { - expect(Decimal128.add("1", "1")).toStrictEqual("2"); + expect(one.add(one).toString()).toStrictEqual("2"); }); test("one plus minus one equals zero", () => { - expect(Decimal128.add("1", "-1")).toStrictEqual("0"); - expect(Decimal128.add("-1", "1")).toStrictEqual("0"); + expect(one.add(minusOne).toString()).toStrictEqual("0"); + expect(minusOne.add(one).toString()).toStrictEqual("0"); }); test("two negatives", () => { - expect(Decimal128.add("-1", "-99")).toStrictEqual("-100"); + expect(minusOne.add(new Decimal128("-99").toString())).toStrictEqual("-100"); }); test("0.1 + 0.2 = 0.3", () => { let a = "0.1"; let b = "0.2"; let c = "0.3"; - expect(Decimal128.add(a, b)).toStrictEqual(c); - expect(Decimal128.add(b, a)).toStrictEqual(c); + expect(new Decimal128(a).add(new Decimal128(b)).toString()).toStrictEqual(c); + expect(new Decimal128(b).add(new Decimal128(a)).toString()).toStrictEqual(c); }); + let big = new Decimal128(bigDigits); test("big plus zero is OK", () => { - expect(Decimal128.add(bigDigits, "0")).toStrictEqual(bigDigits); + expect(big.add(zero).toString()).toStrictEqual(bigDigits); }); test("zero plus big is OK", () => { - expect(Decimal128.add("0", bigDigits)).toStrictEqual(bigDigits); + expect(zero.add(big).toString()).toStrictEqual(bigDigits); }); test("big plus one is OK", () => { - expect(Decimal128.add(bigDigits, "1")).toStrictEqual( - Decimal128.add("1", bigDigits) - ); + expect(big.add(one).toString()).toStrictEqual(one.add(big).toString()); }); test("two plus big is not OK (too many significant digits)", () => { - expect(() => Decimal128.add("2", bigDigits)).toThrow(RangeError); + expect(() => two.add(big)).toThrow(RangeError); }); test("big plus two is not OK (too many significant digits)", () => { - expect(() => Decimal128.add(bigDigits, "2")).toThrow(RangeError); + expect(() => big.add(two)).toThrow(RangeError); }); }); diff --git a/tests/remainder.test.js b/tests/remainder.test.js index e800a7a..8d8948e 100644 --- a/tests/remainder.test.js +++ b/tests/remainder.test.js @@ -4,24 +4,24 @@ describe("remainder", () => { let a = "4.1"; let b = "1.25"; test("simple example", () => { - expect(Decimal128.remainder(a, b)).toStrictEqual("0.35"); + expect(new Decimal128(a).remainder(new Decimal128(b)).toString()).toStrictEqual("0.35"); }); test("negative, with positive argument", () => { - expect(Decimal128.remainder("-4.1", b)).toStrictEqual("-0.35"); + expect(new Decimal128("-4.1").remainder(new Decimal128(b)).toString()).toStrictEqual("-0.35"); }); test("negative argument", () => { - expect(Decimal128.remainder(a, "-1.25")).toStrictEqual("0.35"); + expect(new Decimal128(a).remainder(new Decimal128("-1.25")).toString()).toStrictEqual("0.35"); }); test("negative, with negative argument", () => { - expect(Decimal128.remainder("-4.1", "-1.25")).toStrictEqual("-0.35"); + expect(new Decimal128("-4.1").remainder(new Decimal128("-1.25")).toString()).toStrictEqual("-0.35"); }); test("divide by zero", () => { - expect(() => Decimal128.remainder("42", "0")).toThrow(RangeError); + expect(() => new Decimal128("42").remainder(new Decimal128("0"))).toThrow(RangeError); }); test("divide by minus zero", () => { - expect(() => Decimal128.remainder("42", "-0")).toThrow(RangeError); + expect(() => new Decimal128("42").remainder(new Decimal128("-0"))).toThrow(RangeError); }); test("cleanly divides", () => { - expect(Decimal128.remainder("10", "5")).toStrictEqual("0"); + expect(new Decimal128("10").remainder(new Decimal128("5")).toString()).toStrictEqual("0"); }); }); diff --git a/tests/round.test.js b/tests/round.test.js index f76171e..068e148 100644 --- a/tests/round.test.js +++ b/tests/round.test.js @@ -3,74 +3,74 @@ import { Decimal128 } from "../src/decimal128.mjs"; describe("rounding", () => { describe("no arguments (round to integer)", () => { test("positive odd", () => { - expect(Decimal128.round("1.5")).toStrictEqual("2"); + expect(new Decimal128("1.5").round().toString()).toStrictEqual("2"); }); test("positive even", () => { - expect(Decimal128.round("2.5")).toStrictEqual("2"); + expect(new Decimal128("2.5").round().toString()).toStrictEqual("2"); }); test("round up (positive)", () => { - expect(Decimal128.round("2.6")).toStrictEqual("3"); + expect(new Decimal128("2.6").round().toString()).toStrictEqual("3"); }); test("negative odd", () => { - expect(Decimal128.round("-1.5")).toStrictEqual("-2"); + expect(new Decimal128("-1.5").round().toString()).toStrictEqual("-2"); }); test("negative even", () => { - expect(Decimal128.round("-2.5")).toStrictEqual("-2"); + expect(new Decimal128("-2.5").round().toString()).toStrictEqual("-2"); }); test("round down (positive)", () => { - expect(Decimal128.round("1.1")).toStrictEqual("1"); + expect(new Decimal128("1.1").round().toString()).toStrictEqual("1"); }); }); describe("round to one decimal place, with one decimal place available", () => { test("positive odd", () => { - expect(Decimal128.round("1.5", 1)).toStrictEqual("2"); + expect(new Decimal128("1.5").round(1).toString()).toStrictEqual("2"); }); test("positive even", () => { - expect(Decimal128.round("2.5", 1)).toStrictEqual("2"); + expect(new Decimal128("2.5").round(1).toString()).toStrictEqual("2"); }); test("round up (positive)", () => { - expect(Decimal128.round("2.6", 1)).toStrictEqual("3"); + expect(new Decimal128("2.6").round(1).toString()).toStrictEqual("3"); }); test("negative odd", () => { - expect(Decimal128.round("-1.5", 1)).toStrictEqual("-2"); + expect(new Decimal128("-1.5").round(1).toString()).toStrictEqual("-2"); }); test("negative even", () => { - expect(Decimal128.round("-2.5", 1)).toStrictEqual("-2"); + expect(new Decimal128("-2.5").round(1).toString()).toStrictEqual("-2"); }); test("round down (positive)", () => { - expect(Decimal128.round("1.1", 1).toString()).toStrictEqual("1"); + expect(new Decimal128("1.1").round(1).toString()).toStrictEqual("1"); }); }); describe("round to one decimal place, more than one decimal place available", () => { test("positive odd", () => { - expect(Decimal128.round("1.75", 1)).toStrictEqual("1.8"); + expect(new Decimal128("1.75").round(1).toString()).toStrictEqual("1.8"); }); test("positive even", () => { - expect(Decimal128.round("2.55", 1)).toStrictEqual("2.6"); + expect(new Decimal128("2.55").round( 1).toString()).toStrictEqual("2.6"); }); test("round up (positive)", () => { - expect(Decimal128.round("2.26", 1)).toStrictEqual("2.3"); + expect(new Decimal128("2.26").round( 1).toString()).toStrictEqual("2.3"); }); test("negative odd", () => { - expect(Decimal128.round("-1.95", 1)).toStrictEqual("-2"); + expect(new Decimal128("-1.95").round(1).toString()).toStrictEqual("-2"); }); test("negative even", () => { - expect(Decimal128.round("-2.65", 1)).toStrictEqual("-2.6"); + expect(new Decimal128("-2.65").round(1).toString()).toStrictEqual("-2.6"); }); test("round down (positive)", () => { - expect(Decimal128.round("1.81", 1)).toStrictEqual("1.8"); + expect(new Decimal128("1.81").round(1).toString()).toStrictEqual("1.8"); }); }); test("round integer", () => { - expect(Decimal128.round("42", 6)).toStrictEqual("42"); + expect(new Decimal128("42").round(6).toString()).toStrictEqual("42"); }); test("round with non-number number of digits", () => { - expect(() => Decimal128.round("42", "1")).toThrow(TypeError); + expect(() => new Decimal128("42").round( "1")).toThrow(TypeError); }); test("round with non-integer number of digits", () => { - expect(() => Decimal128.round("42", 1.5)).toThrow(TypeError); + expect(() => new Decimal128("42").round(1.5)).toThrow(TypeError); }); test("round with negative number of digits", () => { - expect(() => Decimal128.round("42", -1)).toThrow(RangeError); + expect(() => new Decimal128("42").round(-1)).toThrow(RangeError); }); }); diff --git a/tests/string.test.js b/tests/string.test.js index 8da4917..3cdcbb2 100644 --- a/tests/string.test.js +++ b/tests/string.test.js @@ -1,37 +1,39 @@ import { Decimal128 } from "../src/decimal128.mjs"; +import { expectDecimal128 } from "./util.js"; const d = "123.456"; describe("to decimal places", function () { + const decimalD = new Decimal128(d); test("more digits than available means no change", () => { - expect(Decimal128.toDecimalPlaces(d, 7)).toStrictEqual("123.456"); + expectDecimal128(decimalD.toDecimalPlaces(7), d); }); test("same number of digits as available means no change", () => { - expect(Decimal128.toDecimalPlaces(d, 6)).toStrictEqual("123.456"); + expectDecimal128(decimalD.toDecimalPlaces(6), d); }); test("round if number has more digits than requested (1)", () => { - expect(Decimal128.toDecimalPlaces(d, 5)).toStrictEqual("123.456"); + expectDecimal128(decimalD.toDecimalPlaces(5), d); }); test("round if number has more digits than requested (2)", () => { - expect(Decimal128.toDecimalPlaces(d, 4)).toStrictEqual("123.456"); + expectDecimal128(decimalD.toDecimalPlaces(4), d); }); test("round if number has more digits than requested (3)", () => { - expect(Decimal128.toDecimalPlaces(d, 3)).toStrictEqual("123.456"); + expectDecimal128(decimalD.toDecimalPlaces(3), d); }); test("round if number has more digits than requested (4)", () => { - expect(Decimal128.toDecimalPlaces(d, 2)).toStrictEqual("123.46"); + expectDecimal128(decimalD.toDecimalPlaces(2), "123.46"); }); test("round if number has more digits than requested (5)", () => { - expect(Decimal128.toDecimalPlaces(d, 1)).toStrictEqual("123.5"); + expectDecimal128(decimalD.toDecimalPlaces(1), "123.5"); }); test("zero decimal places", () => { - expect(Decimal128.toDecimalPlaces(d, 0)).toStrictEqual("123"); + expectDecimal128(decimalD.toDecimalPlaces(0), "123"); }); test("negative number of decimal places", () => { - expect(() => Decimal128.toDecimalPlaces(d, -1)).toThrow(RangeError); + expect(() => decimalD.toDecimalPlaces(-1)).toThrow(RangeError); }); test("non-integer number of decimal places", () => { - expect(() => Decimal128.toDecimalPlaces(d, 1.5)).toThrow(TypeError); + expect(() => decimalD.toDecimalPlaces(1.5).toThrow(TypeError)); }); }); @@ -44,6 +46,6 @@ describe("to exponential string", () => { 1: "1E0", }; for (let [input, output] of Object.entries(data)) { - expect(Decimal128.toExponentialString(input)).toStrictEqual(output); + expectDecimal128(new Decimal128(input).toExponentialString(), output); } }); diff --git a/tests/subtract.test.js b/tests/subtract.test.js index 0b0fcf4..06c4a39 100644 --- a/tests/subtract.test.js +++ b/tests/subtract.test.js @@ -1,25 +1,27 @@ import { Decimal128 } from "../src/decimal128.mjs"; +import { expectDecimal128 } from "./util.js"; const MAX_SIGNIFICANT_DIGITS = 34; let bigDigits = "9".repeat(MAX_SIGNIFICANT_DIGITS); describe("subtraction", () => { test("subtract decimal part", () => { - expect(Decimal128.subtract("123.456", "0.456")).toStrictEqual("123"); + expectDecimal128(new Decimal128("123.456").subtract(new Decimal128("0.456")), "123"); }); test("minus negative number", () => { - expect(Decimal128.subtract("0.1", "-0.2")).toStrictEqual("0.3"); + expectDecimal128(new Decimal128("0.1").subtract(new Decimal128("-0.2")), "0.3"); }); test("subtract two negatives", () => { - expect(Decimal128.subtract("-1.9", "-2.7")).toStrictEqual("0.8"); + expectDecimal128(new Decimal128("-1.9").subtract(new Decimal128("-2.7")), "0.8"); }); + const big = new Decimal128(bigDigits); test("close to range limit", () => { - expect(Decimal128.subtract(bigDigits, "9")).toStrictEqual( + expectDecimal128(big.subtract(new Decimal128("9")), "9".repeat(MAX_SIGNIFICANT_DIGITS - 1) + "0" ); }); test("integer overflow", () => { - expect(() => Decimal128.subtract("-" + bigDigits, "9")).toThrow( + expect(() => new Decimal128("-" + bigDigits).subtract(new Decimal128("9"))).toThrow( RangeError ); }); diff --git a/tests/truncate.test.js b/tests/truncate.test.js index 2dc1014..f4c02fe 100644 --- a/tests/truncate.test.js +++ b/tests/truncate.test.js @@ -1,13 +1,16 @@ import { Decimal128 } from "../src/decimal128.mjs"; +import { expectDecimal128 } from "./util.js"; + describe("truncate", () => { - test("basic example", () => { - expect(Decimal128.truncate("123.45678")).toStrictEqual("123"); - }); - test("truncate negative", () => { - expect(Decimal128.truncate("-42.99")).toStrictEqual("-42"); - }); - test("between zero and one", () => { - expect(Decimal128.truncate("0.00765")).toStrictEqual("0"); - }); + let data = { + "123.45678": "123", + "-42.99": "-42", + "0.00765": "0" + }; + for (let [key, value] of Object.entries(data)) { + test(key, () => { + expectDecimal128(new Decimal128(key).truncate(), value) + }); + } }); diff --git a/tests/util.js b/tests/util.js new file mode 100644 index 0000000..55f7e66 --- /dev/null +++ b/tests/util.js @@ -0,0 +1,8 @@ +import { Decimal128 } from "../src/decimal128.mjs"; + +export function expectDecimal128(a, b) +{ + let lhs = a instanceof Decimal128 ? a.toString() : a; + let rhs = b instanceof Decimal128 ? b.toString() : b; + expect(lhs).toStrictEqual(rhs); +}