Skip to content

Commit

Permalink
Add support for rounding
Browse files Browse the repository at this point in the history
Also, rearrange some files with more helpful names.
  • Loading branch information
jessealama committed Sep 19, 2023
1 parent a8ebcb2 commit 2cd0e8a
Show file tree
Hide file tree
Showing 5 changed files with 365 additions and 79 deletions.
1 change: 1 addition & 0 deletions src/common.mts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export function countSignificantDigits(s: string): number {
}

export type Digit = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; // -1 signals that we're moving from the integer part to the decimal part of a decimal number
export type DigitOrTen = Digit | 10;
161 changes: 151 additions & 10 deletions src/decimal128.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* @author Jesse Alama <[email protected]>
*/

import { countSignificantDigits } from "./common.mjs";
import { countSignificantDigits, Digit, DigitOrTen } from "./common.mjs";
import { Rational } from "./rational.mjs";

const EXPONENT_MIN = -6143;
Expand Down Expand Up @@ -281,7 +281,8 @@ function handleExponentialNotation(s: string): Decimal128Constructor {
}

function handleDecimalNotation(s: string): Decimal128Constructor {
let normalized = normalize(s.replace(/_/g, ""));
let withoutUnderscores = s.replace(/_/g, "");
let normalized = normalize(withoutUnderscores);
let isNegative = !!normalized.match(/^-/);
let sg = significand(normalized);
let exp = exponent(normalized);
Expand Down Expand Up @@ -344,6 +345,133 @@ function handleDecimalNotation(s: string): Decimal128Constructor {
};
}

export const ROUNDING_MODE_CEILING: RoundingMode = "ceil";
export const ROUNDING_MODE_FLOOR: RoundingMode = "floor";
export const ROUNDING_MODE_EXPAND: RoundingMode = "expand";
export const ROUNDING_MODE_TRUNCATE: RoundingMode = "trunc";
export const ROUNDING_MODE_HALF_EVEN: RoundingMode = "halfEven";
export const ROUNDING_MODE_HALF_EXPAND: RoundingMode = "halfExpand";
export const ROUNDING_MODE_HALF_CEILING: RoundingMode = "halfCeil";
export const ROUNDING_MODE_HALF_FLOOR: RoundingMode = "halfFloor";
export const ROUNDING_MODE_HALF_TRUNCATE: RoundingMode = "halfTrunc";

const ROUNDING_MODE_DEFAULT = ROUNDING_MODE_HALF_EXPAND;

function roundIt(
isNegative: boolean,
digitToRound: Digit,
decidingDigit: Digit,
roundingMode: RoundingMode
): DigitOrTen {
switch (roundingMode) {
case ROUNDING_MODE_CEILING:
if (decidingDigit > 0) {
if (isNegative) {
return digitToRound;
}

return (digitToRound + 1) as DigitOrTen;
}

return digitToRound;
break;
case ROUNDING_MODE_FLOOR:
if (decidingDigit > 0) {
if (isNegative) {
return (digitToRound + 1) as DigitOrTen;
}

return digitToRound;
}

return digitToRound;
break;
case ROUNDING_MODE_EXPAND:
return (digitToRound + 1) as DigitOrTen;
break;
case ROUNDING_MODE_TRUNCATE:
return digitToRound;
break;
case ROUNDING_MODE_HALF_CEILING:
if (decidingDigit >= 5) {
if (isNegative) {
return digitToRound;
}

return (digitToRound + 1) as DigitOrTen;
}

return digitToRound;
break;
case ROUNDING_MODE_HALF_FLOOR:
if (decidingDigit === 5) {
if (isNegative) {
return (digitToRound + 1) as DigitOrTen;
}

return digitToRound;
}

if (decidingDigit > 5) {
return (digitToRound + 1) as DigitOrTen;
}

return digitToRound;
break;
case ROUNDING_MODE_HALF_TRUNCATE:
if (decidingDigit === 5) {
return digitToRound;
}

if (decidingDigit > 5) {
return (digitToRound + 1) as DigitOrTen;
}

return digitToRound;
break;
case ROUNDING_MODE_HALF_EXPAND:
if (decidingDigit >= 5) {
return (digitToRound + 1) as DigitOrTen;
}

return digitToRound;
break;
case ROUNDING_MODE_HALF_EVEN:
if (decidingDigit === 5) {
if (digitToRound % 2 === 0) {
return digitToRound;
}

return (digitToRound + 1) as DigitOrTen;
}

if (decidingDigit > 5) {
return (digitToRound + 1) as DigitOrTen;
}

return digitToRound;
break;
default:
throw new TypeError(`Unknown rounding mode "${roundingMode}"`);
}
}

type ConstructorOptions = {
"rounding-mode"?: RoundingMode;
"max-decimal-digits"?: number;
};

type RoundingMode =
| "ceil"
| "floor"
| "expand"
| "trunc"
| "halfEven"
| "halfExpand"
| "halfCeil"
| "halfFloor"
| "halfTrunc";

export class Decimal128 {
public readonly significand: string;
public readonly exponent: number;
Expand All @@ -353,7 +481,7 @@ export class Decimal128 {
private readonly exponentRegExp = /^-?[1-9][0-9]*[eE][-+]?[1-9][0-9]*$/;
private readonly rat;

constructor(n: string) {
constructor(n: string, opts?: ConstructorOptions) {
let data = undefined;

if (n.match(this.exponentRegExp)) {
Expand Down Expand Up @@ -588,16 +716,29 @@ export class Decimal128 {
);
}

round(n: number = 0): Decimal128 {
if (!Number.isInteger(n)) {
throw new TypeError("Argument must be an integer");
}
/**
*
* @param {RoundingMode} mode (default: ROUNDING_MODE_DEFAULT)
*/
round(mode: RoundingMode = ROUNDING_MODE_DEFAULT): Decimal128 {
let s = this.toString();
let [lhs, rhs] = s.split(".");

if (n < 0) {
throw new RangeError("Argument must be non-negative");
if (undefined === rhs) {
return this;
}

return new Decimal128(roundDigitStringTiesToEven(this.toString(), n));
let finalIntegerDigit = parseInt(lhs.charAt(lhs.length - 1)) as Digit;
let firstDecimalDigit = parseInt(rhs.charAt(0)) as Digit;
let roundedFinalDigit = roundIt(
this.isNegative,
finalIntegerDigit,
firstDecimalDigit,
mode
);
return new Decimal128(
lhs.substring(0, lhs.length - 1) + `${roundedFinalDigit}`
);
}

negate(): Decimal128 {
Expand Down
20 changes: 0 additions & 20 deletions tests/decimal128.test.js → tests/constructor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,23 +269,3 @@ describe("exponent and significand", () => {
).toStrictEqual("1");
});
});

describe("normalization", () => {
let tests = [
["0123.456", "123.456"],
["123.4560", "123.456"],
["123.0", "123"],
["00.123", "0.123"],
["0.0", "0"],
["-0.0", "0"],
["00.0", "0"],
["-00.0", "0"],
["0.00", "0"],
["-0.00", "0"],
];
for (let [a, b] of tests) {
test(`${a} is actually ${b}`, () => {
expect(new Decimal128(a).toString()).toStrictEqual(b);
});
}
});
Loading

0 comments on commit 2cd0e8a

Please sign in to comment.