Skip to content

Commit

Permalink
Use the official IEEE 754 rounding mode names (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
jessealama authored May 7, 2024
1 parent 7c34396 commit b735edf
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 215 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/floor.mts
Original file line number Diff line number Diff line change
@@ -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 };
78 changes: 25 additions & 53 deletions examples/round.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,64 +11,36 @@ let zeroPointSix = new Decimal128("0.6");
let onePointFive = new Decimal128("1.5");

// ceiling
"-1" === minusOnePointFive.round(0, "ceil").toString();
"1" === zeroPointFour.round(0, "ceil").toString();
"1" === zeroPointFive.round(0, "ceil").toString();
"1" === zeroPointSix.round(0, "ceil").toString();
"2" === onePointFive.round(0, "ceil").toString();
"-1" === minusOnePointFive.round(0, "roundTowardPositive").toString();
"1" === zeroPointFour.round(0, "roundTowardPositive").toString();
"1" === zeroPointFive.round(0, "roundTowardPositive").toString();
"1" === zeroPointSix.round(0, "roundTowardPositive").toString();
"2" === onePointFive.round(0, "roundTowardPositive").toString();

// floor
"-2" === minusOnePointFive.round(0, "floor").toString();
"0" === zeroPointFour.round(0, "floor").toString();
"0" === zeroPointFive.round(0, "floor").toString();
"0" === zeroPointSix.round(0, "floor").toString();
"1" === onePointFive.round(0, "floor").toString();

// expand
"-2" === minusOnePointFive.round(0, "expand").toString();
"1" === zeroPointFour.round(0, "expand").toString();
"1" === zeroPointFive.round(0, "expand").toString();
"1" === zeroPointSix.round(0, "expand").toString();
"2" === onePointFive.round(0, "expand").toString();
"-2" === minusOnePointFive.round(0, "roundTowardNegative").toString();
"0" === zeroPointFour.round(0, "roundTowardNegative").toString();
"0" === zeroPointFive.round(0, "roundTowardNegative").toString();
"0" === zeroPointSix.round(0, "roundTowardNegative").toString();
"1" === onePointFive.round(0, "roundTowardNegative").toString();

// truncate
"-1" === minusOnePointFive.round(0, "trunc").toString();
"0" === zeroPointFour.round(0, "trunc").toString();
"0" === zeroPointFive.round(0, "trunc").toString();
"0" === zeroPointSix.round(0, "trunc").toString();
"1" === onePointFive.round(0, "trunc").toString();

// round ties to ceiling
"-1" === minusOnePointFive.round(0, "halfCeil").toString();
"0" === zeroPointFour.round(0, "halfCeil").toString();
"1" === zeroPointFive.round(0, "halfCeil").toString();
"1" === zeroPointSix.round(0, "halfCeil").toString();
"2" === onePointFive.round(0, "halfCeil").toString();

// round ties to floor
"-2" === minusOnePointFive.round(0, "halfFloor").toString();
"0" === zeroPointFour.round(0, "halfFloor").toString();
"0" === zeroPointFive.round(0, "halfFloor").toString();
"1" === zeroPointSix.round(0, "halfFloor").toString();
"1" === onePointFive.round(0, "halfFloor").toString();
"-1" === minusOnePointFive.round(0, "roundTowardZero").toString();
"0" === zeroPointFour.round(0, "roundTowardZero").toString();
"0" === zeroPointFive.round(0, "roundTowardZero").toString();
"0" === zeroPointSix.round(0, "roundTowardZero").toString();
"1" === onePointFive.round(0, "roundTowardZero").toString();

// round ties away from zero
"-2" === minusOnePointFive.round(0, "halfExpand").toString();
"0" === zeroPointFour.round(0, "halfExpand").toString();
"1" === zeroPointFive.round(0, "halfExpand").toString();
"1" === zeroPointSix.round(0, "halfExpand").toString();
"2" === onePointFive.round(0, "halfExpand").toString();

// round ties to toward zero
"-1" === minusOnePointFive.round(0, "halfTrunc").toString();
"0" === zeroPointFour.round(0, "halfTrunc").toString();
"0" === zeroPointFive.round(0, "halfTrunc").toString();
"1" === zeroPointSix.round(0, "halfTrunc").toString();
"1" === onePointFive.round(0, "halfTrunc").toString();
"-2" === minusOnePointFive.round(0, "roundTiesToAway").toString();
"0" === zeroPointFour.round(0, "roundTiesToAway").toString();
"1" === zeroPointFive.round(0, "roundTiesToAway").toString();
"1" === zeroPointSix.round(0, "roundTiesToAway").toString();
"2" === onePointFive.round(0, "roundTiesToAway").toString();

// round ties to even
"-2" === minusOnePointFive.round(0, "halfEven").toString();
"0" === zeroPointFour.round(0, "halfEven").toString();
"0" === zeroPointFive.round(0, "halfEven").toString();
"1" === zeroPointSix.round(0, "halfEven").toString();
"2" === onePointFive.round(0, "halfEven").toString();
"-2" === minusOnePointFive.round(0, "roundTiesToEven").toString();
"0" === zeroPointFour.round(0, "roundTiesToEven").toString();
"0" === zeroPointFive.round(0, "roundTiesToEven").toString();
"1" === zeroPointSix.round(0, "roundTiesToEven").toString();
"2" === onePointFive.round(0, "roundTiesToEven").toString();
111 changes: 60 additions & 51 deletions src/decimal128.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -435,7 +445,7 @@ function roundHalfCeil(
x.isNegative,
penultimateDigit,
lastDigit,
ROUNDING_MODE_HALF_CEILING
ROUNDING_MODE_TRUNCATE
);

return {
Expand All @@ -450,16 +460,14 @@ function adjustNonInteger(
options: FullySpecifiedConstructorOptions
): SignedSignificandExponent {
switch (options.roundingMode) {
case ROUNDING_MODE_HALF_EVEN:
return roundHalfEven(x);
case ROUNDING_MODE_CEILING:
return roundCeiling(x);
case ROUNDING_MODE_FLOOR:
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);
}
Expand Down Expand Up @@ -572,14 +580,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,
Expand Down Expand Up @@ -610,12 +617,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;
}

Expand All @@ -637,14 +640,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 =
Expand All @@ -659,14 +667,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"];
Expand Down Expand Up @@ -1349,6 +1354,10 @@ export class Decimal128 {
return this.clone();
}

if (!ROUNDING_MODES.includes(mode)) {
throw new RangeError(`Invalid rounding mode "${mode}"`);
}

if (numDecimalDigits < 0) {
numDecimalDigits = 0;
}
Expand Down
20 changes: 10 additions & 10 deletions tests/constructor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
roundTiesToAway: "-1234567890123456789012345678901235",
};
for (const [mode, expected] of Object.entries(answers)) {
test(`constructor with rounding mode "${mode}"`, () => {
Expand All @@ -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",
roundTiesToAway: "-1234567890123456789012345678901240",
};
for (const [mode, expected] of Object.entries(roundUpAnswers)) {
test(`constructor with rounding mode "${mode}"`, () => {
Expand Down
Loading

0 comments on commit b735edf

Please sign in to comment.