@c9up/atom 0.1.11 → 0.1.13
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 +9 -1
- package/dist/Decimal.d.ts +8 -1
- package/dist/Decimal.d.ts.map +1 -1
- package/dist/Decimal.js +132 -96
- package/dist/Decimal.js.map +1 -1
- package/dist/Money.d.ts +15 -2
- package/dist/Money.d.ts.map +1 -1
- package/dist/Money.js +28 -3
- package/dist/Money.js.map +1 -1
- package/dist/atlas.d.ts +2 -5
- package/dist/atlas.d.ts.map +1 -1
- package/dist/atlas.js +2 -5
- package/dist/atlas.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -33
- package/dist/index.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 +8 -7
- package/scripts/build-napi-types.mjs +4 -3
- package/scripts/clean-wasm-pack-meta.mjs +32 -0
- package/scripts/copy-napi.mjs +11 -3
- package/scripts/generate-napi-types.mjs +9 -6
- package/scripts/verify-wasm.mjs +29 -1
- package/src/Decimal.ts +203 -134
- package/src/Money.ts +44 -5
- package/src/atlas.ts +2 -5
- package/src/index.ts +75 -36
- package/wasm/atom_engine_wasm.d.ts +60 -0
- package/wasm/atom_engine_wasm.js +416 -0
- package/wasm/atom_engine_wasm_bg.wasm +0 -0
- package/wasm/atom_engine_wasm_bg.wasm.d.ts +17 -0
package/src/Decimal.ts
CHANGED
|
@@ -43,9 +43,16 @@ export interface BetweenOptions {
|
|
|
43
43
|
inclusive?: boolean;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* `quantize` takes a rounding mode and nothing else.
|
|
48
|
+
*
|
|
49
|
+
* It used to accept a `precision` too, which it validated and then ignored:
|
|
50
|
+
* `quantize('0.01', { precision: 4 })` answered `1.23`, the same as without it.
|
|
51
|
+
* There is nothing the option could have meant — the result's scale comes from
|
|
52
|
+
* the step, which is the whole point of quantizing to one.
|
|
53
|
+
*/
|
|
46
54
|
export interface QuantizeOptions {
|
|
47
55
|
mode?: RoundMode;
|
|
48
|
-
precision?: number;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
export interface MedianOptions {
|
|
@@ -139,9 +146,13 @@ export class Decimal {
|
|
|
139
146
|
localesOrRosetta?: Intl.LocalesArgument | RosettaLike,
|
|
140
147
|
): Decimal {
|
|
141
148
|
if (isRosettaLike(localesOrRosetta)) {
|
|
142
|
-
return new Decimal(
|
|
149
|
+
return new Decimal(
|
|
150
|
+
normalizeLocalized(value, shapeFromRosetta(localesOrRosetta)),
|
|
151
|
+
);
|
|
143
152
|
}
|
|
144
|
-
return new Decimal(
|
|
153
|
+
return new Decimal(
|
|
154
|
+
normalizeLocalized(value, shapeFromIntl(localesOrRosetta)),
|
|
155
|
+
);
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
plus(other: DecimalInput): Decimal {
|
|
@@ -332,9 +343,6 @@ export class Decimal {
|
|
|
332
343
|
if (!stepValue.gt(0)) {
|
|
333
344
|
throw new Error("Quantize step must be greater than zero");
|
|
334
345
|
}
|
|
335
|
-
if (options.precision !== undefined) {
|
|
336
|
-
assertScale(options.precision);
|
|
337
|
-
}
|
|
338
346
|
const thisParts = parseDecimal(this.#value);
|
|
339
347
|
const stepParts = parseDecimal(stepValue.#value);
|
|
340
348
|
const numerator = thisParts.int * pow10BigInt(stepParts.scale);
|
|
@@ -466,8 +474,7 @@ export class Decimal {
|
|
|
466
474
|
const remainders: Array<{ index: number; remainder: bigint }> = [];
|
|
467
475
|
let consumed = 0n;
|
|
468
476
|
|
|
469
|
-
for (
|
|
470
|
-
const ratio = normalized[index];
|
|
477
|
+
for (const [index, ratio] of normalized.entries()) {
|
|
471
478
|
const weighted = total * ratio;
|
|
472
479
|
const share = weighted / ratioTotal;
|
|
473
480
|
const remainder = weighted % ratioTotal;
|
|
@@ -485,7 +492,13 @@ export class Decimal {
|
|
|
485
492
|
|
|
486
493
|
let pointer = 0;
|
|
487
494
|
while (left > 0n) {
|
|
488
|
-
|
|
495
|
+
// `remainders` has one entry per ratio and the pointer wraps inside
|
|
496
|
+
// it, so both reads land; naming them is what says so.
|
|
497
|
+
const target = remainders[pointer];
|
|
498
|
+
if (target === undefined) break;
|
|
499
|
+
const share = baseShares[target.index];
|
|
500
|
+
if (share === undefined) break;
|
|
501
|
+
baseShares[target.index] = share + 1n;
|
|
489
502
|
left -= 1n;
|
|
490
503
|
pointer++;
|
|
491
504
|
if (pointer >= remainders.length) pointer = 0;
|
|
@@ -571,10 +584,7 @@ export class Decimal {
|
|
|
571
584
|
if (isRosettaLike(localesOrRosetta)) {
|
|
572
585
|
return localesOrRosetta.formatNumberString(this.#value, options);
|
|
573
586
|
}
|
|
574
|
-
const formatter =
|
|
575
|
-
localesOrRosetta,
|
|
576
|
-
options,
|
|
577
|
-
) as StringFormatter;
|
|
587
|
+
const formatter = stringFormatter(localesOrRosetta, options);
|
|
578
588
|
if (this.isInteger()) {
|
|
579
589
|
return formatter.format(BigInt(this.#value));
|
|
580
590
|
}
|
|
@@ -593,6 +603,20 @@ interface StringFormatter extends Intl.NumberFormat {
|
|
|
593
603
|
format(value: number | bigint | string): string;
|
|
594
604
|
}
|
|
595
605
|
|
|
606
|
+
/**
|
|
607
|
+
* A formatter typed with that overload.
|
|
608
|
+
*
|
|
609
|
+
* A method parameter is compared bivariantly, so the constructed formatter
|
|
610
|
+
* satisfies the wider signature on its own — the assertion that used to sit
|
|
611
|
+
* here was claiming something the compiler could check.
|
|
612
|
+
*/
|
|
613
|
+
function stringFormatter(
|
|
614
|
+
locales?: Intl.LocalesArgument,
|
|
615
|
+
options?: Intl.NumberFormatOptions,
|
|
616
|
+
): StringFormatter {
|
|
617
|
+
return new Intl.NumberFormat(locales, options);
|
|
618
|
+
}
|
|
619
|
+
|
|
596
620
|
function normalizeInput(value: DecimalInput): string {
|
|
597
621
|
if (value instanceof Decimal) {
|
|
598
622
|
return value.toString();
|
|
@@ -642,7 +666,7 @@ function decimalStringFromNumber(value: number): string {
|
|
|
642
666
|
|
|
643
667
|
const [coefficient, exponentRaw] = raw.toLowerCase().split("e");
|
|
644
668
|
const exponent = Number(exponentRaw);
|
|
645
|
-
if (!Number.isInteger(exponent)) {
|
|
669
|
+
if (coefficient === undefined || !Number.isInteger(exponent)) {
|
|
646
670
|
throw new Error(`Invalid decimal: ${value}`);
|
|
647
671
|
}
|
|
648
672
|
const negative = coefficient.startsWith("-");
|
|
@@ -650,7 +674,7 @@ function decimalStringFromNumber(value: number): string {
|
|
|
650
674
|
negative || coefficient.startsWith("+")
|
|
651
675
|
? coefficient.slice(1)
|
|
652
676
|
: coefficient;
|
|
653
|
-
const [wholeRaw, fracRaw = ""] = unsigned.split(".");
|
|
677
|
+
const [wholeRaw = "", fracRaw = ""] = unsigned.split(".");
|
|
654
678
|
const digits = `${wholeRaw}${fracRaw}`;
|
|
655
679
|
const decimalIndex = wholeRaw.length + exponent;
|
|
656
680
|
|
|
@@ -866,85 +890,177 @@ function isRosettaLike(arg: unknown): arg is RosettaLike {
|
|
|
866
890
|
typeof arg === "object" &&
|
|
867
891
|
arg !== null &&
|
|
868
892
|
"getNumberFormatData" in arg &&
|
|
869
|
-
typeof
|
|
893
|
+
typeof arg.getNumberFormatData === "function" &&
|
|
870
894
|
"formatNumberString" in arg &&
|
|
871
|
-
typeof
|
|
895
|
+
typeof arg.formatNumberString === "function"
|
|
872
896
|
);
|
|
873
897
|
}
|
|
874
898
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
899
|
+
/**
|
|
900
|
+
* Everything reading one locale's numbers takes.
|
|
901
|
+
*
|
|
902
|
+
* There is one normalizer, and both ways of naming a locale build one of
|
|
903
|
+
* these for it. Written as two normalizers, the Rosetta one carried a subset
|
|
904
|
+
* of the rules: it substituted the separators but never mapped the locale's
|
|
905
|
+
* digits and validated grouping against the 3-3 of en-US whatever the locale
|
|
906
|
+
* actually grouped by. `parseLocale('١٬٢٣٤٫٥٦', 'ar-EG')` and
|
|
907
|
+
* `parseLocale('١٬٢٣٤٫٥٦', rosetta)` therefore disagreed about the same locale
|
|
908
|
+
* — the second refused it — and so did every Indic locale, whose 3-2 grouping
|
|
909
|
+
* only the first one asked about.
|
|
910
|
+
*/
|
|
911
|
+
interface LocaleNumberShape {
|
|
912
|
+
decimal: string;
|
|
913
|
+
group: string;
|
|
914
|
+
minus: string;
|
|
915
|
+
plusSign: string;
|
|
916
|
+
/** The locale's digits, mapped back to ASCII. Empty when they are ASCII. */
|
|
917
|
+
digits: ReadonlyMap<string, string>;
|
|
918
|
+
grouping: GroupingSpec;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
interface GroupingSpec {
|
|
922
|
+
primary: number;
|
|
923
|
+
secondary: number;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Format a decimal string the way one locale writes it. */
|
|
927
|
+
type FormatNumber = (
|
|
928
|
+
value: string,
|
|
929
|
+
options?: Intl.NumberFormatOptions,
|
|
930
|
+
) => string;
|
|
931
|
+
|
|
932
|
+
function shapeFromIntl(locales?: Intl.LocalesArgument): LocaleNumberShape {
|
|
933
|
+
const format: FormatNumber = (value, options) =>
|
|
934
|
+
stringFormatter(locales, options).format(value);
|
|
935
|
+
const parts = new Intl.NumberFormat(locales).formatToParts(-12345.6);
|
|
936
|
+
const signed = new Intl.NumberFormat(locales, {
|
|
937
|
+
signDisplay: "always",
|
|
938
|
+
}).formatToParts(1);
|
|
939
|
+
const group = parts.find((part) => part.type === "group")?.value ?? ",";
|
|
940
|
+
const digits = digitsOf(format);
|
|
941
|
+
return {
|
|
942
|
+
decimal: parts.find((part) => part.type === "decimal")?.value ?? ".",
|
|
943
|
+
group,
|
|
944
|
+
minus: parts.find((part) => part.type === "minusSign")?.value ?? "-",
|
|
945
|
+
plusSign: signed.find((part) => part.type === "plusSign")?.value ?? "+",
|
|
946
|
+
digits,
|
|
947
|
+
grouping: groupingOf(format, group, digits),
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* The same shape, read off a Rosetta.
|
|
953
|
+
*
|
|
954
|
+
* `getNumberFormatData` names the separators; the digits and the grouping come
|
|
955
|
+
* out of `formatNumberString`, which the same interface already declares — so
|
|
956
|
+
* the Rosetta path honours its fallback chain for all four without asking the
|
|
957
|
+
* caller for a locale tag it deliberately does not take.
|
|
958
|
+
*/
|
|
959
|
+
function shapeFromRosetta(rosetta: RosettaLike): LocaleNumberShape {
|
|
960
|
+
const format: FormatNumber = (value, options) =>
|
|
961
|
+
rosetta.formatNumberString(value, options);
|
|
880
962
|
const raw = rosetta.getNumberFormatData();
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
|
|
884
|
-
const
|
|
963
|
+
// A malformed `RosettaLike` returning empty separators would build regexes
|
|
964
|
+
// matching every position. Fall back to ASCII rather than corrupt the input.
|
|
965
|
+
const group = raw.group || ",";
|
|
966
|
+
const digits = digitsOf(format);
|
|
967
|
+
return {
|
|
885
968
|
decimal: raw.decimal || ".",
|
|
886
|
-
group
|
|
969
|
+
group,
|
|
887
970
|
minus: raw.minus || "-",
|
|
888
971
|
plusSign: raw.plusSign || "+",
|
|
972
|
+
digits,
|
|
973
|
+
grouping: groupingOf(format, group, digits),
|
|
889
974
|
};
|
|
890
|
-
|
|
891
|
-
if (data.minus !== "-") {
|
|
892
|
-
normalized = normalized.replace(
|
|
893
|
-
new RegExp(escapeRegExp(data.minus), "g"),
|
|
894
|
-
"-",
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
normalized = normalized.replace(/[−﹣-]/g, "-");
|
|
898
|
-
// Substitute the locale's plus sign (e.g., U+FF0B `+`,
|
|
899
|
-
// U+FB29 `﬩`) to ASCII so the strict validator accepts it.
|
|
900
|
-
if (data.plusSign !== "+") {
|
|
901
|
-
normalized = normalized.replace(
|
|
902
|
-
new RegExp(escapeRegExp(data.plusSign), "g"),
|
|
903
|
-
"+",
|
|
904
|
-
);
|
|
905
|
-
}
|
|
975
|
+
}
|
|
906
976
|
|
|
907
|
-
|
|
908
|
-
|
|
977
|
+
/**
|
|
978
|
+
* The locale's ten digits, read off the formatter rather than a table.
|
|
979
|
+
*
|
|
980
|
+
* One call names all ten in a known order, so a numbering system whose code
|
|
981
|
+
* points are not contiguous — or one the runtime added after this was written
|
|
982
|
+
* — needs nothing here.
|
|
983
|
+
*/
|
|
984
|
+
function digitsOf(format: FormatNumber): ReadonlyMap<string, string> {
|
|
985
|
+
const map = new Map<string, string>();
|
|
986
|
+
let formatted: string;
|
|
987
|
+
try {
|
|
988
|
+
formatted = format("9876543210", { useGrouping: false });
|
|
989
|
+
} catch {
|
|
990
|
+
return map;
|
|
909
991
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
normalized = normalized.replace(
|
|
917
|
-
new RegExp(escapeRegExp(data.decimal), "g"),
|
|
918
|
-
".",
|
|
919
|
-
);
|
|
920
|
-
|
|
921
|
-
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized)) {
|
|
922
|
-
throw new Error(`Invalid localized decimal: ${input}`);
|
|
992
|
+
let value = 9;
|
|
993
|
+
for (const char of formatted) {
|
|
994
|
+
if (!map.has(char) && value >= 0) {
|
|
995
|
+
map.set(char, String(value));
|
|
996
|
+
value--;
|
|
997
|
+
}
|
|
923
998
|
}
|
|
924
|
-
return
|
|
999
|
+
return map;
|
|
925
1000
|
}
|
|
926
1001
|
|
|
927
|
-
|
|
1002
|
+
/**
|
|
1003
|
+
* How wide the locale's groups are — 3-3 for `1,234,567`, 3-2 for the Indic
|
|
1004
|
+
* `12,34,567`. Measured on a number long enough to show both.
|
|
1005
|
+
*/
|
|
1006
|
+
function groupingOf(
|
|
1007
|
+
format: FormatNumber,
|
|
1008
|
+
group: string,
|
|
1009
|
+
digits: ReadonlyMap<string, string>,
|
|
1010
|
+
): GroupingSpec {
|
|
1011
|
+
const fallback: GroupingSpec = { primary: 3, secondary: 3 };
|
|
1012
|
+
let formatted: string;
|
|
1013
|
+
try {
|
|
1014
|
+
formatted = format("1234567890123");
|
|
1015
|
+
} catch {
|
|
1016
|
+
return fallback;
|
|
1017
|
+
}
|
|
1018
|
+
const segments = toAsciiDigits(formatted, digits).split(group);
|
|
1019
|
+
if (segments.length < 2) return fallback;
|
|
1020
|
+
// Code points, not UTF-16 units: a digit outside the BMP counts once.
|
|
1021
|
+
const lengths = segments.map((segment) => [...segment].length);
|
|
1022
|
+
const primary = lengths.at(-1) ?? 3;
|
|
1023
|
+
const secondary = lengths.at(-2) ?? primary;
|
|
1024
|
+
return { primary, secondary };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function toAsciiDigits(
|
|
928
1028
|
input: string,
|
|
929
|
-
|
|
1029
|
+
digits: ReadonlyMap<string, string>,
|
|
930
1030
|
): string {
|
|
1031
|
+
if (digits.size === 0) return input;
|
|
1032
|
+
let out = "";
|
|
1033
|
+
for (const char of input) {
|
|
1034
|
+
out += digits.get(char) ?? char;
|
|
1035
|
+
}
|
|
1036
|
+
return out;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function normalizeLocalized(input: string, shape: LocaleNumberShape): string {
|
|
931
1040
|
const trimmed = input.trim();
|
|
932
1041
|
if (!trimmed) {
|
|
933
1042
|
throw new Error("Invalid localized decimal: empty string");
|
|
934
1043
|
}
|
|
935
1044
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
normalized = normalized.replace(new RegExp(escapeRegExp(minus), "g"), "-");
|
|
1045
|
+
let normalized = toAsciiDigits(trimmed, shape.digits);
|
|
1046
|
+
// `\s` already covers U+00A0 and U+202F, which is what the space-grouping
|
|
1047
|
+
// locales separate with.
|
|
1048
|
+
normalized = normalized.replace(/\s/g, "");
|
|
1049
|
+
if (shape.minus !== "-") {
|
|
1050
|
+
normalized = normalized.replace(
|
|
1051
|
+
new RegExp(escapeRegExp(shape.minus), "g"),
|
|
1052
|
+
"-",
|
|
1053
|
+
);
|
|
946
1054
|
}
|
|
947
1055
|
normalized = normalized.replace(/[−﹣-]/g, "-");
|
|
1056
|
+
// The locale's plus sign (e.g. U+FF0B `+`, U+FB29 `﬩`) becomes ASCII so the
|
|
1057
|
+
// strict validator below accepts it.
|
|
1058
|
+
if (shape.plusSign !== "+") {
|
|
1059
|
+
normalized = normalized.replace(
|
|
1060
|
+
new RegExp(escapeRegExp(shape.plusSign), "g"),
|
|
1061
|
+
"+",
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
948
1064
|
|
|
949
1065
|
if (/^\(.*\)$/.test(normalized)) {
|
|
950
1066
|
normalized = `-${normalized.slice(1, -1)}`;
|
|
@@ -952,12 +1068,18 @@ function normalizeLocaleNumber(
|
|
|
952
1068
|
|
|
953
1069
|
validateLocalizedSyntax(
|
|
954
1070
|
normalized,
|
|
955
|
-
group,
|
|
956
|
-
decimal,
|
|
957
|
-
|
|
1071
|
+
shape.group,
|
|
1072
|
+
shape.decimal,
|
|
1073
|
+
shape.grouping,
|
|
1074
|
+
);
|
|
1075
|
+
normalized = normalized.replace(
|
|
1076
|
+
new RegExp(escapeRegExp(shape.group), "g"),
|
|
1077
|
+
"",
|
|
1078
|
+
);
|
|
1079
|
+
normalized = normalized.replace(
|
|
1080
|
+
new RegExp(escapeRegExp(shape.decimal), "g"),
|
|
1081
|
+
".",
|
|
958
1082
|
);
|
|
959
|
-
normalized = normalized.replace(new RegExp(escapeRegExp(group), "g"), "");
|
|
960
|
-
normalized = normalized.replace(new RegExp(escapeRegExp(decimal), "g"), ".");
|
|
961
1083
|
|
|
962
1084
|
if (!/^[+-]?\d+(\.\d+)?$/.test(normalized)) {
|
|
963
1085
|
throw new Error(`Invalid localized decimal: ${input}`);
|
|
@@ -965,62 +1087,6 @@ function normalizeLocaleNumber(
|
|
|
965
1087
|
return normalized;
|
|
966
1088
|
}
|
|
967
1089
|
|
|
968
|
-
function normalizeLocaleDigits(
|
|
969
|
-
input: string,
|
|
970
|
-
locales?: Intl.LocalesArgument,
|
|
971
|
-
): string {
|
|
972
|
-
const digitMap = getLocaleDigitMap(locales);
|
|
973
|
-
let out = "";
|
|
974
|
-
for (const char of input) {
|
|
975
|
-
out += digitMap.get(char) ?? char;
|
|
976
|
-
}
|
|
977
|
-
return out;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function getLocaleDigitMap(
|
|
981
|
-
locales?: Intl.LocalesArgument,
|
|
982
|
-
): Map<string, string> {
|
|
983
|
-
const formatter = new Intl.NumberFormat(locales, { useGrouping: false });
|
|
984
|
-
const digits = formatter.format(9876543210);
|
|
985
|
-
const map = new Map<string, string>();
|
|
986
|
-
let value = 9;
|
|
987
|
-
for (const char of digits) {
|
|
988
|
-
if (!map.has(char) && value >= 0) {
|
|
989
|
-
map.set(char, String(value));
|
|
990
|
-
value--;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
return map;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
interface GroupingSpec {
|
|
997
|
-
primary: number;
|
|
998
|
-
secondary: number;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
function getLocaleGrouping(locales?: Intl.LocalesArgument): GroupingSpec {
|
|
1002
|
-
const parts = new Intl.NumberFormat(locales)
|
|
1003
|
-
.formatToParts(1234567890123)
|
|
1004
|
-
.filter((part) => part.type === "integer" || part.type === "group");
|
|
1005
|
-
const lengths: number[] = [];
|
|
1006
|
-
let current = 0;
|
|
1007
|
-
for (const part of parts) {
|
|
1008
|
-
if (part.type === "integer") {
|
|
1009
|
-
current += [...part.value].length;
|
|
1010
|
-
} else {
|
|
1011
|
-
lengths.push(current);
|
|
1012
|
-
current = 0;
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
lengths.push(current);
|
|
1016
|
-
if (lengths.length < 2) {
|
|
1017
|
-
return { primary: 3, secondary: 3 };
|
|
1018
|
-
}
|
|
1019
|
-
const primary = lengths[lengths.length - 1];
|
|
1020
|
-
const secondary = lengths[lengths.length - 2] ?? primary;
|
|
1021
|
-
return { primary, secondary };
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
1090
|
function validateLocalizedSyntax(
|
|
1025
1091
|
value: string,
|
|
1026
1092
|
group: string,
|
|
@@ -1054,11 +1120,14 @@ function validateLocalizedSyntax(
|
|
|
1054
1120
|
}
|
|
1055
1121
|
for (let i = groups.length - 1, distance = 0; i >= 0; i--, distance++) {
|
|
1056
1122
|
const size = distance === 0 ? grouping.primary : grouping.secondary;
|
|
1123
|
+
const group = groups[i];
|
|
1124
|
+
if (group === undefined)
|
|
1125
|
+
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1057
1126
|
if (i === 0) {
|
|
1058
|
-
if (
|
|
1127
|
+
if (group.length < 1 || group.length > size) {
|
|
1059
1128
|
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1060
1129
|
}
|
|
1061
|
-
} else if (
|
|
1130
|
+
} else if (group.length !== size) {
|
|
1062
1131
|
throw new Error(`Invalid localized decimal: ${value}`);
|
|
1063
1132
|
}
|
|
1064
1133
|
}
|
package/src/Money.ts
CHANGED
|
@@ -7,18 +7,53 @@ export interface MoneyOptions {
|
|
|
7
7
|
mode?: RoundMode;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* What `times` and `div` take.
|
|
12
|
+
*
|
|
13
|
+
* The rounding mode, and only that. They used to declare the whole
|
|
14
|
+
* {@link MoneyOptions} and forward one field of it: `scale` and `exact` were
|
|
15
|
+
* overwritten on the way through, so `.times("1.5", { scale: 4 })` came back at
|
|
16
|
+
* the currency's scale and `.times("1.333", { exact: true })` rounded anyway.
|
|
17
|
+
* Neither could ever have been honoured — a multiplied amount of money is still
|
|
18
|
+
* money, and money is held at the scale its currency has.
|
|
19
|
+
*/
|
|
20
|
+
export interface MoneyRoundingOptions {
|
|
21
|
+
mode?: RoundMode;
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
export interface MoneyFormatOptions
|
|
11
25
|
extends Omit<Intl.NumberFormatOptions, "style" | "currency"> {
|
|
12
26
|
locale?: Intl.LocalesArgument;
|
|
13
27
|
}
|
|
14
28
|
|
|
29
|
+
/**
|
|
30
|
+
* ISO 4217 minor units, for every currency that does not have two.
|
|
31
|
+
*
|
|
32
|
+
* Only the exceptions are listed; anything absent is two, which is what the
|
|
33
|
+
* lookups below fall back to. Written as a partial list of the ones someone
|
|
34
|
+
* happened to think of, the fallback answered for the rest — and answered
|
|
35
|
+
* wrong. A `Money` built for ISK, whose króna has no subunit, was held at two
|
|
36
|
+
* decimals: `toMinorUnits()` returned 123400 for 1234 króna, a hundredfold
|
|
37
|
+
* error on its way into an integer column, `toString()` printed a fractional
|
|
38
|
+
* part the currency does not have, and `format()` asked Intl for two decimals
|
|
39
|
+
* it would otherwise have refused to print. JPY, three lines away in the same
|
|
40
|
+
* table, was correct — the difference between the two was which one someone
|
|
41
|
+
* had typed in.
|
|
42
|
+
*
|
|
43
|
+
* ISO is the source of truth here, not CLDR, and they disagree on one entry:
|
|
44
|
+
* ISO gives IQD three minor units, `Intl` prints zero. `format()` passes this
|
|
45
|
+
* scale to Intl explicitly, so the value keeps the fils ISO says it has.
|
|
46
|
+
*/
|
|
15
47
|
const ISO_MINOR_UNITS: Record<string, number> = Object.freeze({
|
|
16
48
|
BHD: 3,
|
|
49
|
+
BIF: 0,
|
|
17
50
|
CLF: 4,
|
|
18
51
|
CLP: 0,
|
|
19
52
|
DJF: 0,
|
|
20
|
-
|
|
21
|
-
|
|
53
|
+
GNF: 0,
|
|
54
|
+
IQD: 3,
|
|
55
|
+
ISK: 0,
|
|
56
|
+
JOD: 3,
|
|
22
57
|
JPY: 0,
|
|
23
58
|
KMF: 0,
|
|
24
59
|
KRW: 0,
|
|
@@ -26,9 +61,13 @@ const ISO_MINOR_UNITS: Record<string, number> = Object.freeze({
|
|
|
26
61
|
LYD: 3,
|
|
27
62
|
OMR: 3,
|
|
28
63
|
PYG: 0,
|
|
64
|
+
RWF: 0,
|
|
29
65
|
TND: 3,
|
|
30
|
-
|
|
66
|
+
UGX: 0,
|
|
67
|
+
UYI: 0,
|
|
68
|
+
UYW: 4,
|
|
31
69
|
VND: 0,
|
|
70
|
+
VUV: 0,
|
|
32
71
|
XAF: 0,
|
|
33
72
|
XOF: 0,
|
|
34
73
|
XPF: 0,
|
|
@@ -102,7 +141,7 @@ export class Money {
|
|
|
102
141
|
});
|
|
103
142
|
}
|
|
104
143
|
|
|
105
|
-
times(multiplier: DecimalInput, options:
|
|
144
|
+
times(multiplier: DecimalInput, options: MoneyRoundingOptions = {}): Money {
|
|
106
145
|
return new Money(this.#amount.times(multiplier), this.#currency, {
|
|
107
146
|
scale: this.#scale,
|
|
108
147
|
exact: false,
|
|
@@ -110,7 +149,7 @@ export class Money {
|
|
|
110
149
|
});
|
|
111
150
|
}
|
|
112
151
|
|
|
113
|
-
div(divisor: DecimalInput, options:
|
|
152
|
+
div(divisor: DecimalInput, options: MoneyRoundingOptions = {}): Money {
|
|
114
153
|
return new Money(this.#amount.div(divisor), this.#currency, {
|
|
115
154
|
scale: this.#scale,
|
|
116
155
|
exact: false,
|
package/src/atlas.ts
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* column pipeline. Lives on a sub-export so the default `@c9up/atom` import
|
|
6
6
|
* surface stays adapter-free.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* boot-time wiring.
|
|
8
|
+
* The callbacks are baked into the entity definition: no global registry, no
|
|
9
|
+
* boot-time wiring, nothing to import in a provider.
|
|
11
10
|
*
|
|
12
11
|
* Usage:
|
|
13
12
|
*
|
|
@@ -19,8 +18,6 @@
|
|
|
19
18
|
* @PrimaryKey() id!: number
|
|
20
19
|
* @Column(decimalAtlasAdapter) balance!: Decimal | null
|
|
21
20
|
* }
|
|
22
|
-
*
|
|
23
|
-
* @implements Story 35.10
|
|
24
21
|
*/
|
|
25
22
|
|
|
26
23
|
import { Decimal, type RoundMode } from "./Decimal.js";
|