Skip to content
This repository was archived by the owner on Jan 15, 2025. It is now read-only.

Commit 1634d4d

Browse files
authored
Make cmp work with mathematical values unless otherwise specified (#74)
* Clean up examples * Add normalization option in `cmp` Also, remove normalization possibility in the constructor. We now accept strings as-is.
1 parent e77c531 commit 1634d4d

14 files changed

+210
-158
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [10.0.0] - 2024-01-30
11+
12+
### Changed
13+
14+
- Added an option to `cmp` to normalize values values before comparison (default is `true`, i.e., compare mathematical values).
15+
16+
### Removed
17+
18+
- `normalize` method (can now be safely defined in user space as `new Decimal128(x.toString())`.
19+
- Option to normalize strings in the constructor (we now accept the given digit string as-is)
20+
1021
## [9.1.0] - 2024-01-26
1122

1223
### Added

examples/mortgage.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,17 @@ const one = new Decimal128("1");
55
const paymentsPerYear = new Decimal128("12");
66

77
function calculateMonthlyPayment(p, r, y) {
8-
let principal = new Decimal128(p);
9-
let annualInterestRate = new Decimal128(r);
10-
let years = new Decimal128(y);
8+
const principal = new Decimal128(p);
9+
const annualInterestRate = new Decimal128(r);
10+
const years = new Decimal128(y);
1111
const monthlyInterestRate = annualInterestRate.divide(paymentsPerYear);
1212
const paymentCount = paymentsPerYear.multiply(years);
1313
const onePlusInterestRate = monthlyInterestRate.add(one);
1414
const ratePower = pow(onePlusInterestRate, paymentCount);
1515

16-
let x = principal.multiply(monthlyInterestRate);
17-
let numerator = x.multiply(ratePower);
16+
const x = principal.multiply(monthlyInterestRate);
1817

19-
let denominator = ratePower.subtract(one);
20-
21-
return numerator.divide(denominator);
18+
return x.multiply(ratePower).divide(ratePower.subtract(one));
2219
}
2320

2421
console.log(calculateMonthlyPayment("5000000", "0.05", "30").toString());

examples/step.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ function stepUp(d, n, x) {
1111

1212
let starting = new Decimal128("1.23");
1313
let stepped = stepUp(starting, new Decimal128("3"), new Decimal128("-4"));
14-
console.log(stepped.toString({ numDecimalDigits: 4 })); // 1.2305
14+
console.log(stepped.toString({ numFractionalDigits: 4 })); // 1.2305

src/decimal128.mts

+102-74
Original file line numberDiff line numberDiff line change
@@ -23,45 +23,6 @@ const MAX_SIGNIFICANT_DIGITS = 34;
2323
const bigTen = BigInt(10);
2424
const bigOne = BigInt(1);
2525

26-
/**
27-
* Normalize a digit string. This means:
28-
*
29-
* + removing any initial zeros
30-
* + removing any trailing zeros
31-
* + rewriting 0.0 to 0
32-
*
33-
* Sign is preserved. Thus, -0.0 (e.g.) gets normalized to -0.
34-
*
35-
* @param s A digit string
36-
*
37-
* @example normalize("000123.456000") // => "123.456"
38-
* @example normalize("000000.000000") // => "0"
39-
* @example normalize("000000.000001") // => "0.000001"
40-
* @example normalize("000000.100000") // => "0.1"
41-
*/
42-
function normalize(s: string): string {
43-
if (s.match(/^-/)) {
44-
return "-" + normalize(s.substring(1));
45-
}
46-
47-
let a = s.replace(/^0+/, "");
48-
let b = a.match(/[.]/) ? a.replace(/0+$/, "") : a;
49-
50-
if (b.match(/^[.]/)) {
51-
b = "0" + b;
52-
}
53-
54-
if (b.match(/[.]$/)) {
55-
b = b.substring(0, b.length - 1);
56-
}
57-
58-
if ("" === b) {
59-
b = "0";
60-
}
61-
62-
return b;
63-
}
64-
6526
/**
6627
* Return the significand of a digit string, assumed to be normalized.
6728
* The returned value is a digit string that has no decimal point, even if the original
@@ -123,6 +84,10 @@ function cutoffAfterSignificantDigits(s: string, n: number): string {
12384
}
12485

12586
function ensureDecimalDigits(s: string, n?: number): string {
87+
if (s.match(/^-/)) {
88+
return "-" + ensureDecimalDigits(s.substring(1), n);
89+
}
90+
12691
if (undefined === n) {
12792
return s;
12893
}
@@ -690,11 +655,6 @@ function handleDecimalNotation(
690655
options: FullySpecifiedConstructorOptions
691656
): Decimal128Constructor {
692657
let withoutUnderscores = s.replace(/_/g, "");
693-
694-
if (options.normalize) {
695-
withoutUnderscores = normalize(withoutUnderscores);
696-
}
697-
698658
let isNegative = !!withoutUnderscores.match(/^-/);
699659
let sg = significand(withoutUnderscores);
700660
let exp = exponent(withoutUnderscores);
@@ -737,7 +697,7 @@ export const ROUNDING_MODE_HALF_FLOOR: RoundingMode = "halfFloor";
737697
export const ROUNDING_MODE_HALF_TRUNCATE: RoundingMode = "halfTrunc";
738698

739699
const ROUNDING_MODE_DEFAULT = ROUNDING_MODE_HALF_EVEN;
740-
const CONSTRUCTOR_SHOULD_NORMALIZE = true;
700+
const CONSTRUCTOR_SHOULD_NORMALIZE = false;
741701

742702
function roundIt(
743703
isNegative: boolean,
@@ -890,16 +850,31 @@ const TOSTRING_FORMATS: string[] = ["decimal", "exponential"];
890850
interface ToStringOptions {
891851
format?: ToStringFormat;
892852
numDecimalDigits?: number;
853+
normalize?: boolean;
893854
}
894855

895856
interface FullySpecifiedToStringOptions {
896857
format: string;
897858
numDecimalDigits: number | undefined;
859+
normalize: boolean;
898860
}
899861

900862
const DEFAULT_TOSTRING_OPTIONS: FullySpecifiedToStringOptions = Object.freeze({
901863
format: "decimal",
902864
numDecimalDigits: undefined,
865+
normalize: true,
866+
});
867+
868+
interface CmpOptions {
869+
normalize?: boolean;
870+
}
871+
872+
interface FullySpecifiedCmpOptions {
873+
normalize: boolean;
874+
}
875+
876+
const DEFAULT_CMP_OPTIONS: FullySpecifiedCmpOptions = Object.freeze({
877+
normalize: true,
903878
});
904879

905880
function ensureFullySpecifiedConstructorOptions(
@@ -967,6 +942,26 @@ function ensureFullySpecifiedToStringOptions(
967942
opts.numDecimalDigits = options.numDecimalDigits;
968943
}
969944

945+
if ("boolean" === typeof options.normalize) {
946+
opts.normalize = options.normalize;
947+
}
948+
949+
return opts;
950+
}
951+
952+
function ensureFullySpecifiedCmpOptions(
953+
options?: CmpOptions
954+
): FullySpecifiedCmpOptions {
955+
let opts = { ...DEFAULT_CMP_OPTIONS };
956+
957+
if (undefined === options) {
958+
return opts;
959+
}
960+
961+
if ("boolean" === typeof options.normalize) {
962+
opts.normalize = options.normalize;
963+
}
964+
970965
return opts;
971966
}
972967

@@ -1076,11 +1071,39 @@ export class Decimal128 {
10761071
return (this.isNegative ? "-" : "") + POSITIVE_INFINITY;
10771072
}
10781073

1079-
let prefix = this.isNegative ? "-" : "";
1074+
let neg = this.isNegative;
1075+
let prefix = neg ? "-" : "";
10801076
let sg = this.significand;
10811077
let exp = this.exponent;
1078+
let isZ = this.isZero();
1079+
let numFractionalDigits = options.numDecimalDigits;
1080+
1081+
if (
1082+
"number" === typeof numFractionalDigits &&
1083+
numFractionalDigits < 0
1084+
) {
1085+
numFractionalDigits = undefined;
1086+
}
1087+
1088+
let renderedRat = this.rat.toDecimalPlaces(
1089+
numFractionalDigits ?? MAX_SIGNIFICANT_DIGITS
1090+
);
10821091

10831092
function emitDecimal(): string {
1093+
if (options.normalize && options.numDecimalDigits === undefined) {
1094+
if (isZ) {
1095+
if (neg) {
1096+
return "-0";
1097+
}
1098+
1099+
return "0";
1100+
}
1101+
return ensureDecimalDigits(
1102+
renderedRat,
1103+
options.numDecimalDigits
1104+
);
1105+
}
1106+
10841107
if (exp >= 0) {
10851108
return ensureDecimalDigits(
10861109
prefix + sg + "0".repeat(exp),
@@ -1144,8 +1167,9 @@ export class Decimal128 {
11441167
* + 1 otherwise.
11451168
*
11461169
* @param x
1170+
* @param opts
11471171
*/
1148-
cmp(x: Decimal128): -1 | 0 | 1 | undefined {
1172+
cmp(x: Decimal128, opts?: CmpOptions): -1 | 0 | 1 | undefined {
11491173
if (this.isNaN() || x.isNaN()) {
11501174
return undefined;
11511175
}
@@ -1170,7 +1194,25 @@ export class Decimal128 {
11701194
return x.isNegative ? 1 : -1;
11711195
}
11721196

1173-
return this.rat.cmp(x.rat);
1197+
let options = ensureFullySpecifiedCmpOptions(opts);
1198+
1199+
let ratCmp = this.rat.cmp(x.rat);
1200+
1201+
if (ratCmp !== 0) {
1202+
return ratCmp;
1203+
}
1204+
1205+
if (this.isZero() || options.normalize) {
1206+
return 0;
1207+
}
1208+
1209+
let renderedThis = this.toString({
1210+
format: "decimal",
1211+
normalize: false,
1212+
});
1213+
let renderedThat = x.toString({ format: "decimal", normalize: false });
1214+
1215+
return renderedThis > renderedThat ? -1 : 1;
11741216
}
11751217

11761218
/**
@@ -1214,7 +1256,7 @@ export class Decimal128 {
12141256
Math.min(this.exponent, x.exponent)
12151257
);
12161258

1217-
return new Decimal128(adjusted.toString(), { normalize: false });
1259+
return new Decimal128(adjusted.toString({ normalize: false }));
12181260
}
12191261

12201262
/**
@@ -1258,7 +1300,7 @@ export class Decimal128 {
12581300
let adjusted = initialResult.setExponent(
12591301
Math.min(this.exponent, x.exponent)
12601302
);
1261-
return new Decimal128(adjusted.toString(), { normalize: false });
1303+
return new Decimal128(adjusted.toString({ normalize: false }));
12621304
}
12631305

12641306
/**
@@ -1313,11 +1355,11 @@ export class Decimal128 {
13131355
);
13141356
let adjusted = initialResult.setExponent(this.exponent + x.exponent);
13151357

1316-
return new Decimal128(adjusted.toString(), { normalize: false });
1358+
return new Decimal128(adjusted.toString({ normalize: false }));
13171359
}
13181360

13191361
private isZero(): boolean {
1320-
return this.isFinite() && this.significand === "0";
1362+
return this.isFinite() && !!this.significand.match(/^0+$/);
13211363
}
13221364

13231365
private clone(): Decimal128 {
@@ -1432,23 +1474,15 @@ export class Decimal128 {
14321474
numDecimalDigits: number = 0,
14331475
mode: RoundingMode = ROUNDING_MODE_DEFAULT
14341476
): Decimal128 {
1435-
if (!Number.isSafeInteger(numDecimalDigits)) {
1436-
throw new RangeError(
1437-
"Argument for number of decimal digits must be a safe integer"
1438-
);
1477+
if (this.isNaN() || !this.isFinite()) {
1478+
return this.clone();
14391479
}
14401480

14411481
if (numDecimalDigits < 0) {
1442-
throw new RangeError(
1443-
"Argument for number of decimal digits must be non-negative"
1444-
);
1445-
}
1446-
1447-
if (this.isNaN() || !this.isFinite()) {
1448-
return this;
1482+
numDecimalDigits = 0;
14491483
}
14501484

1451-
let s = this.toString();
1485+
let s = this.toString({ normalize: false });
14521486
let [lhs, rhs] = s.split(".");
14531487

14541488
if (undefined === rhs) {
@@ -1485,13 +1519,13 @@ export class Decimal128 {
14851519
}
14861520

14871521
private negate(): Decimal128 {
1488-
let s = this.toString();
1522+
let s = this.toString({ normalize: false });
14891523

14901524
if (s.match(/^-/)) {
1491-
return new Decimal128(s.substring(1), { normalize: false });
1525+
return new Decimal128(s.substring(1));
14921526
}
14931527

1494-
return new Decimal128("-" + s, { normalize: false });
1528+
return new Decimal128("-" + s);
14951529
}
14961530

14971531
/**
@@ -1530,10 +1564,6 @@ export class Decimal128 {
15301564
return this.subtract(d.multiply(q), opts);
15311565
}
15321566

1533-
normalize(): Decimal128 {
1534-
return new Decimal128(normalize(this.toString()));
1535-
}
1536-
15371567
private decrementExponent(): Decimal128 {
15381568
let exp = this.exponent;
15391569
let sig = this.significand;
@@ -1542,9 +1572,7 @@ export class Decimal128 {
15421572
let newExp = exp - 1;
15431573
let newSig = sig + "0";
15441574

1545-
return new Decimal128(`${prefix}${newSig}E${newExp}`, {
1546-
normalize: false,
1547-
});
1575+
return new Decimal128(`${prefix}${newSig}E${newExp}`);
15481576
}
15491577

15501578
private setExponent(newExp: number): Decimal128 {

tests/add.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe("addition", () => {
6262
.add(new Decimal128("1.0", { normalize: false }), {
6363
normalize: false,
6464
})
65-
.toString()
65+
.toString({ normalize: false })
6666
).toStrictEqual("2.0");
6767
});
6868
});
@@ -132,7 +132,7 @@ describe("examples from the General Decimal Arithmetic specification", () => {
132132
expect(
133133
new Decimal128("12")
134134
.add(new Decimal128("7.00", { normalize: false }))
135-
.toString()
135+
.toString({ normalize: false })
136136
).toStrictEqual("19.00");
137137
});
138138
test("example two", () => {

0 commit comments

Comments
 (0)