From 5ead1357e413173b919bb5d55122cfbe01fdc147 Mon Sep 17 00:00:00 2001 From: Jesse Alama Date: Tue, 7 May 2024 10:51:05 +0200 Subject: [PATCH] Use the official IEEE 754 rounding mode names --- CHANGELOG.md | 4 + examples/floor.mts | 2 +- src/decimal128.mts | 109 ++++++++++--------- tests/constructor.test.js | 20 ++-- tests/round.test.js | 224 +++++++++++++++++++------------------- 5 files changed, 188 insertions(+), 171 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b1a65..1046150 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [15.0.0] - 2024-05-07 + +- Use the official IEEE 754 rounding names rather than "trunc", "ceil", etc. This is a breaking change if you're using those rounding modes. If not, you shouldn't see any change. + ## [14.1.0] - 2024-05-06 ### Added diff --git a/examples/floor.mts b/examples/floor.mts index 669621f..0b46f53 100644 --- a/examples/floor.mts +++ b/examples/floor.mts @@ -1,7 +1,7 @@ import { Decimal128 } from "../src/decimal128.mjs"; function floor(d: Decimal128): Decimal128 { - return d.round(0, "floor"); + return d.round(0, "roundTowardNegative"); } export { floor }; diff --git a/src/decimal128.mts b/src/decimal128.mts index a3969d1..17fb3da 100644 --- a/src/decimal128.mts +++ b/src/decimal128.mts @@ -333,7 +333,9 @@ function roundHalfEven( }; } -function roundCeiling(x: SignedSignificandExponent): SignedSignificandExponent { +function roundHalfExpand( + x: SignedSignificandExponent +): SignedSignificandExponent { let sig = x.significand.toString(); let lastDigit = parseInt(sig.charAt(MAX_SIGNIFICANT_DIGITS)) as Digit; let cutoff = cutoffAfterSignificantDigits(sig, MAX_SIGNIFICANT_DIGITS - 1); @@ -348,17 +350,27 @@ function roundCeiling(x: SignedSignificandExponent): SignedSignificandExponent { x.isNegative, penultimateDigit, lastDigit, - ROUNDING_MODE_CEILING + ROUNDING_MODE_HALF_EXPAND ); + if (finalDigit < 10) { + return { + isNegative: x.isNegative, + significand: BigInt(`${cutoff}${finalDigit}`), + exponent: exp, + }; + } + + let rounded = propagateCarryFromRight(cutoff); + return { isNegative: x.isNegative, - significand: BigInt(`${cutoff}${finalDigit}`), + significand: BigInt(`${rounded}0`), exponent: exp, }; } -function roundFloor(x: SignedSignificandExponent): SignedSignificandExponent { +function roundCeiling(x: SignedSignificandExponent): SignedSignificandExponent { let sig = x.significand.toString(); let lastDigit = parseInt(sig.charAt(MAX_SIGNIFICANT_DIGITS)) as Digit; let cutoff = cutoffAfterSignificantDigits(sig, MAX_SIGNIFICANT_DIGITS - 1); @@ -373,27 +385,17 @@ function roundFloor(x: SignedSignificandExponent): SignedSignificandExponent { x.isNegative, penultimateDigit, lastDigit, - ROUNDING_MODE_FLOOR + ROUNDING_MODE_CEILING ); - if (finalDigit < 10) { - return { - isNegative: x.isNegative, - significand: BigInt(`${cutoff}${finalDigit}`), - exponent: exp, - }; - } - - let rounded = propagateCarryFromRight(cutoff); - return { isNegative: x.isNegative, - significand: BigInt(`${rounded}0`), + significand: BigInt(`${cutoff}${finalDigit}`), exponent: exp, }; } -function roundTrunc(x: SignedSignificandExponent): SignedSignificandExponent { +function roundFloor(x: SignedSignificandExponent): SignedSignificandExponent { let sig = x.significand.toString(); let lastDigit = parseInt(sig.charAt(MAX_SIGNIFICANT_DIGITS)) as Digit; let cutoff = cutoffAfterSignificantDigits(sig, MAX_SIGNIFICANT_DIGITS - 1); @@ -408,19 +410,27 @@ function roundTrunc(x: SignedSignificandExponent): SignedSignificandExponent { x.isNegative, penultimateDigit, lastDigit, - ROUNDING_MODE_TRUNCATE + ROUNDING_MODE_FLOOR ); + if (finalDigit < 10) { + return { + isNegative: x.isNegative, + significand: BigInt(`${cutoff}${finalDigit}`), + exponent: exp, + }; + } + + let rounded = propagateCarryFromRight(cutoff); + return { isNegative: x.isNegative, - significand: BigInt(`${cutoff}${finalDigit}`), + significand: BigInt(`${rounded}0`), exponent: exp, }; } -function roundHalfCeil( - x: SignedSignificandExponent -): SignedSignificandExponent { +function roundTrunc(x: SignedSignificandExponent): SignedSignificandExponent { let sig = x.significand.toString(); let lastDigit = parseInt(sig.charAt(MAX_SIGNIFICANT_DIGITS)) as Digit; let cutoff = cutoffAfterSignificantDigits(sig, MAX_SIGNIFICANT_DIGITS - 1); @@ -435,7 +445,7 @@ function roundHalfCeil( x.isNegative, penultimateDigit, lastDigit, - ROUNDING_MODE_HALF_CEILING + ROUNDING_MODE_TRUNCATE ); return { @@ -458,8 +468,8 @@ function adjustNonInteger( return roundFloor(x); case ROUNDING_MODE_TRUNCATE: return roundTrunc(x); - case ROUNDING_MODE_HALF_CEILING: - return roundHalfCeil(x); + case ROUNDING_MODE_HALF_EXPAND: + return roundHalfExpand(x); default: return roundHalfEven(x); } @@ -572,14 +582,13 @@ function handleInfinity(s: string): Decimal128Constructor { }; } -export const ROUNDING_MODE_CEILING: RoundingMode = "ceil"; -export const ROUNDING_MODE_FLOOR: RoundingMode = "floor"; -export const ROUNDING_MODE_TRUNCATE: RoundingMode = "trunc"; -export const ROUNDING_MODE_HALF_EVEN: RoundingMode = "halfEven"; -export const ROUNDING_MODE_HALF_CEILING: RoundingMode = "halfCeil"; +export const ROUNDING_MODE_CEILING: RoundingMode = "roundTowardPositive"; +export const ROUNDING_MODE_FLOOR: RoundingMode = "roundTowardNegative"; +export const ROUNDING_MODE_TRUNCATE: RoundingMode = "roundTowardZero"; +export const ROUNDING_MODE_HALF_EVEN: RoundingMode = "roundTiesToEven"; +export const ROUNDING_MODE_HALF_EXPAND: RoundingMode = "roundTiesToAway"; -const ROUNDING_MODE_DEFAULT = ROUNDING_MODE_HALF_EVEN; -const CONSTRUCTOR_SHOULD_NORMALIZE = false; +const ROUNDING_MODE_DEFAULT: RoundingMode = ROUNDING_MODE_HALF_EVEN; function roundIt( isNegative: boolean, @@ -610,12 +619,8 @@ function roundIt( return digitToRound; case ROUNDING_MODE_TRUNCATE: return digitToRound; - case ROUNDING_MODE_HALF_CEILING: + case ROUNDING_MODE_HALF_EXPAND: if (decidingDigit >= 5) { - if (isNegative) { - return digitToRound; - } - return (digitToRound + 1) as DigitOrTen; } @@ -637,14 +642,19 @@ function roundIt( } } -type RoundingMode = "ceil" | "floor" | "trunc" | "halfEven" | "halfCeil"; +type RoundingMode = + | "roundTowardPositive" + | "roundTowardNegative" + | "roundTowardZero" + | "roundTiesToEven" + | "roundTiesToAway"; const ROUNDING_MODES: RoundingMode[] = [ - "ceil", - "floor", - "trunc", - "halfEven", - "halfCeil", + "roundTowardPositive", + "roundTowardNegative", + "roundTowardZero", + "roundTiesToEven", + "roundTiesToAway", ]; const digitStrRegExp = @@ -659,14 +669,11 @@ interface ConstructorOptions { interface FullySpecifiedConstructorOptions { roundingMode: RoundingMode; - normalize: boolean; } -const DEFAULT_CONSTRUCTOR_OPTIONS: FullySpecifiedConstructorOptions = - Object.freeze({ - roundingMode: ROUNDING_MODE_DEFAULT, - normalize: CONSTRUCTOR_SHOULD_NORMALIZE, - }); +const DEFAULT_CONSTRUCTOR_OPTIONS: FullySpecifiedConstructorOptions = { + roundingMode: ROUNDING_MODE_DEFAULT, +}; type ToStringFormat = "decimal" | "exponential"; const TOSTRING_FORMATS: string[] = ["decimal", "exponential"]; @@ -1349,6 +1356,10 @@ export class Decimal128 { return this.clone(); } + if (!ROUNDING_MODES.includes(mode)) { + throw new RangeError(`Invalid rounding mode "${mode}"`); + } + if (numDecimalDigits < 0) { numDecimalDigits = 0; } diff --git a/tests/constructor.test.js b/tests/constructor.test.js index 933b101..e2fae21 100644 --- a/tests/constructor.test.js +++ b/tests/constructor.test.js @@ -462,11 +462,11 @@ describe("rounding options", () => { describe("negative value, final decimal digit is five, penultimate digit is less than nine", () => { let val = "-1234567890123456789012345678901234.5"; let answers = { - ceil: "-1234567890123456789012345678901234", - floor: "-1234567890123456789012345678901235", - trunc: "-1234567890123456789012345678901234", - halfEven: "-1234567890123456789012345678901234", - halfCeil: "-1234567890123456789012345678901234", + roundTowardPositive: "-1234567890123456789012345678901234", + roundTowardNegative: "-1234567890123456789012345678901235", + roundTowardZero: "-1234567890123456789012345678901234", + roundTiesToEven: "-1234567890123456789012345678901234", + roundTiesAway: "-1234567890123456789012345678901234", }; for (const [mode, expected] of Object.entries(answers)) { test(`constructor with rounding mode "${mode}"`, () => { @@ -479,11 +479,11 @@ describe("rounding options", () => { describe("negative value, final decimal digit is five, penultimate digit is nine", () => { let roundNineVal = "-1234567890123456789012345678901239.5"; let roundUpAnswers = { - ceil: "-1234567890123456789012345678901239", - floor: "-1234567890123456789012345678901240", - trunc: "-1234567890123456789012345678901239", - halfEven: "-1234567890123456789012345678901240", - halfCeil: "-1234567890123456789012345678901239", + roundTowardPositive: "-1234567890123456789012345678901239", + roundTowardNegative: "-1234567890123456789012345678901240", + roundTowardZero: "-1234567890123456789012345678901239", + roundTiesToEven: "-1234567890123456789012345678901240", + roundTiesAway: "-1234567890123456789012345678901240", }; for (const [mode, expected] of Object.entries(roundUpAnswers)) { test(`constructor with rounding mode "${mode}"`, () => { diff --git a/tests/round.test.js b/tests/round.test.js index e00cc84..0a534b5 100644 --- a/tests/round.test.js +++ b/tests/round.test.js @@ -3,15 +3,11 @@ import * as string_decoder from "string_decoder"; import { expectDecimal128 } from "./util.js"; const roundingModes = [ - "ceil", - "floor", - "expand", - "trunc", - "halfEven", - "halfExpand", - "halfCeil", - "halfFloor", - "halfTrunc", + "roundTowardPositive", + "roundTowardNegative", + "roundTowardZero", + "roundTiesToEven", + "roundTiesToAway", ]; describe("rounding", () => { @@ -75,6 +71,14 @@ describe("rounding", () => { }); }); +describe("unsupported rounding mode", () => { + test("throws", () => { + expect(() => new Decimal128("1.5").round(0, "foobar")).toThrow( + RangeError + ); + }); +}); + describe("Intl.NumberFormat examples", () => { let minusOnePointFive = new Decimal128("-1.5"); let zeroPointFour = new Decimal128("0.4"); @@ -83,133 +87,110 @@ describe("Intl.NumberFormat examples", () => { let onePointFive = new Decimal128("1.5"); describe("ceil", () => { test("-1.5", () => { - expect(minusOnePointFive.round(0, "ceil").toString()).toStrictEqual( - "-1" - ); + expect( + minusOnePointFive.round(0, "roundTowardPositive").toString() + ).toStrictEqual("-1"); }); test("0.4", () => { - expect(zeroPointFour.round(0, "ceil").toString()).toStrictEqual( - "1" - ); + expect( + zeroPointFour.round(0, "roundTowardPositive").toString() + ).toStrictEqual("1"); }); test("0.5", () => { - expect(zeroPointFive.round(0, "ceil").toString()).toStrictEqual( - "1" - ); + expect( + zeroPointFive.round(0, "roundTowardPositive").toString() + ).toStrictEqual("1"); }); test("0.6", () => { - expect(zeroPointSix.round(0, "ceil").toString()).toStrictEqual("1"); + expect( + zeroPointSix.round(0, "roundTowardPositive").toString() + ).toStrictEqual("1"); }); test("1.5", () => { - expect(onePointFive.round(0, "ceil").toString()).toStrictEqual("2"); + expect( + onePointFive.round(0, "roundTowardPositive").toString() + ).toStrictEqual("2"); }); }); describe("floor", () => { test("-1.5", () => { expect( - minusOnePointFive.round(0, "floor").toString() + minusOnePointFive.round(0, "roundTowardNegative").toString() ).toStrictEqual("-2"); }); test("0.4", () => { - expect(zeroPointFour.round(0, "floor").toString()).toStrictEqual( - "0" - ); + expect( + zeroPointFour.round(0, "roundTowardNegative").toString() + ).toStrictEqual("0"); }); test("0.5", () => { - expect(zeroPointFive.round(0, "floor").toString()).toStrictEqual( - "0" - ); + expect( + zeroPointFive.round(0, "roundTowardNegative").toString() + ).toStrictEqual("0"); }); test("0.6", () => { - expect(zeroPointSix.round(0, "floor").toString()).toStrictEqual( - "0" - ); + expect( + zeroPointSix.round(0, "roundTowardNegative").toString() + ).toStrictEqual("0"); }); test("1.5", () => { - expect(onePointFive.round(0, "floor").toString()).toStrictEqual( - "1" - ); + expect( + onePointFive.round(0, "roundTowardNegative").toString() + ).toStrictEqual("1"); }); }); describe("trunc", () => { test("-1.5", () => { expect( - minusOnePointFive.round(0, "trunc").toString() + minusOnePointFive.round(0, "roundTowardZero").toString() ).toStrictEqual("-1"); }); test("0.4", () => { - expect(zeroPointFour.round(0, "trunc").toString()).toStrictEqual( - "0" - ); - }); - test("0.5", () => { - expect(zeroPointFive.round(0, "trunc").toString()).toStrictEqual( - "0" - ); - }); - test("0.6", () => { - expect(zeroPointSix.round(0, "trunc").toString()).toStrictEqual( - "0" - ); - }); - test("1.5", () => { - expect(onePointFive.round(0, "trunc").toString()).toStrictEqual( - "1" - ); - }); - }); - describe("halfCeil", () => { - test("-1.5", () => { expect( - minusOnePointFive.round(0, "halfCeil").toString() - ).toStrictEqual("-1"); - }); - test("0.4", () => { - expect(zeroPointFour.round(0, "halfCeil").toString()).toStrictEqual( - "0" - ); + zeroPointFour.round(0, "roundTowardZero").toString() + ).toStrictEqual("0"); }); test("0.5", () => { - expect(zeroPointFive.round(0, "halfCeil").toString()).toStrictEqual( - "1" - ); + expect( + zeroPointFive.round(0, "roundTowardZero").toString() + ).toStrictEqual("0"); }); test("0.6", () => { - expect(zeroPointSix.round(0, "halfCeil").toString()).toStrictEqual( - "1" - ); + expect( + zeroPointSix.round(0, "roundTowardZero").toString() + ).toStrictEqual("0"); }); test("1.5", () => { - expect(onePointFive.round(0, "halfCeil").toString()).toStrictEqual( - "2" - ); + expect( + onePointFive.round(0, "roundTowardZero").toString() + ).toStrictEqual("1"); }); }); describe("halfEven", () => { test("-1.5", () => { expect( - minusOnePointFive.round(0, "halfEven").toString() + minusOnePointFive.round(0, "roundTiesToEven").toString() ).toStrictEqual("-2"); }); test("0.4", () => { - expect(zeroPointFour.round(0, "halfEven").toString()).toStrictEqual( - "0" - ); + expect( + zeroPointFour.round(0, "roundTiesToEven").toString() + ).toStrictEqual("0"); }); test("0.5", () => { - expect(zeroPointFive.round(0, "halfEven").toString()).toStrictEqual( - "0" - ); + expect( + zeroPointFive.round(0, "roundTiesToEven").toString() + ).toStrictEqual("0"); }); test("0.6", () => { - expect(zeroPointSix.round(0, "halfEven").toString()).toStrictEqual( - "1" - ); + expect( + zeroPointSix.round(0, "roundTiesToEven").toString() + ).toStrictEqual("1"); }); test("1.5", () => { - expect(onePointFive.round(0, "halfEven").toString()).toStrictEqual( - "2" - ); + expect( + onePointFive.round(0, "roundTiesToEven").toString() + ).toStrictEqual("2"); }); }); test("NaN", () => { @@ -250,32 +231,38 @@ describe("Intl.NumberFormat examples", () => { describe("ceiling", function () { test("ceiling works (positive)", () => { expect( - new Decimal128("123.456").round(0, "ceil").toString() + new Decimal128("123.456").round(0, "roundTowardPositive").toString() ).toStrictEqual("124"); }); test("ceiling works (negative)", () => { expect( - new Decimal128("-123.456").round(0, "ceil").toString() + new Decimal128("-123.456") + .round(0, "roundTowardPositive") + .toString() ).toStrictEqual("-123"); }); test("ceiling of an integer is unchanged", () => { - expect(new Decimal128("123").round(0, "ceil").toString()).toStrictEqual( - "123" - ); + expect( + new Decimal128("123").round(0, "roundTowardPositive").toString() + ).toStrictEqual("123"); }); test("NaN", () => { - expect(new Decimal128("NaN").round(0, "ceil").toString()).toStrictEqual( - "NaN" - ); + expect( + new Decimal128("NaN").round(0, "roundTowardPositive").toString() + ).toStrictEqual("NaN"); }); test("positive infinity", () => { expect( - new Decimal128("Infinity").round(0, "ceil").toString() + new Decimal128("Infinity") + .round(0, "roundTowardPositive") + .toString() ).toStrictEqual("Infinity"); }); test("minus infinity", () => { expect( - new Decimal128("-Infinity").round(0, "ceil").toString() + new Decimal128("-Infinity") + .round(0, "roundTowardPositive") + .toString() ).toStrictEqual("-Infinity"); }); }); @@ -289,12 +276,15 @@ describe("truncate", () => { }; for (let [key, value] of Object.entries(data)) { test(key, () => { - expectDecimal128(new Decimal128(key).round(0, "trunc"), value); + expectDecimal128( + new Decimal128(key).round(0, "roundTowardZero"), + value + ); }); } test("NaN", () => { expect( - new Decimal128("NaN").round(0, "trunc").toString() + new Decimal128("NaN").round(0, "roundTowardZero").toString() ).toStrictEqual("NaN"); }); }); @@ -302,12 +292,16 @@ describe("truncate", () => { describe("infinity", () => { test("positive infinity", () => { expect( - new Decimal128("Infinity").round(0, "trunc").toString() + new Decimal128("Infinity") + .round(0, "roundTowardZero") + .toString() ).toStrictEqual("Infinity"); }); test("negative infinity", () => { expect( - new Decimal128("-Infinity").round(0, "trunc").toString() + new Decimal128("-Infinity") + .round(0, "roundTowardZero") + .toString() ).toStrictEqual("-Infinity"); }); }); @@ -316,37 +310,43 @@ describe("truncate", () => { describe("floor", function () { test("floor works (positive)", () => { expect( - new Decimal128("123.456").round(0, "floor").toString() + new Decimal128("123.456").round(0, "roundTowardNegative").toString() ).toStrictEqual("123"); }); test("floor works (negative)", () => { expect( - new Decimal128("-123.456").round(0, "floor").toString() + new Decimal128("-123.456") + .round(0, "roundTowardNegative") + .toString() ).toStrictEqual("-124"); }); test("floor of integer is unchanged", () => { expect( - new Decimal128("123").round(0, "floor").toString() + new Decimal128("123").round(0, "roundTowardNegative").toString() ).toStrictEqual("123"); }); test("floor of zero is unchanged", () => { - expect(new Decimal128("0").round(0, "floor").toString()).toStrictEqual( - "0" - ); + expect( + new Decimal128("0").round(0, "roundTowardNegative").toString() + ).toStrictEqual("0"); }); test("NaN", () => { expect( - new Decimal128("NaN").round(0, "floor").toString() + new Decimal128("NaN").round(0, "roundTowardNegative").toString() ).toStrictEqual("NaN"); }); test("positive infinity", () => { expect( - new Decimal128("Infinity").round(0, "floor").toString() + new Decimal128("Infinity") + .round(0, "roundTowardNegative") + .toString() ).toStrictEqual("Infinity"); }); test("minus infinity", () => { expect( - new Decimal128("-Infinity").round(0, "floor").toString() + new Decimal128("-Infinity") + .round(0, "roundTowardNegative") + .toString() ).toStrictEqual("-Infinity"); }); }); @@ -354,13 +354,15 @@ describe("floor", function () { describe("examples for TC39 plenary slides", () => { let a = new Decimal128("1.456"); test("round to 2 decimal places, rounding mode is ceiling", () => { - expect(a.round(2, "ceil").toString()).toStrictEqual("1.46"); + expect(a.round(2, "roundTowardPositive").toString()).toStrictEqual( + "1.46" + ); }); test("round to 1 decimal place, rounding mode unspecified", () => { expect(a.round(1).toString()).toStrictEqual("1.4"); - expect(a.round(1, "halfEven").toString()).toStrictEqual("1.4"); + expect(a.round(1, "roundTiesToEven").toString()).toStrictEqual("1.4"); }); test("round to 0 decimal places, rounding mode is floor", () => { - expect(a.round(0, "floor").toString()).toStrictEqual("1"); + expect(a.round(0, "roundTowardNegative").toString()).toStrictEqual("1"); }); });