@c9up/atom 0.1.6 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -10
- package/dist/Decimal.d.ts +12 -1
- package/dist/Decimal.d.ts.map +1 -1
- package/dist/Decimal.js +312 -39
- package/dist/Decimal.js.map +1 -1
- package/dist/Money.d.ts +32 -0
- package/dist/Money.d.ts.map +1 -0
- package/dist/Money.js +128 -0
- package/dist/Money.js.map +1 -0
- package/dist/atlas.d.ts +22 -5
- package/dist/atlas.d.ts.map +1 -1
- package/dist/atlas.js +41 -0
- package/dist/atlas.js.map +1 -1
- package/dist/context.d.ts +15 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +46 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +53 -3
- package/dist/math.js.map +1 -1
- package/dist/native.d.ts +3 -2
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +8 -2
- package/dist/native.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -1
- package/scripts/bench.mjs +26 -0
- package/scripts/verify-napi.mjs +10 -1
- package/scripts/verify-wasm.mjs +32 -3
- package/src/Decimal.ts +403 -47
- package/src/Money.ts +179 -0
- package/src/atlas.ts +78 -2
- package/src/context.ts +67 -0
- package/src/index.ts +20 -5
- package/src/math.ts +56 -3
- package/src/native.ts +9 -2
package/src/Decimal.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
defaultPrecision,
|
|
3
|
+
defaultQuantizeMode,
|
|
4
|
+
defaultRoundMode,
|
|
5
|
+
} from "./context.js";
|
|
6
|
+
import {
|
|
7
|
+
addTs,
|
|
8
|
+
cmpTs,
|
|
9
|
+
divTs,
|
|
10
|
+
formatDecimal,
|
|
11
|
+
modTs,
|
|
12
|
+
mulTs,
|
|
13
|
+
parseDecimal,
|
|
14
|
+
pow10BigInt,
|
|
15
|
+
powTs,
|
|
16
|
+
sqrtTs,
|
|
17
|
+
subTs,
|
|
18
|
+
} from "./math.js";
|
|
19
|
+
import { tryNativeAtom } from "./native.js";
|
|
3
20
|
|
|
4
21
|
export type DecimalInput = string | number | bigint | Decimal;
|
|
5
22
|
export type RoundMode = "trunc" | "floor" | "ceil" | "half-up" | "half-even";
|
|
@@ -46,17 +63,60 @@ export interface ToMinorUnitsOptions {
|
|
|
46
63
|
mode?: RoundMode;
|
|
47
64
|
}
|
|
48
65
|
|
|
66
|
+
export type DecimalSafeParseResult =
|
|
67
|
+
| { success: true; value: Decimal }
|
|
68
|
+
| { success: false; error: Error };
|
|
69
|
+
|
|
70
|
+
const MAX_SCALE = 10_000;
|
|
71
|
+
const MAX_EXPONENT_ABS = 100_000;
|
|
72
|
+
|
|
49
73
|
export class Decimal {
|
|
50
74
|
#value: string;
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
#scaleHint: number;
|
|
76
|
+
|
|
77
|
+
constructor(value: DecimalInput, scaleHint?: number) {
|
|
78
|
+
if (value instanceof Decimal) {
|
|
79
|
+
this.#value = value.#value;
|
|
80
|
+
this.#scaleHint = scaleHint ?? value.#scaleHint;
|
|
81
|
+
assertScale(this.#scaleHint);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
53
84
|
this.#value = normalizeInput(value);
|
|
85
|
+
this.#scaleHint = scaleHint ?? inferInputScale(value, this.#value);
|
|
86
|
+
assertScale(this.#scaleHint);
|
|
54
87
|
}
|
|
55
88
|
|
|
56
89
|
static from(value: DecimalInput): Decimal {
|
|
57
90
|
return new Decimal(value);
|
|
58
91
|
}
|
|
59
92
|
|
|
93
|
+
static parse(value: DecimalInput): Decimal {
|
|
94
|
+
return new Decimal(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static tryParse(value: unknown): Decimal | null {
|
|
98
|
+
const parsed = Decimal.safeParse(value);
|
|
99
|
+
return parsed.success ? parsed.value : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static safeParse(value: unknown): DecimalSafeParseResult {
|
|
103
|
+
try {
|
|
104
|
+
if (!isDecimalInput(value)) {
|
|
105
|
+
throw new Error(`Invalid decimal input type: ${typeof value}`);
|
|
106
|
+
}
|
|
107
|
+
return { success: true, value: new Decimal(value) };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static isDecimal(value: unknown): value is Decimal {
|
|
117
|
+
return value instanceof Decimal;
|
|
118
|
+
}
|
|
119
|
+
|
|
60
120
|
static zero(): Decimal {
|
|
61
121
|
return new Decimal("0");
|
|
62
122
|
}
|
|
@@ -86,63 +146,70 @@ export class Decimal {
|
|
|
86
146
|
|
|
87
147
|
plus(other: DecimalInput): Decimal {
|
|
88
148
|
const b = normalizeInput(other);
|
|
89
|
-
const
|
|
149
|
+
const native = tryNativeAtom();
|
|
150
|
+
const result = native ? native.add(this.#value, b) : addTs(this.#value, b);
|
|
90
151
|
return new Decimal(result);
|
|
91
152
|
}
|
|
92
153
|
|
|
93
154
|
minus(other: DecimalInput): Decimal {
|
|
94
155
|
const b = normalizeInput(other);
|
|
95
|
-
const
|
|
156
|
+
const native = tryNativeAtom();
|
|
157
|
+
const result = native ? native.sub(this.#value, b) : subTs(this.#value, b);
|
|
96
158
|
return new Decimal(result);
|
|
97
159
|
}
|
|
98
160
|
|
|
99
161
|
times(other: DecimalInput): Decimal {
|
|
100
162
|
const b = normalizeInput(other);
|
|
101
|
-
const
|
|
163
|
+
const native = tryNativeAtom();
|
|
164
|
+
const result = native ? native.mul(this.#value, b) : mulTs(this.#value, b);
|
|
102
165
|
return new Decimal(result);
|
|
103
166
|
}
|
|
104
167
|
|
|
105
168
|
div(other: DecimalInput, options: DivOptions = {}): Decimal {
|
|
106
169
|
const b = normalizeInput(other);
|
|
107
|
-
const precision = options.precision ??
|
|
170
|
+
const precision = options.precision ?? defaultPrecision();
|
|
108
171
|
assertScale(precision);
|
|
109
|
-
const
|
|
172
|
+
const native = tryNativeAtom();
|
|
173
|
+
const result = native
|
|
174
|
+
? native.div(this.#value, b, precision)
|
|
175
|
+
: divTs(this.#value, b, precision);
|
|
110
176
|
return new Decimal(result);
|
|
111
177
|
}
|
|
112
178
|
|
|
113
179
|
mod(other: DecimalInput): Decimal {
|
|
114
180
|
const b = normalizeInput(other);
|
|
115
|
-
const
|
|
181
|
+
const native = tryNativeAtom();
|
|
182
|
+
const result = native ? native.rem(this.#value, b) : modTs(this.#value, b);
|
|
116
183
|
return new Decimal(result);
|
|
117
184
|
}
|
|
118
185
|
|
|
119
186
|
pow(exp: number, options: PowOptions = {}): Decimal {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
const precision = options.precision ?? 18;
|
|
187
|
+
assertExponent(exp);
|
|
188
|
+
const precision = options.precision ?? defaultPrecision();
|
|
124
189
|
assertScale(precision);
|
|
125
|
-
const
|
|
190
|
+
const native = tryNativeAtom();
|
|
191
|
+
const result = native
|
|
192
|
+
? native.pow(this.#value, exp, precision)
|
|
193
|
+
: powTs(this.#value, exp, precision);
|
|
126
194
|
return new Decimal(result);
|
|
127
195
|
}
|
|
128
196
|
|
|
129
197
|
sqrt(options: SqrtOptions = {}): Decimal {
|
|
130
|
-
const precision = options.precision ??
|
|
131
|
-
const mode = options.mode ??
|
|
198
|
+
const precision = options.precision ?? defaultPrecision();
|
|
199
|
+
const mode = options.mode ?? defaultRoundMode();
|
|
132
200
|
assertScale(precision);
|
|
133
201
|
if (mode === "trunc") {
|
|
134
|
-
|
|
135
|
-
return new Decimal(result);
|
|
202
|
+
return fromIntScale(this.#sqrtTruncInt(precision), precision);
|
|
136
203
|
}
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const rounded = roundIntScale(parsed.int, precision + 1, precision, mode);
|
|
204
|
+
const truncated = this.#sqrtTruncInt(precision);
|
|
205
|
+
const rounded = roundSqrtInt(this.#value, truncated, precision, mode);
|
|
140
206
|
return fromIntScale(rounded, precision);
|
|
141
207
|
}
|
|
142
208
|
|
|
143
209
|
cmp(other: DecimalInput): -1 | 0 | 1 {
|
|
144
210
|
const b = normalizeInput(other);
|
|
145
|
-
const
|
|
211
|
+
const native = tryNativeAtom();
|
|
212
|
+
const result = native ? native.cmp(this.#value, b) : cmpTs(this.#value, b);
|
|
146
213
|
if (result < 0) return -1;
|
|
147
214
|
if (result > 0) return 1;
|
|
148
215
|
return 0;
|
|
@@ -260,16 +327,22 @@ export class Decimal {
|
|
|
260
327
|
* new Decimal('1.025').quantize('0.05') // → Decimal('1.05')
|
|
261
328
|
*/
|
|
262
329
|
quantize(step: DecimalInput, options: QuantizeOptions = {}): Decimal {
|
|
263
|
-
const { mode =
|
|
330
|
+
const { mode = defaultQuantizeMode() } = options;
|
|
264
331
|
const stepValue = new Decimal(step);
|
|
265
332
|
if (!stepValue.gt(0)) {
|
|
266
333
|
throw new Error("Quantize step must be greater than zero");
|
|
267
334
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
335
|
+
if (options.precision !== undefined) {
|
|
336
|
+
assertScale(options.precision);
|
|
337
|
+
}
|
|
338
|
+
const thisParts = parseDecimal(this.#value);
|
|
339
|
+
const stepParts = parseDecimal(stepValue.#value);
|
|
340
|
+
const numerator = thisParts.int * pow10BigInt(stepParts.scale);
|
|
341
|
+
const denominator = stepParts.int * pow10BigInt(thisParts.scale);
|
|
342
|
+
const units = fromIntScale(
|
|
343
|
+
roundRationalToInt(numerator, denominator, mode),
|
|
344
|
+
0,
|
|
345
|
+
);
|
|
273
346
|
return units.times(stepValue);
|
|
274
347
|
}
|
|
275
348
|
|
|
@@ -385,9 +458,9 @@ export class Decimal {
|
|
|
385
458
|
throw new Error("Allocate requires at least one positive ratio");
|
|
386
459
|
}
|
|
387
460
|
|
|
388
|
-
const parsed =
|
|
389
|
-
const sign = parsed.
|
|
390
|
-
const total = parsed.
|
|
461
|
+
const parsed = this.toParts();
|
|
462
|
+
const sign = parsed.value < 0n ? -1n : 1n;
|
|
463
|
+
const total = parsed.value < 0n ? -parsed.value : parsed.value;
|
|
391
464
|
|
|
392
465
|
const baseShares: bigint[] = [];
|
|
393
466
|
const remainders: Array<{ index: number; remainder: bigint }> = [];
|
|
@@ -423,7 +496,13 @@ export class Decimal {
|
|
|
423
496
|
|
|
424
497
|
toParts(): DecimalScaled {
|
|
425
498
|
const parsed = parseDecimal(this.#value);
|
|
426
|
-
|
|
499
|
+
if (this.#scaleHint <= parsed.scale) {
|
|
500
|
+
return { value: parsed.int, scale: parsed.scale };
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
value: parsed.int * pow10BigInt(this.#scaleHint - parsed.scale),
|
|
504
|
+
scale: this.#scaleHint,
|
|
505
|
+
};
|
|
427
506
|
}
|
|
428
507
|
|
|
429
508
|
toString(): string {
|
|
@@ -443,6 +522,15 @@ export class Decimal {
|
|
|
443
522
|
return Number(this.#value);
|
|
444
523
|
}
|
|
445
524
|
|
|
525
|
+
#sqrtTruncInt(precision: number): bigint {
|
|
526
|
+
const native = tryNativeAtom();
|
|
527
|
+
const result = native
|
|
528
|
+
? native.sqrt(this.#value, precision)
|
|
529
|
+
: sqrtTs(this.#value, precision);
|
|
530
|
+
const parsed = parseDecimal(result);
|
|
531
|
+
return roundIntScale(parsed.int, parsed.scale, precision, "trunc");
|
|
532
|
+
}
|
|
533
|
+
|
|
446
534
|
/**
|
|
447
535
|
* Format the value as a localized string via `Intl.NumberFormat`.
|
|
448
536
|
*
|
|
@@ -497,11 +585,67 @@ function normalizeInput(value: DecimalInput): string {
|
|
|
497
585
|
if (!Number.isFinite(value)) {
|
|
498
586
|
throw new Error(`Invalid decimal: ${value}`);
|
|
499
587
|
}
|
|
500
|
-
return normalizeDecimalString(
|
|
588
|
+
return normalizeDecimalString(decimalStringFromNumber(value));
|
|
501
589
|
}
|
|
502
590
|
return normalizeDecimalString(value);
|
|
503
591
|
}
|
|
504
592
|
|
|
593
|
+
function isDecimalInput(value: unknown): value is DecimalInput {
|
|
594
|
+
return (
|
|
595
|
+
value instanceof Decimal ||
|
|
596
|
+
typeof value === "string" ||
|
|
597
|
+
typeof value === "number" ||
|
|
598
|
+
typeof value === "bigint"
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function inferInputScale(
|
|
603
|
+
value: string | number | bigint,
|
|
604
|
+
normalized: string,
|
|
605
|
+
): number {
|
|
606
|
+
if (typeof value === "bigint") return 0;
|
|
607
|
+
if (typeof value === "number") {
|
|
608
|
+
return parseDecimal(decimalStringFromNumber(value)).scale;
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
return parseDecimal(value).scale;
|
|
612
|
+
} catch {
|
|
613
|
+
return parseDecimal(normalized).scale;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function decimalStringFromNumber(value: number): string {
|
|
618
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
619
|
+
throw new Error(`Unsafe integer decimal input: ${value}`);
|
|
620
|
+
}
|
|
621
|
+
const raw = String(value);
|
|
622
|
+
if (!/[eE]/.test(raw)) return raw;
|
|
623
|
+
|
|
624
|
+
const [coefficient, exponentRaw] = raw.toLowerCase().split("e");
|
|
625
|
+
const exponent = Number(exponentRaw);
|
|
626
|
+
if (!Number.isInteger(exponent)) {
|
|
627
|
+
throw new Error(`Invalid decimal: ${value}`);
|
|
628
|
+
}
|
|
629
|
+
const negative = coefficient.startsWith("-");
|
|
630
|
+
const unsigned =
|
|
631
|
+
negative || coefficient.startsWith("+")
|
|
632
|
+
? coefficient.slice(1)
|
|
633
|
+
: coefficient;
|
|
634
|
+
const [wholeRaw, fracRaw = ""] = unsigned.split(".");
|
|
635
|
+
const digits = `${wholeRaw}${fracRaw}`;
|
|
636
|
+
const decimalIndex = wholeRaw.length + exponent;
|
|
637
|
+
|
|
638
|
+
let expanded: string;
|
|
639
|
+
if (decimalIndex <= 0) {
|
|
640
|
+
expanded = `0.${"0".repeat(-decimalIndex)}${digits}`;
|
|
641
|
+
} else if (decimalIndex >= digits.length) {
|
|
642
|
+
expanded = `${digits}${"0".repeat(decimalIndex - digits.length)}`;
|
|
643
|
+
} else {
|
|
644
|
+
expanded = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
|
|
645
|
+
}
|
|
646
|
+
return negative ? `-${expanded}` : expanded;
|
|
647
|
+
}
|
|
648
|
+
|
|
505
649
|
function normalizeDecimalString(input: string): string {
|
|
506
650
|
const parsed = parseDecimal(input);
|
|
507
651
|
return formatDecimal(parsed.int, parsed.scale);
|
|
@@ -513,6 +657,9 @@ function parseIntegerInput(value: string | number | bigint): bigint {
|
|
|
513
657
|
if (!Number.isInteger(value)) {
|
|
514
658
|
throw new Error(`Invalid integer: ${value}`);
|
|
515
659
|
}
|
|
660
|
+
if (!Number.isSafeInteger(value)) {
|
|
661
|
+
throw new Error(`Unsafe integer input: ${value}`);
|
|
662
|
+
}
|
|
516
663
|
return BigInt(value);
|
|
517
664
|
}
|
|
518
665
|
const s = value.trim();
|
|
@@ -523,15 +670,25 @@ function parseIntegerInput(value: string | number | bigint): bigint {
|
|
|
523
670
|
}
|
|
524
671
|
|
|
525
672
|
function fromIntScale(int: bigint, scale: number): Decimal {
|
|
526
|
-
return new Decimal(formatDecimal(int, scale));
|
|
673
|
+
return new Decimal(formatDecimal(int, scale), scale);
|
|
527
674
|
}
|
|
528
675
|
|
|
529
676
|
function assertScale(scale: number): void {
|
|
530
|
-
if (!Number.isInteger(scale) || scale < 0) {
|
|
677
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
531
678
|
throw new Error(`Invalid scale: ${scale}`);
|
|
532
679
|
}
|
|
533
680
|
}
|
|
534
681
|
|
|
682
|
+
function assertExponent(exp: number): void {
|
|
683
|
+
if (
|
|
684
|
+
!Number.isInteger(exp) ||
|
|
685
|
+
exp < -MAX_EXPONENT_ABS ||
|
|
686
|
+
exp > MAX_EXPONENT_ABS
|
|
687
|
+
) {
|
|
688
|
+
throw new Error(`Invalid exponent: ${exp}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
535
692
|
function roundIntScale(
|
|
536
693
|
int: bigint,
|
|
537
694
|
sourceScale: number,
|
|
@@ -577,6 +734,96 @@ function roundIntScale(
|
|
|
577
734
|
}
|
|
578
735
|
}
|
|
579
736
|
|
|
737
|
+
function roundRationalToInt(
|
|
738
|
+
numerator: bigint,
|
|
739
|
+
denominator: bigint,
|
|
740
|
+
mode: RoundMode,
|
|
741
|
+
): bigint {
|
|
742
|
+
if (denominator <= 0n) {
|
|
743
|
+
throw new Error("Invalid rational denominator");
|
|
744
|
+
}
|
|
745
|
+
const q = numerator / denominator;
|
|
746
|
+
const r = numerator % denominator;
|
|
747
|
+
if (r === 0n) return q;
|
|
748
|
+
|
|
749
|
+
const absR = r < 0n ? -r : r;
|
|
750
|
+
const sign = numerator < 0n ? -1n : 1n;
|
|
751
|
+
|
|
752
|
+
switch (mode) {
|
|
753
|
+
case "trunc":
|
|
754
|
+
return q;
|
|
755
|
+
case "floor":
|
|
756
|
+
return numerator < 0n ? q - 1n : q;
|
|
757
|
+
case "ceil":
|
|
758
|
+
return numerator > 0n ? q + 1n : q;
|
|
759
|
+
case "half-up":
|
|
760
|
+
return absR * 2n >= denominator ? q + sign : q;
|
|
761
|
+
case "half-even": {
|
|
762
|
+
const twice = absR * 2n;
|
|
763
|
+
if (twice < denominator) return q;
|
|
764
|
+
if (twice > denominator) return q + sign;
|
|
765
|
+
const isEven = (q < 0n ? -q : q) % 2n === 0n;
|
|
766
|
+
return isEven ? q : q + sign;
|
|
767
|
+
}
|
|
768
|
+
default:
|
|
769
|
+
throw new Error(`Unknown rounding mode: ${String(mode)}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function roundSqrtInt(
|
|
774
|
+
value: string,
|
|
775
|
+
truncated: bigint,
|
|
776
|
+
precision: number,
|
|
777
|
+
mode: Exclude<RoundMode, "trunc">,
|
|
778
|
+
): bigint {
|
|
779
|
+
const parsed = parseDecimal(value);
|
|
780
|
+
if (parsed.int < 0n) {
|
|
781
|
+
throw new Error("Cannot compute sqrt of a negative decimal");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const squareCmp = compareScaled(
|
|
785
|
+
truncated * truncated,
|
|
786
|
+
2 * precision,
|
|
787
|
+
parsed.int,
|
|
788
|
+
parsed.scale,
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
if (mode === "floor") return truncated;
|
|
792
|
+
if (mode === "ceil") {
|
|
793
|
+
return squareCmp === 0 ? truncated : truncated + 1n;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const thresholdCmp = compareScaled(
|
|
797
|
+
(2n * truncated + 1n) * (2n * truncated + 1n),
|
|
798
|
+
2 * precision,
|
|
799
|
+
4n * parsed.int,
|
|
800
|
+
parsed.scale,
|
|
801
|
+
);
|
|
802
|
+
if (thresholdCmp < 0) return truncated + 1n;
|
|
803
|
+
if (thresholdCmp > 0) return truncated;
|
|
804
|
+
if (mode === "half-up") return truncated + 1n;
|
|
805
|
+
|
|
806
|
+
return truncated % 2n === 0n ? truncated : truncated + 1n;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function compareScaled(
|
|
810
|
+
leftInt: bigint,
|
|
811
|
+
leftScale: number,
|
|
812
|
+
rightInt: bigint,
|
|
813
|
+
rightScale: number,
|
|
814
|
+
): -1 | 0 | 1 {
|
|
815
|
+
let left = leftInt;
|
|
816
|
+
let right = rightInt;
|
|
817
|
+
if (leftScale > rightScale) {
|
|
818
|
+
right *= pow10BigInt(leftScale - rightScale);
|
|
819
|
+
} else if (rightScale > leftScale) {
|
|
820
|
+
left *= pow10BigInt(rightScale - leftScale);
|
|
821
|
+
}
|
|
822
|
+
if (left < right) return -1;
|
|
823
|
+
if (left > right) return 1;
|
|
824
|
+
return 0;
|
|
825
|
+
}
|
|
826
|
+
|
|
580
827
|
/**
|
|
581
828
|
* Structural type matching `@c9up/rosetta`'s `Rosetta` and
|
|
582
829
|
* `RosettaLocale` surfaces. Atom never imports the Rosetta
|
|
@@ -622,14 +869,6 @@ function normalizeViaRosetta(input: string, rosetta: RosettaLike): string {
|
|
|
622
869
|
plusSign: raw.plusSign || "+",
|
|
623
870
|
};
|
|
624
871
|
let normalized = trimmed.replace(/\s| | /g, "");
|
|
625
|
-
normalized = normalized.replace(
|
|
626
|
-
new RegExp(escapeRegExp(data.group), "g"),
|
|
627
|
-
"",
|
|
628
|
-
);
|
|
629
|
-
normalized = normalized.replace(
|
|
630
|
-
new RegExp(escapeRegExp(data.decimal), "g"),
|
|
631
|
-
".",
|
|
632
|
-
);
|
|
633
872
|
if (data.minus !== "-") {
|
|
634
873
|
normalized = normalized.replace(
|
|
635
874
|
new RegExp(escapeRegExp(data.minus), "g"),
|
|
@@ -650,6 +889,16 @@ function normalizeViaRosetta(input: string, rosetta: RosettaLike): string {
|
|
|
650
889
|
normalized = `-${normalized.slice(1, -1)}`;
|
|
651
890
|
}
|
|
652
891
|
|
|
892
|
+
validateLocalizedSyntax(normalized, data.group, data.decimal);
|
|
893
|
+
normalized = normalized.replace(
|
|
894
|
+
new RegExp(escapeRegExp(data.group), "g"),
|
|
895
|
+
"",
|
|
896
|
+
);
|
|
897
|
+
normalized = normalized.replace(
|
|
898
|
+
new RegExp(escapeRegExp(data.decimal), "g"),
|
|
899
|
+
".",
|
|
900
|
+
);
|
|
901
|
+
|
|
653
902
|
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized)) {
|
|
654
903
|
throw new Error(`Invalid localized decimal: ${input}`);
|
|
655
904
|
}
|
|
@@ -671,9 +920,8 @@ function normalizeLocaleNumber(
|
|
|
671
920
|
const decimal = parts.find((part) => part.type === "decimal")?.value ?? ".";
|
|
672
921
|
const minus = parts.find((part) => part.type === "minusSign")?.value ?? "-";
|
|
673
922
|
|
|
674
|
-
let normalized = trimmed
|
|
675
|
-
normalized = normalized.replace(
|
|
676
|
-
normalized = normalized.replace(new RegExp(escapeRegExp(decimal), "g"), ".");
|
|
923
|
+
let normalized = normalizeLocaleDigits(trimmed, locales);
|
|
924
|
+
normalized = normalized.replace(/\s|\u00A0|\u202F/g, "");
|
|
677
925
|
if (minus !== "-") {
|
|
678
926
|
normalized = normalized.replace(new RegExp(escapeRegExp(minus), "g"), "-");
|
|
679
927
|
}
|
|
@@ -683,12 +931,120 @@ function normalizeLocaleNumber(
|
|
|
683
931
|
normalized = `-${normalized.slice(1, -1)}`;
|
|
684
932
|
}
|
|
685
933
|
|
|
934
|
+
validateLocalizedSyntax(
|
|
935
|
+
normalized,
|
|
936
|
+
group,
|
|
937
|
+
decimal,
|
|
938
|
+
getLocaleGrouping(locales),
|
|
939
|
+
);
|
|
940
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(group), "g"), "");
|
|
941
|
+
normalized = normalized.replace(new RegExp(escapeRegExp(decimal), "g"), ".");
|
|
942
|
+
|
|
686
943
|
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized)) {
|
|
687
944
|
throw new Error(`Invalid localized decimal: ${input}`);
|
|
688
945
|
}
|
|
689
946
|
return normalized;
|
|
690
947
|
}
|
|
691
948
|
|
|
949
|
+
function normalizeLocaleDigits(
|
|
950
|
+
input: string,
|
|
951
|
+
locales?: Intl.LocalesArgument,
|
|
952
|
+
): string {
|
|
953
|
+
const digitMap = getLocaleDigitMap(locales);
|
|
954
|
+
let out = "";
|
|
955
|
+
for (const char of input) {
|
|
956
|
+
out += digitMap.get(char) ?? char;
|
|
957
|
+
}
|
|
958
|
+
return out;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function getLocaleDigitMap(
|
|
962
|
+
locales?: Intl.LocalesArgument,
|
|
963
|
+
): Map<string, string> {
|
|
964
|
+
const formatter = new Intl.NumberFormat(locales, { useGrouping: false });
|
|
965
|
+
const digits = formatter.format(9876543210);
|
|
966
|
+
const map = new Map<string, string>();
|
|
967
|
+
let value = 9;
|
|
968
|
+
for (const char of digits) {
|
|
969
|
+
if (!map.has(char) && value >= 0) {
|
|
970
|
+
map.set(char, String(value));
|
|
971
|
+
value--;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return map;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
interface GroupingSpec {
|
|
978
|
+
primary: number;
|
|
979
|
+
secondary: number;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function getLocaleGrouping(locales?: Intl.LocalesArgument): GroupingSpec {
|
|
983
|
+
const parts = new Intl.NumberFormat(locales)
|
|
984
|
+
.formatToParts(1234567890123)
|
|
985
|
+
.filter((part) => part.type === "integer" || part.type === "group");
|
|
986
|
+
const lengths: number[] = [];
|
|
987
|
+
let current = 0;
|
|
988
|
+
for (const part of parts) {
|
|
989
|
+
if (part.type === "integer") {
|
|
990
|
+
current += [...part.value].length;
|
|
991
|
+
} else {
|
|
992
|
+
lengths.push(current);
|
|
993
|
+
current = 0;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
lengths.push(current);
|
|
997
|
+
if (lengths.length < 2) {
|
|
998
|
+
return { primary: 3, secondary: 3 };
|
|
999
|
+
}
|
|
1000
|
+
const primary = lengths[lengths.length - 1];
|
|
1001
|
+
const secondary = lengths[lengths.length - 2] ?? primary;
|
|
1002
|
+
return { primary, secondary };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function validateLocalizedSyntax(
|
|
1006
|
+
value: string,
|
|
1007
|
+
group: string,
|
|
1008
|
+
decimal: string,
|
|
1009
|
+
grouping: GroupingSpec = { primary: 3, secondary: 3 },
|
|
1010
|
+
): void {
|
|
1011
|
+
const decimalParts = value.split(decimal);
|
|
1012
|
+
if (decimalParts.length > 2) {
|
|
1013
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1014
|
+
}
|
|
1015
|
+
let integer = decimalParts[0] ?? "";
|
|
1016
|
+
const fraction = decimalParts[1];
|
|
1017
|
+
if (integer.startsWith("+") || integer.startsWith("-")) {
|
|
1018
|
+
integer = integer.slice(1);
|
|
1019
|
+
}
|
|
1020
|
+
if (!integer) {
|
|
1021
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1022
|
+
}
|
|
1023
|
+
if (fraction !== undefined && !/^\d+$/.test(fraction)) {
|
|
1024
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1025
|
+
}
|
|
1026
|
+
if (!integer.includes(group)) {
|
|
1027
|
+
if (!/^\d+$/.test(integer)) {
|
|
1028
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1029
|
+
}
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const groups = integer.split(group);
|
|
1033
|
+
if (groups.some((part) => !/^\d+$/.test(part))) {
|
|
1034
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1035
|
+
}
|
|
1036
|
+
for (let i = groups.length - 1, distance = 0; i >= 0; i--, distance++) {
|
|
1037
|
+
const size = distance === 0 ? grouping.primary : grouping.secondary;
|
|
1038
|
+
if (i === 0) {
|
|
1039
|
+
if (groups[i].length < 1 || groups[i].length > size) {
|
|
1040
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1041
|
+
}
|
|
1042
|
+
} else if (groups[i].length !== size) {
|
|
1043
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
692
1048
|
function escapeRegExp(value: string): string {
|
|
693
1049
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
694
1050
|
}
|