@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/src/Decimal.ts CHANGED
@@ -1,5 +1,22 @@
1
- import { formatDecimal, parseDecimal, pow10BigInt } from "./math.js";
2
- import { nativeAtom } from "./native.js";
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
- constructor(value: DecimalInput) {
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 result = nativeAtom().add(this.#value, b);
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 result = nativeAtom().sub(this.#value, b);
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 result = nativeAtom().mul(this.#value, b);
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 ?? 18;
170
+ const precision = options.precision ?? defaultPrecision();
108
171
  assertScale(precision);
109
- const result = nativeAtom().div(this.#value, b, precision);
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 result = nativeAtom().rem(this.#value, b);
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
- if (!Number.isInteger(exp)) {
121
- throw new Error(`Invalid exponent: ${exp}`);
122
- }
123
- const precision = options.precision ?? 18;
187
+ assertExponent(exp);
188
+ const precision = options.precision ?? defaultPrecision();
124
189
  assertScale(precision);
125
- const result = nativeAtom().pow(this.#value, exp, precision);
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 ?? 18;
131
- const mode = options.mode ?? "trunc";
198
+ const precision = options.precision ?? defaultPrecision();
199
+ const mode = options.mode ?? defaultRoundMode();
132
200
  assertScale(precision);
133
201
  if (mode === "trunc") {
134
- const result = nativeAtom().sqrt(this.#value, precision);
135
- return new Decimal(result);
202
+ return fromIntScale(this.#sqrtTruncInt(precision), precision);
136
203
  }
137
- const withExtra = nativeAtom().sqrt(this.#value, precision + 1);
138
- const parsed = parseDecimal(withExtra);
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 result = nativeAtom().cmp(this.#value, b);
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 = "half-up" } = options;
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
- const thisScale = this.toParts().scale;
269
- const stepScale = stepValue.toParts().scale;
270
- const precision =
271
- options.precision ?? Math.max(18, thisScale + stepScale + 6);
272
- const units = this.div(stepValue, { precision }).round(0, mode);
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 = parseDecimal(this.#value);
389
- const sign = parsed.int < 0n ? -1n : 1n;
390
- const total = parsed.int < 0n ? -parsed.int : parsed.int;
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
- return { value: parsed.int, scale: parsed.scale };
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(String(value));
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.replace(/\s|\u00A0|\u202F/g, "");
675
- normalized = normalized.replace(new RegExp(escapeRegExp(group), "g"), "");
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
  }