Skip to content

Commit

Permalink
Add support for NaN
Browse files Browse the repository at this point in the history
  • Loading branch information
jessealama committed Oct 11, 2023
1 parent 45eee4d commit 2e2b545
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 23 deletions.
56 changes: 48 additions & 8 deletions src/decimal128.mts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ function exponent(s: string): number {
}

interface Decimal128Constructor {
isNan: boolean;
isNaN: boolean;
significand: string;
exponent: bigint;
isNegative: boolean;
Expand All @@ -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
}

Expand All @@ -207,7 +207,7 @@ function handleNan(s: string): Decimal128Constructor {
significand: "",
exponent: bigZero,
isNegative: false,
isNan: true,
isNaN: true,
};
}
function handleExponentialNotation(s: string): Decimal128Constructor {
Expand All @@ -227,7 +227,7 @@ function handleExponentialNotation(s: string): Decimal128Constructor {
significand: sg,
exponent: BigInt(exp),
isNegative: isNegative,
isNan: false,
isNaN: false,
};
}

Expand Down Expand Up @@ -293,7 +293,7 @@ function handleDecimalNotation(s: string): Decimal128Constructor {
significand: sg,
exponent: BigInt(exp),
isNegative: isNegative,
isNan: false,
isNaN: false,
};
}

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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.
*
Expand All @@ -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
Expand All @@ -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(".");

Expand Down
3 changes: 3 additions & 0 deletions tests/abs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ describe("absolute value", function () {
"123.456"
);
});
test("NaN", () => {
expect(new Decimal128("NaN").abs().toString()).toStrictEqual("NaN");
});
});
17 changes: 17 additions & 0 deletions tests/add.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
3 changes: 3 additions & 0 deletions tests/ceiling.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
17 changes: 17 additions & 0 deletions tests/cmp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
30 changes: 21 additions & 9 deletions tests/divide.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
3 changes: 3 additions & 0 deletions tests/floor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
17 changes: 17 additions & 0 deletions tests/multiply.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
31 changes: 25 additions & 6 deletions tests/remainder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
3 changes: 3 additions & 0 deletions tests/round.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,7 @@ describe("Intl.NumberFormat examples", () => {
);
});
});
test("NaN", () => {
expect(new Decimal128("NaN").round().toString()).toStrictEqual("NaN");
});
});
4 changes: 4 additions & 0 deletions tests/string.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@ describe("normalization", () => {
});
}
});

describe("NaN", () => {
expect(new Decimal128("NaN").toString()).toStrictEqual("NaN");
});
17 changes: 17 additions & 0 deletions tests/subtract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
5 changes: 5 additions & 0 deletions tests/truncate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ describe("truncate", () => {
expectDecimal128(new Decimal128(key).truncate(), value);
});
}
test("NaN", () => {
expect(new Decimal128("NaN").truncate().toString()).toStrictEqual(
"NaN"
);
});
});

0 comments on commit 2e2b545

Please sign in to comment.