@amboras-dev/money 0.0.0-stage → 0.1.0
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 +393 -2
- package/dist/ai.cjs +185 -0
- package/dist/ai.d.cts +97 -0
- package/dist/ai.d.ts +97 -0
- package/dist/ai.js +175 -0
- package/dist/allocate-CdnhvAda.js +199 -0
- package/dist/allocate-D7tm_t_o.cjs +222 -0
- package/dist/amount-D7dj76nH.js +200 -0
- package/dist/amount-DHtvZSSm.cjs +241 -0
- package/dist/arithmetic-Bq-8mSal.cjs +233 -0
- package/dist/arithmetic-C-Q_aI49.js +132 -0
- package/dist/billing.cjs +184 -0
- package/dist/billing.d.cts +81 -0
- package/dist/billing.d.ts +81 -0
- package/dist/billing.js +175 -0
- package/dist/commerce.cjs +536 -0
- package/dist/commerce.d.cts +293 -0
- package/dist/commerce.d.ts +293 -0
- package/dist/commerce.js +516 -0
- package/dist/format-BdB7LEIp.js +90 -0
- package/dist/format-BnA1aLJk.cjs +113 -0
- package/dist/fx.cjs +407 -0
- package/dist/fx.d.cts +157 -0
- package/dist/fx.d.ts +157 -0
- package/dist/fx.js +374 -0
- package/dist/index.cjs +349 -0
- package/dist/index.d.cts +288 -0
- package/dist/index.d.ts +288 -0
- package/dist/index.js +276 -0
- package/dist/input.cjs +163 -0
- package/dist/input.d.cts +203 -0
- package/dist/input.d.ts +203 -0
- package/dist/input.js +153 -0
- package/dist/iso-minor--gQWI1f2.cjs +65 -0
- package/dist/iso-minor-CtaiIifs.js +48 -0
- package/dist/medusa-CmxCni-G.js +101 -0
- package/dist/medusa-DuQ3-KoD.d.cts +78 -0
- package/dist/medusa-OJ2TJOxt.cjs +154 -0
- package/dist/medusa-RwqxaDGq.d.ts +78 -0
- package/dist/money-CIgELekG.js +3571 -0
- package/dist/money-CbhsT0fB.d.cts +232 -0
- package/dist/money-CbhsT0fB.d.ts +232 -0
- package/dist/money-D5-vlv0h.cjs +3960 -0
- package/dist/number-text-BpvN6NAo.cjs +145 -0
- package/dist/number-text-C82oh6JB.js +134 -0
- package/dist/own-D7544D3k.js +4 -0
- package/dist/own-oP9DXFWv.cjs +9 -0
- package/dist/percent-CKVI_c-m.js +20 -0
- package/dist/percent-Dt8vNIAW.cjs +43 -0
- package/dist/rate-CmeTGNex.d.ts +63 -0
- package/dist/rate-DGPfB_jc.cjs +217 -0
- package/dist/rate-lLPViBuM.js +152 -0
- package/dist/rate-toyS3EMf.d.cts +63 -0
- package/dist/reporting-5xby-Zas.d.ts +31 -0
- package/dist/reporting-DEM97DhQ.d.cts +31 -0
- package/dist/retail-BsvpNv1F.js +69 -0
- package/dist/retail-BwcFJcVN.d.ts +31 -0
- package/dist/retail-DTk2XAMM.cjs +86 -0
- package/dist/retail-Mqd-sPMF.d.cts +31 -0
- package/dist/rounding-CVAPEfq_.js +50 -0
- package/dist/rounding-oZ832yTD.cjs +79 -0
- package/dist/sql.cjs +290 -0
- package/dist/sql.d.cts +147 -0
- package/dist/sql.d.ts +147 -0
- package/dist/sql.js +271 -0
- package/dist/testing.cjs +28518 -0
- package/dist/testing.d.cts +97 -0
- package/dist/testing.d.ts +97 -0
- package/dist/testing.js +28510 -0
- package/dist/wire-CI3Sm1iC.js +1675 -0
- package/dist/wire-Cd4Q1Jmm.d.ts +269 -0
- package/dist/wire-DQZ6fA3c.cjs +1872 -0
- package/dist/wire-hWuixv3w.d.cts +269 -0
- package/dist/wire.cjs +35 -0
- package/dist/wire.d.cts +2 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.js +2 -0
- package/package.json +158 -3
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
const require_money = require("./money-D5-vlv0h.cjs");
|
|
2
|
+
const require_rounding = require("./rounding-oZ832yTD.cjs");
|
|
3
|
+
//#region src/allocate.ts
|
|
4
|
+
const invalid = (message) => new require_money.MoneyError("amount_invalid", message);
|
|
5
|
+
const sumOf = (values) => values.reduce((a, b) => a + b, 0n);
|
|
6
|
+
const floorShares = (units, weights, total) => weights.map((w) => require_money.divRound(units * w, total, "floor"));
|
|
7
|
+
function largestRemainder(units, weights, total) {
|
|
8
|
+
const shares = floorShares(units, weights, total);
|
|
9
|
+
let rest = units - sumOf(shares);
|
|
10
|
+
const order = weights.map((w, i) => ({
|
|
11
|
+
i,
|
|
12
|
+
fraction: units * w % total
|
|
13
|
+
})).sort((a, b) => a.fraction === b.fraction ? a.i - b.i : a.fraction > b.fraction ? -1 : 1);
|
|
14
|
+
for (const { i } of order) {
|
|
15
|
+
if (rest === 0n) break;
|
|
16
|
+
shares[i] += 1n;
|
|
17
|
+
rest -= 1n;
|
|
18
|
+
}
|
|
19
|
+
return shares;
|
|
20
|
+
}
|
|
21
|
+
function largestWeight(units, weights, total) {
|
|
22
|
+
const top = weights.indexOf(weights.reduce((a, b) => b > a ? b : a));
|
|
23
|
+
const shares = weights.map((w, i) => i === top ? 0n : require_money.mulDiv(units, w, total, "half-up"));
|
|
24
|
+
let deficit = sumOf(shares) - units;
|
|
25
|
+
if (deficit <= 0n) {
|
|
26
|
+
shares[top] = -deficit;
|
|
27
|
+
return shares;
|
|
28
|
+
}
|
|
29
|
+
const byWeight = weights.map((w, i) => ({
|
|
30
|
+
w,
|
|
31
|
+
i
|
|
32
|
+
})).sort((a, b) => a.w === b.w ? a.i - b.i : a.w > b.w ? -1 : 1);
|
|
33
|
+
for (const { i } of byWeight) {
|
|
34
|
+
const take = shares[i] < deficit ? shares[i] : deficit;
|
|
35
|
+
shares[i] -= take;
|
|
36
|
+
deficit -= take;
|
|
37
|
+
}
|
|
38
|
+
return shares;
|
|
39
|
+
}
|
|
40
|
+
function lastAbsorbs(units, weights, total) {
|
|
41
|
+
const shares = floorShares(units, weights, total);
|
|
42
|
+
let last = weights.length - 1;
|
|
43
|
+
while (weights[last] === 0n) last -= 1;
|
|
44
|
+
shares[last] += units - sumOf(shares);
|
|
45
|
+
return shares;
|
|
46
|
+
}
|
|
47
|
+
function cumulative(units, weights, total) {
|
|
48
|
+
let running = 0n;
|
|
49
|
+
let previous = 0n;
|
|
50
|
+
return weights.map((w) => {
|
|
51
|
+
running += w;
|
|
52
|
+
const reached = require_money.mulDiv(units, running, total, "half-up");
|
|
53
|
+
const share = reached - previous;
|
|
54
|
+
previous = reached;
|
|
55
|
+
return share;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function lastFirst(amount, weights, total, step, caps) {
|
|
59
|
+
const capTotal = sumOf(caps);
|
|
60
|
+
const target = amount < capTotal ? amount : capTotal;
|
|
61
|
+
const shares = weights.map((w, i) => {
|
|
62
|
+
const share = require_money.mulDiv(target, w, total * step, "half-up") * step;
|
|
63
|
+
return share < caps[i] ? share : caps[i];
|
|
64
|
+
});
|
|
65
|
+
let rest = target - sumOf(shares);
|
|
66
|
+
for (let i = shares.length - 1; i >= 0 && rest !== 0n; i--) {
|
|
67
|
+
const wanted = shares[i] + rest;
|
|
68
|
+
const next = wanted < 0n ? 0n : wanted > caps[i] ? caps[i] : wanted;
|
|
69
|
+
rest -= next - shares[i];
|
|
70
|
+
shares[i] = next;
|
|
71
|
+
}
|
|
72
|
+
return shares;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Internal: `total` split across non-negative integer `weights` in integer units, exactly.
|
|
76
|
+
* Shares are multiples of `step`; a negative total is split by magnitude and negated;
|
|
77
|
+
* all-zero weights need a zero total. Throws `amount_invalid` (weights, caps, step) or
|
|
78
|
+
* `precision` (total not a multiple of step).
|
|
79
|
+
*/
|
|
80
|
+
function allocateUnits(total, weights, options = {}) {
|
|
81
|
+
const strategy = options.strategy ?? "largest-remainder";
|
|
82
|
+
const step = options.step ?? 1n;
|
|
83
|
+
if (step <= 0n) throw invalid(`Allocation step must be positive, got ${step}`);
|
|
84
|
+
if (weights.some((w) => w < 0n)) throw invalid("Allocation weights must not be negative");
|
|
85
|
+
if (total % step !== 0n) throw new require_money.MoneyError("precision", `${total} is not a whole number of ${step}-unit steps; round it first`);
|
|
86
|
+
const weightTotal = sumOf(weights);
|
|
87
|
+
const negative = total < 0n;
|
|
88
|
+
const magnitude = negative ? -total : total;
|
|
89
|
+
let shares;
|
|
90
|
+
if (weightTotal === 0n) {
|
|
91
|
+
if (magnitude !== 0n) throw invalid("Cannot allocate a non-zero total across zero weights");
|
|
92
|
+
shares = weights.map(() => 0n);
|
|
93
|
+
} else if (strategy === "last-first") {
|
|
94
|
+
const caps = options.caps;
|
|
95
|
+
if (!caps || caps.length !== weights.length || caps.some((c) => c < 0n)) throw invalid("'last-first' needs one non-negative cap per weight");
|
|
96
|
+
shares = lastFirst(magnitude, weights, weightTotal, step, caps);
|
|
97
|
+
} else {
|
|
98
|
+
const units = magnitude / step;
|
|
99
|
+
const split = strategy === "largest-weight" ? largestWeight : strategy === "last" ? lastAbsorbs : strategy === "cumulative" ? cumulative : strategy === "largest-remainder" ? largestRemainder : void 0;
|
|
100
|
+
if (!split) throw invalid(`Unknown allocation strategy: ${require_money.describeValue(strategy)}`);
|
|
101
|
+
shares = split(units, weights, weightTotal).map((s) => s * step);
|
|
102
|
+
}
|
|
103
|
+
return negative ? shares.map((s) => s === 0n ? 0n : -s) : shares;
|
|
104
|
+
}
|
|
105
|
+
/** Weights as integers on one common scale. Money weights must be in `currency` (when given). */
|
|
106
|
+
function weightUnits(weights, info) {
|
|
107
|
+
const values = weights.map((w) => {
|
|
108
|
+
if (typeof w !== "object" || w === null) return require_money.decimalOf(w, true, "weight");
|
|
109
|
+
const own = require_money.storedDecimal(w).info;
|
|
110
|
+
if (info && own.code !== info.code) throw new require_money.MoneyError("currency_mismatch", `Weight in ${own.code}, total in ${info.code}`);
|
|
111
|
+
return {
|
|
112
|
+
units: BigInt(w.amount),
|
|
113
|
+
scale: 0
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
const scale = Math.max(0, ...values.map((v) => v.scale));
|
|
117
|
+
return values.map((v) => require_money.roundToScale(v, scale, "down").units);
|
|
118
|
+
}
|
|
119
|
+
function stepOf(info, unit) {
|
|
120
|
+
if (typeof unit === "object" && unit !== null) {
|
|
121
|
+
if (!Number.isSafeInteger(unit.step) || unit.step < 1) throw invalid(`unit.step must be a positive integer, got ${require_money.describeValue(unit.step)}`);
|
|
122
|
+
return BigInt(unit.step);
|
|
123
|
+
}
|
|
124
|
+
return require_rounding.unitSize(info, unit ?? "storage");
|
|
125
|
+
}
|
|
126
|
+
function allocate(total, weights, options = {}) {
|
|
127
|
+
if (typeof total === "object" && total !== null) {
|
|
128
|
+
const { info } = require_money.storedDecimal(total);
|
|
129
|
+
const units = weightUnits(weights, info);
|
|
130
|
+
let caps;
|
|
131
|
+
if (options.strategy === "last-first") caps = (options.caps ?? (weights.every((w) => typeof w === "object" && w !== null) ? weights : void 0))?.map((c) => weightUnits([c], info)[0]);
|
|
132
|
+
return allocateUnits(BigInt(total.amount), units, {
|
|
133
|
+
strategy: options.strategy,
|
|
134
|
+
step: stepOf(info, options.unit),
|
|
135
|
+
caps
|
|
136
|
+
}).map((s) => require_money.boundedMoney(s, info));
|
|
137
|
+
}
|
|
138
|
+
const scale = options.scale;
|
|
139
|
+
if (scale === void 0 || !Number.isSafeInteger(scale) || scale < 0 || scale > 100) throw invalid(`scale must be an integer from 0 to 100, got ${require_money.describeValue(scale)}`);
|
|
140
|
+
if (options.strategy === "last-first") throw invalid("'last-first' needs caps: allocate Money instead");
|
|
141
|
+
const value = require_money.decimalOf(total, true, "total");
|
|
142
|
+
if (require_money.fractionDigitsOf(value) > scale) throw new require_money.MoneyError("precision", `${total} has more than ${scale} fraction digits`);
|
|
143
|
+
return allocateUnits(require_money.roundToScale(value, scale, "down").units, weightUnits(weights, null), { strategy: options.strategy }).map((s) => require_money.toDecimalText({
|
|
144
|
+
units: s,
|
|
145
|
+
scale
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Greedy capped allocation in the given order: `take_i = min(left, cap_i)`; `shortfall` is
|
|
150
|
+
* what could not be placed. With `allOrNothing`, a shortfall allocates nothing (all zeros,
|
|
151
|
+
* `shortfall = amount`). Negative caps or amount throw `amount_invalid`; all one currency.
|
|
152
|
+
*/
|
|
153
|
+
function allocateWaterfall(caps, amount, options = {}) {
|
|
154
|
+
const { info } = require_money.storedDecimal(amount);
|
|
155
|
+
const limits = caps.map((c) => weightUnits([c], info)[0]);
|
|
156
|
+
let left = BigInt(amount.amount);
|
|
157
|
+
if (left < 0n || limits.some((c) => c < 0n)) throw invalid("Waterfall caps and amount must not be negative");
|
|
158
|
+
const takes = limits.map((cap) => {
|
|
159
|
+
const take = cap < left ? cap : left;
|
|
160
|
+
left -= take;
|
|
161
|
+
return take;
|
|
162
|
+
});
|
|
163
|
+
if (left > 0n && options.allOrNothing) return {
|
|
164
|
+
allocations: takes.map(() => require_money.boundedMoney(0n, info)),
|
|
165
|
+
shortfall: require_money.boundedMoney(BigInt(amount.amount), info)
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
allocations: takes.map((t) => require_money.boundedMoney(t, info)),
|
|
169
|
+
shortfall: require_money.boundedMoney(left, info)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* A line total charged at one unit price, or split in two parts one `unit` apart when it does
|
|
174
|
+
* not divide by `quantity`, so `sum(quantity x unitPrice) === net` exactly (JPY 100000 x 3
|
|
175
|
+
* gives `2 @ 33333`, `1 @ 33334`). Zero-quantity parts are omitted. `quantity` must be a
|
|
176
|
+
* positive integer; `net` a whole `unit` (else `precision`).
|
|
177
|
+
*/
|
|
178
|
+
function splitUnitPrice(net, quantity, options = {}) {
|
|
179
|
+
const { info } = require_money.storedDecimal(net);
|
|
180
|
+
if (!Number.isSafeInteger(quantity) || quantity < 1) throw invalid(`quantity must be a positive integer, got ${require_money.describeValue(quantity)}`);
|
|
181
|
+
const unit = require_rounding.unitSize(info, options.unit ?? "storage");
|
|
182
|
+
const amount = BigInt(net.amount);
|
|
183
|
+
if (amount % unit !== 0n) throw new require_money.MoneyError("precision", `${net.amount} is not a whole number of ${unit}-unit steps`);
|
|
184
|
+
const units = amount / unit;
|
|
185
|
+
const q = BigInt(quantity);
|
|
186
|
+
const per = require_money.divRound(units, q, "floor");
|
|
187
|
+
const extra = units - per * q;
|
|
188
|
+
const parts = [{
|
|
189
|
+
quantity: Number(q - extra),
|
|
190
|
+
unitPrice: require_money.boundedMoney(per * unit, info)
|
|
191
|
+
}];
|
|
192
|
+
if (extra > 0n) parts.push({
|
|
193
|
+
quantity: Number(extra),
|
|
194
|
+
unitPrice: require_money.boundedMoney((per + 1n) * unit, info)
|
|
195
|
+
});
|
|
196
|
+
return parts;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
Object.defineProperty(exports, "allocate", {
|
|
200
|
+
enumerable: true,
|
|
201
|
+
get: function() {
|
|
202
|
+
return allocate;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
Object.defineProperty(exports, "allocateUnits", {
|
|
206
|
+
enumerable: true,
|
|
207
|
+
get: function() {
|
|
208
|
+
return allocateUnits;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
Object.defineProperty(exports, "allocateWaterfall", {
|
|
212
|
+
enumerable: true,
|
|
213
|
+
get: function() {
|
|
214
|
+
return allocateWaterfall;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
Object.defineProperty(exports, "splitUnitPrice", {
|
|
218
|
+
enumerable: true,
|
|
219
|
+
get: function() {
|
|
220
|
+
return splitUnitPrice;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { E as resolveCurrency, K as trimDecimalZeros, M as resolveLocale, N as separatorsOf, Z as MoneyError, c as toMajorDecimal, et as compareDecimal, it as fractionDigitsOf, n as fromMajor, rt as formatDecimal, s as storedDecimal } from "./money-CIgELekG.js";
|
|
2
|
+
import { r as compare } from "./arithmetic-C-Q_aI49.js";
|
|
3
|
+
import { n as readNumberText } from "./number-text-C82oh6JB.js";
|
|
4
|
+
//#region src/input/amount.ts
|
|
5
|
+
/** Internal: the exact signed decimal a successful separator-engine read holds. */
|
|
6
|
+
function decimalOfText(read) {
|
|
7
|
+
return {
|
|
8
|
+
units: BigInt(read.sign) * read.units,
|
|
9
|
+
scale: read.scale
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Internal: the locale's separators, as the engine takes them. */
|
|
13
|
+
function localeSeparators(locale) {
|
|
14
|
+
const { decimal, group } = separatorsOf(resolveLocale(locale));
|
|
15
|
+
return {
|
|
16
|
+
decimalSeparator: decimal,
|
|
17
|
+
groupSeparator: group
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Internal: a read decimal as Money, checked like typed entry (shared by parseModelMoney). */
|
|
21
|
+
function entryMoney(value, info, options) {
|
|
22
|
+
const echo = options.original !== void 0 && compareDecimal(value, storedDecimal(options.original).value) === 0;
|
|
23
|
+
const round = options.roundExtraDigits === true;
|
|
24
|
+
if (!echo && !round && fractionDigitsOf(value) > info.storageDigits) return {
|
|
25
|
+
ok: false,
|
|
26
|
+
reason: "too_many_decimals"
|
|
27
|
+
};
|
|
28
|
+
if (value.units < 0n && !options.allowNegative) return {
|
|
29
|
+
ok: false,
|
|
30
|
+
reason: "negative"
|
|
31
|
+
};
|
|
32
|
+
if (value.units === 0n && options.allowZero === false) return {
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: "zero"
|
|
35
|
+
};
|
|
36
|
+
let money;
|
|
37
|
+
if (echo) money = options.original;
|
|
38
|
+
else try {
|
|
39
|
+
money = fromMajor(formatDecimal(value), info.code, { precision: round ? "round" : "reject" });
|
|
40
|
+
} catch {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: "above_max"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (options.min && compare(money, options.min) < 0) return {
|
|
47
|
+
ok: false,
|
|
48
|
+
reason: "below_min"
|
|
49
|
+
};
|
|
50
|
+
if (options.max && compare(money, options.max) > 0) return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: "above_max"
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
money
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Whether `shown` is exactly what `formatAmountInput(original)` prints for the field's locale or mode, grouped or not. */
|
|
60
|
+
function echoesOriginal(shown, options) {
|
|
61
|
+
const { original, locale, decimalSeparator } = options;
|
|
62
|
+
if (original === void 0) return false;
|
|
63
|
+
const field = {
|
|
64
|
+
locale,
|
|
65
|
+
decimalSeparator
|
|
66
|
+
};
|
|
67
|
+
return shown === formatAmountInput(original, field) || shown === formatAmountInput(original, {
|
|
68
|
+
...field,
|
|
69
|
+
grouped: true
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Which reading a field's options ask for; `decimalSeparator` and `locale` together, or a `decimalSeparator` other than `'.'`, throw. */
|
|
73
|
+
function entryMode(options) {
|
|
74
|
+
if (options.decimalSeparator === void 0) return options.locale === void 0 ? "auto" : "locale";
|
|
75
|
+
if (options.decimalSeparator !== "." || options.locale !== void 0) throw new TypeError("decimalSeparator must be '.' and cannot be combined with locale");
|
|
76
|
+
return "machine";
|
|
77
|
+
}
|
|
78
|
+
const ENTRY_SEPARATORS = {
|
|
79
|
+
auto: {
|
|
80
|
+
decimalSeparator: "auto",
|
|
81
|
+
keepFraction: true
|
|
82
|
+
},
|
|
83
|
+
machine: {
|
|
84
|
+
decimalSeparator: ".",
|
|
85
|
+
groupSeparator: null
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Typed text to Money, one rule for every money field: the currency's symbols and code and all
|
|
90
|
+
* whitespace are stripped; a leading `-` or U+2212 is the sign.
|
|
91
|
+
* - Without `locale` (the chat wizard, any field with no locale): the 'auto' rule decides `.`
|
|
92
|
+
* against `,` (see `parseImportAmount`): `'29,99'` EUR is 29.99, `'1.299,00'` and `'1,299'`
|
|
93
|
+
* are 1299, KWD `'1,5'` is 1.5; a currency storing 0 digits rounds a fraction half-up to a
|
|
94
|
+
* whole unit (`'1200.5'` JPY is 1201).
|
|
95
|
+
* - With `locale`: its decimal separator is the decimal and its group separator is valid only in
|
|
96
|
+
* 3-digit (or Indian lakh) groups; in a currency that stores no fraction (JPY, VND) a separator
|
|
97
|
+
* before exactly three digits in grouping position is grouping (`'1.500'` JPY is 1500).
|
|
98
|
+
* en-US `'29,99'` USD is `'unparseable'`; de-DE `'29,99'` EUR is 29.99; en-US `'1200.5'` JPY
|
|
99
|
+
* is `'too_many_decimals'`.
|
|
100
|
+
* - With `decimalSeparator: '.'` (every `<input type="number">`): `.` is the decimal, nothing
|
|
101
|
+
* groups (`'1,299'` is `'unparseable'`), TND and IQD `'1.500'` are 1.5, and a currency storing
|
|
102
|
+
* 0 digits rounds a fraction half-up to a whole unit (`'1.5'` JPY is 2).
|
|
103
|
+
* Otherwise more fraction digits than the currency stores is `'too_many_decimals'`, never rounded
|
|
104
|
+
* (`'1,2345'` USD). Text equal to `original` returns it unchanged (JPY 120050 shown as
|
|
105
|
+
* `'1200.50'`). An unknown currency, or an `original` in another currency, throws.
|
|
106
|
+
*/
|
|
107
|
+
function parseAmountInput(text, currency, options = {}) {
|
|
108
|
+
const info = resolveCurrency(currency, { allowHistoric: true });
|
|
109
|
+
if (options.original !== void 0 && storedDecimal(options.original).info.code !== info.code) throw new MoneyError("currency_mismatch", `original is ${options.original.currency}, the field is ${info.code}`);
|
|
110
|
+
const shown = typeof text === "string" ? text.trim() : "";
|
|
111
|
+
const mode = entryMode(options);
|
|
112
|
+
let value;
|
|
113
|
+
if (echoesOriginal(shown, options)) value = storedDecimal(options.original).value;
|
|
114
|
+
else {
|
|
115
|
+
const separators = mode === "locale" ? localeSeparators(options.locale) : ENTRY_SEPARATORS[mode];
|
|
116
|
+
const read = readNumberText(text, {
|
|
117
|
+
...separators,
|
|
118
|
+
currency: info.code
|
|
119
|
+
});
|
|
120
|
+
if ("reason" in read) return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: read.reason,
|
|
123
|
+
text: shown
|
|
124
|
+
};
|
|
125
|
+
value = decimalOfText(read);
|
|
126
|
+
}
|
|
127
|
+
const result = entryMoney(value, info, {
|
|
128
|
+
...options,
|
|
129
|
+
roundExtraDigits: mode !== "locale" && info.storageDigits === 0
|
|
130
|
+
});
|
|
131
|
+
if (!result.ok) return {
|
|
132
|
+
ok: false,
|
|
133
|
+
reason: result.reason,
|
|
134
|
+
text: shown
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
money: result.money,
|
|
139
|
+
text: toMajorDecimal(result.money)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Internal: `digits` with `group` between every three, from the right. */
|
|
143
|
+
function groupDigits(digits, group) {
|
|
144
|
+
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, group);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The text a money field shows for a stored amount, no symbol, exact: a stored amount finer than
|
|
148
|
+
* the storage digits keeps its digits (JPY 120050 is never rounded on display).
|
|
149
|
+
* - Without `locale`: the spelling `parseAmountInput` reads back without a locale. Trailing
|
|
150
|
+
* fraction zeros are dropped (`'12.5'`, `'1200'`, KWD 1500 is `'1.5'`, JPY 120050 is
|
|
151
|
+
* `'1200.5'`), a currency written with a decimal comma (TND, `commaDecimal`) uses `,` and
|
|
152
|
+
* groups with `.` (`'1,125'`, `'1.234,5'`), every other currency `.` and `,`. Two amounts have no
|
|
153
|
+
* such spelling: IQD with a three-digit fraction (`'1.125'` reads as 1125, IQD displays no
|
|
154
|
+
* decimals) and a sub-unit amount of a currency storing 0 digits (`'1200.5'` JPY reads as a
|
|
155
|
+
* whole 1201); pass `{ original: m }` so the untouched text returns `m` itself.
|
|
156
|
+
* - With `locale`: its decimal and group separators and the storage digits (`'12,50'` de-DE).
|
|
157
|
+
* - With `decimalSeparator: '.'` (the value of an `<input type="number">`): `.` decimal, never
|
|
158
|
+
* grouped, trailing zeros dropped (TND 1500 is `'1.5'`, JPY 120050 `'1200.5'`), read back
|
|
159
|
+
* exactly by `parseAmountInput(text, currency, { decimalSeparator: '.' })` (JPY sub-unit
|
|
160
|
+
* amounts through `original`).
|
|
161
|
+
* `grouped` adds group separators (not in `'.'` mode); `digits` overrides the zeros
|
|
162
|
+
* (`'storage'` keeps them, `'trim-zero'` drops them).
|
|
163
|
+
*/
|
|
164
|
+
function formatAmountInput(m, options = {}) {
|
|
165
|
+
const { info } = storedDecimal(m);
|
|
166
|
+
const mode = entryMode(options);
|
|
167
|
+
const exact = toMajorDecimal(m);
|
|
168
|
+
const text = (options.digits ?? (mode === "locale" ? "storage" : "trim-zero")) === "trim-zero" ? trimDecimalZeros(exact) : exact;
|
|
169
|
+
const negative = text.startsWith("-");
|
|
170
|
+
const [whole, fraction] = (negative ? text.slice(1) : text).split(".");
|
|
171
|
+
const separators = mode === "locale" ? separatorsOf(resolveLocale(options.locale)) : mode === "auto" && info.commaDecimal ? {
|
|
172
|
+
decimal: ",",
|
|
173
|
+
group: "."
|
|
174
|
+
} : {
|
|
175
|
+
decimal: ".",
|
|
176
|
+
group: ","
|
|
177
|
+
};
|
|
178
|
+
const shownWhole = options.grouped && mode !== "machine" ? groupDigits(whole, separators.group) : whole;
|
|
179
|
+
return `${negative ? "-" : ""}${shownWhole}${fraction === void 0 ? "" : `${separators.decimal}${fraction}`}`;
|
|
180
|
+
}
|
|
181
|
+
const STEPS = {
|
|
182
|
+
0: "1",
|
|
183
|
+
2: "0.01",
|
|
184
|
+
3: "0.001"
|
|
185
|
+
};
|
|
186
|
+
const PLACEHOLDERS = {
|
|
187
|
+
0: "0",
|
|
188
|
+
2: "0.00",
|
|
189
|
+
3: "0.000"
|
|
190
|
+
};
|
|
191
|
+
/** The `step` of a money number input: `'1'` (JPY), `'0.01'` (USD), `'0.001'` (KWD), from the storage digits. */
|
|
192
|
+
function inputStep(currency) {
|
|
193
|
+
return STEPS[resolveCurrency(currency, { allowHistoric: true }).storageDigits];
|
|
194
|
+
}
|
|
195
|
+
/** The placeholder of a money input: `'0'`, `'0.00'` or `'0.000'`, from the storage digits. */
|
|
196
|
+
function inputPlaceholder(currency) {
|
|
197
|
+
return PLACEHOLDERS[resolveCurrency(currency, { allowHistoric: true }).storageDigits];
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
export { inputStep as a, inputPlaceholder as i, formatAmountInput as n, localeSeparators as o, groupDigits as r, parseAmountInput as s, decimalOfText as t };
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const require_money = require("./money-D5-vlv0h.cjs");
|
|
2
|
+
const require_arithmetic = require("./arithmetic-Bq-8mSal.cjs");
|
|
3
|
+
const require_number_text = require("./number-text-BpvN6NAo.cjs");
|
|
4
|
+
//#region src/input/amount.ts
|
|
5
|
+
/** Internal: the exact signed decimal a successful separator-engine read holds. */
|
|
6
|
+
function decimalOfText(read) {
|
|
7
|
+
return {
|
|
8
|
+
units: BigInt(read.sign) * read.units,
|
|
9
|
+
scale: read.scale
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Internal: the locale's separators, as the engine takes them. */
|
|
13
|
+
function localeSeparators(locale) {
|
|
14
|
+
const { decimal, group } = require_money.separatorsOf(require_money.resolveLocale(locale));
|
|
15
|
+
return {
|
|
16
|
+
decimalSeparator: decimal,
|
|
17
|
+
groupSeparator: group
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Internal: a read decimal as Money, checked like typed entry (shared by parseModelMoney). */
|
|
21
|
+
function entryMoney(value, info, options) {
|
|
22
|
+
const echo = options.original !== void 0 && require_money.compareDecimal(value, require_money.storedDecimal(options.original).value) === 0;
|
|
23
|
+
const round = options.roundExtraDigits === true;
|
|
24
|
+
if (!echo && !round && require_money.fractionDigitsOf(value) > info.storageDigits) return {
|
|
25
|
+
ok: false,
|
|
26
|
+
reason: "too_many_decimals"
|
|
27
|
+
};
|
|
28
|
+
if (value.units < 0n && !options.allowNegative) return {
|
|
29
|
+
ok: false,
|
|
30
|
+
reason: "negative"
|
|
31
|
+
};
|
|
32
|
+
if (value.units === 0n && options.allowZero === false) return {
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: "zero"
|
|
35
|
+
};
|
|
36
|
+
let money;
|
|
37
|
+
if (echo) money = options.original;
|
|
38
|
+
else try {
|
|
39
|
+
money = require_money.fromMajor(require_money.formatDecimal(value), info.code, { precision: round ? "round" : "reject" });
|
|
40
|
+
} catch {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: "above_max"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (options.min && require_arithmetic.compare(money, options.min) < 0) return {
|
|
47
|
+
ok: false,
|
|
48
|
+
reason: "below_min"
|
|
49
|
+
};
|
|
50
|
+
if (options.max && require_arithmetic.compare(money, options.max) > 0) return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: "above_max"
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
money
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Whether `shown` is exactly what `formatAmountInput(original)` prints for the field's locale or mode, grouped or not. */
|
|
60
|
+
function echoesOriginal(shown, options) {
|
|
61
|
+
const { original, locale, decimalSeparator } = options;
|
|
62
|
+
if (original === void 0) return false;
|
|
63
|
+
const field = {
|
|
64
|
+
locale,
|
|
65
|
+
decimalSeparator
|
|
66
|
+
};
|
|
67
|
+
return shown === formatAmountInput(original, field) || shown === formatAmountInput(original, {
|
|
68
|
+
...field,
|
|
69
|
+
grouped: true
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Which reading a field's options ask for; `decimalSeparator` and `locale` together, or a `decimalSeparator` other than `'.'`, throw. */
|
|
73
|
+
function entryMode(options) {
|
|
74
|
+
if (options.decimalSeparator === void 0) return options.locale === void 0 ? "auto" : "locale";
|
|
75
|
+
if (options.decimalSeparator !== "." || options.locale !== void 0) throw new TypeError("decimalSeparator must be '.' and cannot be combined with locale");
|
|
76
|
+
return "machine";
|
|
77
|
+
}
|
|
78
|
+
const ENTRY_SEPARATORS = {
|
|
79
|
+
auto: {
|
|
80
|
+
decimalSeparator: "auto",
|
|
81
|
+
keepFraction: true
|
|
82
|
+
},
|
|
83
|
+
machine: {
|
|
84
|
+
decimalSeparator: ".",
|
|
85
|
+
groupSeparator: null
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Typed text to Money, one rule for every money field: the currency's symbols and code and all
|
|
90
|
+
* whitespace are stripped; a leading `-` or U+2212 is the sign.
|
|
91
|
+
* - Without `locale` (the chat wizard, any field with no locale): the 'auto' rule decides `.`
|
|
92
|
+
* against `,` (see `parseImportAmount`): `'29,99'` EUR is 29.99, `'1.299,00'` and `'1,299'`
|
|
93
|
+
* are 1299, KWD `'1,5'` is 1.5; a currency storing 0 digits rounds a fraction half-up to a
|
|
94
|
+
* whole unit (`'1200.5'` JPY is 1201).
|
|
95
|
+
* - With `locale`: its decimal separator is the decimal and its group separator is valid only in
|
|
96
|
+
* 3-digit (or Indian lakh) groups; in a currency that stores no fraction (JPY, VND) a separator
|
|
97
|
+
* before exactly three digits in grouping position is grouping (`'1.500'` JPY is 1500).
|
|
98
|
+
* en-US `'29,99'` USD is `'unparseable'`; de-DE `'29,99'` EUR is 29.99; en-US `'1200.5'` JPY
|
|
99
|
+
* is `'too_many_decimals'`.
|
|
100
|
+
* - With `decimalSeparator: '.'` (every `<input type="number">`): `.` is the decimal, nothing
|
|
101
|
+
* groups (`'1,299'` is `'unparseable'`), TND and IQD `'1.500'` are 1.5, and a currency storing
|
|
102
|
+
* 0 digits rounds a fraction half-up to a whole unit (`'1.5'` JPY is 2).
|
|
103
|
+
* Otherwise more fraction digits than the currency stores is `'too_many_decimals'`, never rounded
|
|
104
|
+
* (`'1,2345'` USD). Text equal to `original` returns it unchanged (JPY 120050 shown as
|
|
105
|
+
* `'1200.50'`). An unknown currency, or an `original` in another currency, throws.
|
|
106
|
+
*/
|
|
107
|
+
function parseAmountInput(text, currency, options = {}) {
|
|
108
|
+
const info = require_money.resolveCurrency(currency, { allowHistoric: true });
|
|
109
|
+
if (options.original !== void 0 && require_money.storedDecimal(options.original).info.code !== info.code) throw new require_money.MoneyError("currency_mismatch", `original is ${options.original.currency}, the field is ${info.code}`);
|
|
110
|
+
const shown = typeof text === "string" ? text.trim() : "";
|
|
111
|
+
const mode = entryMode(options);
|
|
112
|
+
let value;
|
|
113
|
+
if (echoesOriginal(shown, options)) value = require_money.storedDecimal(options.original).value;
|
|
114
|
+
else {
|
|
115
|
+
const separators = mode === "locale" ? localeSeparators(options.locale) : ENTRY_SEPARATORS[mode];
|
|
116
|
+
const read = require_number_text.readNumberText(text, {
|
|
117
|
+
...separators,
|
|
118
|
+
currency: info.code
|
|
119
|
+
});
|
|
120
|
+
if ("reason" in read) return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: read.reason,
|
|
123
|
+
text: shown
|
|
124
|
+
};
|
|
125
|
+
value = decimalOfText(read);
|
|
126
|
+
}
|
|
127
|
+
const result = entryMoney(value, info, {
|
|
128
|
+
...options,
|
|
129
|
+
roundExtraDigits: mode !== "locale" && info.storageDigits === 0
|
|
130
|
+
});
|
|
131
|
+
if (!result.ok) return {
|
|
132
|
+
ok: false,
|
|
133
|
+
reason: result.reason,
|
|
134
|
+
text: shown
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
money: result.money,
|
|
139
|
+
text: require_money.toMajorDecimal(result.money)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Internal: `digits` with `group` between every three, from the right. */
|
|
143
|
+
function groupDigits(digits, group) {
|
|
144
|
+
return digits.replace(/\B(?=(\d{3})+(?!\d))/g, group);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The text a money field shows for a stored amount, no symbol, exact: a stored amount finer than
|
|
148
|
+
* the storage digits keeps its digits (JPY 120050 is never rounded on display).
|
|
149
|
+
* - Without `locale`: the spelling `parseAmountInput` reads back without a locale. Trailing
|
|
150
|
+
* fraction zeros are dropped (`'12.5'`, `'1200'`, KWD 1500 is `'1.5'`, JPY 120050 is
|
|
151
|
+
* `'1200.5'`), a currency written with a decimal comma (TND, `commaDecimal`) uses `,` and
|
|
152
|
+
* groups with `.` (`'1,125'`, `'1.234,5'`), every other currency `.` and `,`. Two amounts have no
|
|
153
|
+
* such spelling: IQD with a three-digit fraction (`'1.125'` reads as 1125, IQD displays no
|
|
154
|
+
* decimals) and a sub-unit amount of a currency storing 0 digits (`'1200.5'` JPY reads as a
|
|
155
|
+
* whole 1201); pass `{ original: m }` so the untouched text returns `m` itself.
|
|
156
|
+
* - With `locale`: its decimal and group separators and the storage digits (`'12,50'` de-DE).
|
|
157
|
+
* - With `decimalSeparator: '.'` (the value of an `<input type="number">`): `.` decimal, never
|
|
158
|
+
* grouped, trailing zeros dropped (TND 1500 is `'1.5'`, JPY 120050 `'1200.5'`), read back
|
|
159
|
+
* exactly by `parseAmountInput(text, currency, { decimalSeparator: '.' })` (JPY sub-unit
|
|
160
|
+
* amounts through `original`).
|
|
161
|
+
* `grouped` adds group separators (not in `'.'` mode); `digits` overrides the zeros
|
|
162
|
+
* (`'storage'` keeps them, `'trim-zero'` drops them).
|
|
163
|
+
*/
|
|
164
|
+
function formatAmountInput(m, options = {}) {
|
|
165
|
+
const { info } = require_money.storedDecimal(m);
|
|
166
|
+
const mode = entryMode(options);
|
|
167
|
+
const exact = require_money.toMajorDecimal(m);
|
|
168
|
+
const text = (options.digits ?? (mode === "locale" ? "storage" : "trim-zero")) === "trim-zero" ? require_money.trimDecimalZeros(exact) : exact;
|
|
169
|
+
const negative = text.startsWith("-");
|
|
170
|
+
const [whole, fraction] = (negative ? text.slice(1) : text).split(".");
|
|
171
|
+
const separators = mode === "locale" ? require_money.separatorsOf(require_money.resolveLocale(options.locale)) : mode === "auto" && info.commaDecimal ? {
|
|
172
|
+
decimal: ",",
|
|
173
|
+
group: "."
|
|
174
|
+
} : {
|
|
175
|
+
decimal: ".",
|
|
176
|
+
group: ","
|
|
177
|
+
};
|
|
178
|
+
const shownWhole = options.grouped && mode !== "machine" ? groupDigits(whole, separators.group) : whole;
|
|
179
|
+
return `${negative ? "-" : ""}${shownWhole}${fraction === void 0 ? "" : `${separators.decimal}${fraction}`}`;
|
|
180
|
+
}
|
|
181
|
+
const STEPS = {
|
|
182
|
+
0: "1",
|
|
183
|
+
2: "0.01",
|
|
184
|
+
3: "0.001"
|
|
185
|
+
};
|
|
186
|
+
const PLACEHOLDERS = {
|
|
187
|
+
0: "0",
|
|
188
|
+
2: "0.00",
|
|
189
|
+
3: "0.000"
|
|
190
|
+
};
|
|
191
|
+
/** The `step` of a money number input: `'1'` (JPY), `'0.01'` (USD), `'0.001'` (KWD), from the storage digits. */
|
|
192
|
+
function inputStep(currency) {
|
|
193
|
+
return STEPS[require_money.resolveCurrency(currency, { allowHistoric: true }).storageDigits];
|
|
194
|
+
}
|
|
195
|
+
/** The placeholder of a money input: `'0'`, `'0.00'` or `'0.000'`, from the storage digits. */
|
|
196
|
+
function inputPlaceholder(currency) {
|
|
197
|
+
return PLACEHOLDERS[require_money.resolveCurrency(currency, { allowHistoric: true }).storageDigits];
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
Object.defineProperty(exports, "decimalOfText", {
|
|
201
|
+
enumerable: true,
|
|
202
|
+
get: function() {
|
|
203
|
+
return decimalOfText;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
Object.defineProperty(exports, "formatAmountInput", {
|
|
207
|
+
enumerable: true,
|
|
208
|
+
get: function() {
|
|
209
|
+
return formatAmountInput;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
Object.defineProperty(exports, "groupDigits", {
|
|
213
|
+
enumerable: true,
|
|
214
|
+
get: function() {
|
|
215
|
+
return groupDigits;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
Object.defineProperty(exports, "inputPlaceholder", {
|
|
219
|
+
enumerable: true,
|
|
220
|
+
get: function() {
|
|
221
|
+
return inputPlaceholder;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
Object.defineProperty(exports, "inputStep", {
|
|
225
|
+
enumerable: true,
|
|
226
|
+
get: function() {
|
|
227
|
+
return inputStep;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
Object.defineProperty(exports, "localeSeparators", {
|
|
231
|
+
enumerable: true,
|
|
232
|
+
get: function() {
|
|
233
|
+
return localeSeparators;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
Object.defineProperty(exports, "parseAmountInput", {
|
|
237
|
+
enumerable: true,
|
|
238
|
+
get: function() {
|
|
239
|
+
return parseAmountInput;
|
|
240
|
+
}
|
|
241
|
+
});
|