From 2e2b54517123ac3e461125a4134e5c96da2bc53b Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Wed, 11 Oct 2023 11:23:28 +0200 Subject: [PATCH] Add support for NaN --- src/decimal128.mts | 56 +++++++++++++++++++++++++++++++++++------ tests/abs.test.js | 3 +++ tests/add.test.js | 17 +++++++++++++ tests/ceiling.test.js | 3 +++ tests/cmp.test.js | 17 +++++++++++++ tests/divide.test.js | 30 +++++++++++++++------- tests/floor.test.js | 3 +++ tests/multiply.test.js | 17 +++++++++++++ tests/remainder.test.js | 31 ++++++++++++++++++----- tests/round.test.js | 3 +++ tests/string.test.js | 4 +++ tests/subtract.test.js | 17 +++++++++++++ tests/truncate.test.js | 5 ++++ 13 files changed, 183 insertions(+), 23 deletions(-) diff --git a/src/decimal128.mts b/src/decimal128.mts index f081db5..6a9139a 100644 --- a/src/decimal128.mts +++ b/src/decimal128.mts @@ -172,7 +172,7 @@ function exponent(s: string): number { } interface Decimal128Constructor { - isNan: boolean; + isNaN: boolean; significand: string; exponent: bigint; isNegative: boolean; @@ -183,7 +183,7 @@ function isInteger(x: Decimal128Constructor): boolean { } function validateConstructorData(x: Decimal128Constructor): void { - if (x.isNan) { + if (x.isNaN) { return; // no further validation needed } @@ -207,7 +207,7 @@ function handleNan(s: string): Decimal128Constructor { significand: "", exponent: bigZero, isNegative: false, - isNan: true, + isNaN: true, }; } function handleExponentialNotation(s: string): Decimal128Constructor { @@ -227,7 +227,7 @@ function handleExponentialNotation(s: string): Decimal128Constructor { significand: sg, exponent: BigInt(exp), isNegative: isNegative, - isNan: false, + isNaN: false, }; } @@ -293,7 +293,7 @@ function handleDecimalNotation(s: string): Decimal128Constructor { significand: sg, exponent: BigInt(exp), isNegative: isNegative, - isNan: false, + isNaN: false, }; } @@ -401,7 +401,7 @@ type RoundingMode = | "halfTrunc"; export class Decimal128 { - private readonly isNan: boolean; + public readonly isNaN: boolean; public readonly significand: string; public readonly exponent: number; public readonly isNegative: boolean; @@ -426,7 +426,7 @@ export class Decimal128 { validateConstructorData(data); - this.isNan = data.isNan; + this.isNaN = data.isNaN; this.significand = data.significand; this.exponent = parseInt(data.exponent.toString()); // safe because the min & max are less than 10000 this.isNegative = data.isNegative; @@ -479,6 +479,10 @@ export class Decimal128 { * Returns a digit string representing this Decimal128. */ toString(): string { + if (this.isNaN) { + return "NaN"; + } + return this.rat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS); } @@ -579,7 +583,11 @@ export class Decimal128 { * * @param x */ - cmp(x: Decimal128): -1 | 0 | 1 { + cmp(x: Decimal128): -1 | 0 | 1 | undefined { + if (this.isNaN || x.isNaN) { + return undefined; + } + return this.rat.cmp(x.rat); } @@ -589,6 +597,10 @@ export class Decimal128 { * @return {Decimal128} An integer (as a Decimal128 value). */ truncate(): Decimal128 { + if (this.isNaN) { + return this; + } + let [lhs] = this.toString().split("."); return new Decimal128(lhs); } @@ -599,6 +611,10 @@ export class Decimal128 { * @param x */ add(x: Decimal128): Decimal128 { + if (this.isNaN || x.isNaN) { + return new Decimal128("NaN"); + } + let resultRat = Rational.add(this.rat, x.rat); return new Decimal128( resultRat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS + 1) @@ -611,6 +627,10 @@ export class Decimal128 { * @param x */ subtract(x: Decimal128): Decimal128 { + if (this.isNaN || x.isNaN) { + return new Decimal128("NaN"); + } + return new Decimal128( Rational.subtract(this.rat, x.rat).toDecimalPlaces( MAX_SIGNIFICANT_DIGITS + 1 @@ -626,12 +646,20 @@ export class Decimal128 { * @param x */ multiply(x: Decimal128): Decimal128 { + if (this.isNaN || x.isNaN) { + return new Decimal128("NaN"); + } + let resultRat = Rational.multiply(this.rat, x.rat); return new Decimal128( resultRat.toDecimalPlaces(MAX_SIGNIFICANT_DIGITS + 1) ); } + private isZero(): boolean { + return this.significand === ""; + } + /** * Divide this Decimal128 value by an array of other Decimal128 values. * @@ -642,6 +670,14 @@ export class Decimal128 { * @param x */ divide(x: Decimal128): Decimal128 { + if (this.isNaN || x.isNaN) { + return new Decimal128("NaN"); + } + + if (x.isZero()) { + return new Decimal128("NaN"); + } + return new Decimal128( Rational.divide(this.rat, x.rat).toDecimalPlaces( MAX_SIGNIFICANT_DIGITS + 1 @@ -654,6 +690,10 @@ export class Decimal128 { * @param {RoundingMode} mode (default: ROUNDING_MODE_DEFAULT) */ round(mode: RoundingMode = ROUNDING_MODE_DEFAULT): Decimal128 { + if (this.isNaN) { + return this; + } + let s = this.toString(); let [lhs, rhs] = s.split("."); diff --git a/tests/abs.test.js b/tests/abs.test.js index 9aa8798..239b061 100644 --- a/tests/abs.test.js +++ b/tests/abs.test.js @@ -11,4 +11,7 @@ describe("absolute value", function () { "123.456" ); }); + test("NaN", () => { + expect(new Decimal128("NaN").abs().toString()).toStrictEqual("NaN"); + }); }); diff --git a/tests/add.test.js b/tests/add.test.js index 3081e4d..ba38d25 100644 --- a/tests/add.test.js +++ b/tests/add.test.js @@ -48,4 +48,21 @@ describe("addition" + "", () => { test("big plus two is not OK (too many significant digits)", () => { expect(() => big.add(two)).toThrow(RangeError); }); + describe("NaN", () => { + test("NaN plus NaN is NaN", () => { + expect( + new Decimal128("NaN").add(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + test("NaN plus number", () => { + expect( + new Decimal128("NaN").add(new Decimal128("1")).toString() + ).toStrictEqual("NaN"); + }); + test("number plus NaN", () => { + expect( + new Decimal128("1").add(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + }); }); diff --git a/tests/ceiling.test.js b/tests/ceiling.test.js index 7e1a21f..ff6592d 100644 --- a/tests/ceiling.test.js +++ b/tests/ceiling.test.js @@ -14,4 +14,7 @@ describe("ceiling", function () { test("ceiling of an integer is unchanged", () => { expect(new Decimal128("123").ceil().toString()).toStrictEqual("123"); }); + test("NaN", () => { + expect(new Decimal128("NaN").ceil().toString()).toStrictEqual("NaN"); + }); }); diff --git a/tests/cmp.test.js b/tests/cmp.test.js index ccc9075..1f64ca6 100644 --- a/tests/cmp.test.js +++ b/tests/cmp.test.js @@ -67,4 +67,21 @@ describe("many digits", () => { ) ).toStrictEqual(-1); }); + describe("NaN", () => { + test("NaN cmp NaN is NaN", () => { + expect( + new Decimal128("NaN").cmp(new Decimal128("NaN")) + ).toStrictEqual(undefined); + }); + test("number cmp NaN is NaN", () => { + expect( + new Decimal128("1").cmp(new Decimal128("NaN")) + ).toStrictEqual(undefined); + }); + test("NaN cmp number is NaN", () => { + expect( + new Decimal128("NaN").cmp(new Decimal128("1")) + ).toStrictEqual(undefined); + }); + }); }); diff --git a/tests/divide.test.js b/tests/divide.test.js index 0f8eea6..7b29d37 100644 --- a/tests/divide.test.js +++ b/tests/divide.test.js @@ -25,14 +25,26 @@ describe("division", () => { ).toStrictEqual(c); }); } - test("divide by zero", () => { - expect(() => - new Decimal128("123.456").divide(new Decimal128("0.0")) - ).toThrow(RangeError); - }); - test("divide by negative zero", () => { - expect(() => - new Decimal128("123.456").divide(new Decimal128("-0")) - ).toThrow(RangeError); + describe("NaN", () => { + test("NaN divided by NaN is NaN", () => { + expect( + new Decimal128("NaN").divide(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + test("NaN divided by number is NaN", () => { + expect( + new Decimal128("NaN").divide(new Decimal128("1")).toString() + ).toStrictEqual("NaN"); + }); + test("number divided by NaN is NaN", () => { + expect( + new Decimal128("1").divide(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + test("divide by zero is NaN", () => { + expect( + new Decimal128("42").divide(new Decimal128("0")).toString() + ).toStrictEqual("NaN"); + }); }); }); diff --git a/tests/floor.test.js b/tests/floor.test.js index 5d62f8a..518da8f 100644 --- a/tests/floor.test.js +++ b/tests/floor.test.js @@ -18,4 +18,7 @@ describe("floor", function () { test("floor of zero is unchanged", () => { expect(new Decimal128("0").floor().toString()).toStrictEqual("0"); }); + test("NaN", () => { + expect(new Decimal128("NaN").floor().toString()).toStrictEqual("NaN"); + }); }); diff --git a/tests/multiply.test.js b/tests/multiply.test.js index c4ebc7c..ee934a5 100644 --- a/tests/multiply.test.js +++ b/tests/multiply.test.js @@ -54,4 +54,21 @@ describe("multiplication", () => { ) ).toThrow(RangeError); }); + describe("NaN", () => { + test("NaN times NaN is NaN", () => { + expect( + new Decimal128("NaN").multiply(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + test("number times NaN is NaN", () => { + expect( + new Decimal128("1").multiply(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + test("NaN times number is NaN", () => { + expect( + new Decimal128("NaN").multiply(new Decimal128("1")).toString() + ).toStrictEqual("NaN"); + }); + }); }); diff --git a/tests/remainder.test.js b/tests/remainder.test.js index ab31816..7487b6e 100644 --- a/tests/remainder.test.js +++ b/tests/remainder.test.js @@ -24,18 +24,37 @@ describe("remainder", () => { ).toStrictEqual("-0.35"); }); test("divide by zero", () => { - expect(() => - new Decimal128("42").remainder(new Decimal128("0")) - ).toThrow(RangeError); + expect( + new Decimal128("42").remainder(new Decimal128("0")).toString() + ).toStrictEqual("NaN"); }); test("divide by minus zero", () => { - expect(() => - new Decimal128("42").remainder(new Decimal128("-0")) - ).toThrow(RangeError); + expect( + new Decimal128("42").remainder(new Decimal128("-0")).toString() + ).toStrictEqual("NaN"); }); test("cleanly divides", () => { expect( new Decimal128("10").remainder(new Decimal128("5")).toString() ).toStrictEqual("0"); }); + describe("NaN", () => { + test("NaN remainder NaN is NaN", () => { + expect( + new Decimal128("NaN") + .remainder(new Decimal128("NaN")) + .toString() + ).toStrictEqual("NaN"); + }); + test("number remainder NaN is NaN", () => { + expect( + new Decimal128("1").remainder(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + test("NaN remainder number is NaN", () => { + expect( + new Decimal128("NaN").remainder(new Decimal128("1")).toString() + ).toStrictEqual("NaN"); + }); + }); }); diff --git a/tests/round.test.js b/tests/round.test.js index d020fa2..321024f 100644 --- a/tests/round.test.js +++ b/tests/round.test.js @@ -245,4 +245,7 @@ describe("Intl.NumberFormat examples", () => { ); }); }); + test("NaN", () => { + expect(new Decimal128("NaN").round().toString()).toStrictEqual("NaN"); + }); }); diff --git a/tests/string.test.js b/tests/string.test.js index 836839c..1b35699 100644 --- a/tests/string.test.js +++ b/tests/string.test.js @@ -69,3 +69,7 @@ describe("normalization", () => { }); } }); + +describe("NaN", () => { + expect(new Decimal128("NaN").toString()).toStrictEqual("NaN"); +}); diff --git a/tests/subtract.test.js b/tests/subtract.test.js index 98e4611..e039cdb 100644 --- a/tests/subtract.test.js +++ b/tests/subtract.test.js @@ -35,4 +35,21 @@ describe("subtraction", () => { new Decimal128("-" + bigDigits).subtract(new Decimal128("9")) ).toThrow(RangeError); }); + describe("NaN", () => { + test("NaN minus NaN is NaN", () => { + expect( + new Decimal128("NaN").subtract(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + test("NaN minus number", () => { + expect( + new Decimal128("NaN").subtract(new Decimal128("1")).toString() + ).toStrictEqual("NaN"); + }); + test("number minus NaN", () => { + expect( + new Decimal128("1").subtract(new Decimal128("NaN")).toString() + ).toStrictEqual("NaN"); + }); + }); }); diff --git a/tests/truncate.test.js b/tests/truncate.test.js index 455ed15..ad9e825 100644 --- a/tests/truncate.test.js +++ b/tests/truncate.test.js @@ -12,4 +12,9 @@ describe("truncate", () => { expectDecimal128(new Decimal128(key).truncate(), value); }); } + test("NaN", () => { + expect(new Decimal128("NaN").truncate().toString()).toStrictEqual( + "NaN" + ); + }); });