@f-o-t/money 1.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 +689 -0
- package/dist/index.d.ts +904 -0
- package/dist/index.js +521 -0
- package/dist/operators/index.d.ts +69 -0
- package/dist/operators/index.js +39 -0
- package/dist/shared/chunk-jf6gzvp1.js +852 -0
- package/package.json +72 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
class MoneyError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "MoneyError";
|
|
6
|
+
Error.captureStackTrace?.(this, MoneyError);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class CurrencyMismatchError extends MoneyError {
|
|
11
|
+
currencyA;
|
|
12
|
+
currencyB;
|
|
13
|
+
constructor(message, currencyA, currencyB) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.currencyA = currencyA;
|
|
16
|
+
this.currencyB = currencyB;
|
|
17
|
+
this.name = "CurrencyMismatchError";
|
|
18
|
+
}
|
|
19
|
+
static create(currencyA, currencyB) {
|
|
20
|
+
return new CurrencyMismatchError(`Cannot operate on different currencies: ${currencyA} and ${currencyB}`, currencyA, currencyB);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class InvalidAmountError extends MoneyError {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "InvalidAmountError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class DivisionByZeroError extends MoneyError {
|
|
32
|
+
constructor(message = "Division by zero") {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "DivisionByZeroError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class UnknownCurrencyError extends MoneyError {
|
|
39
|
+
currencyCode;
|
|
40
|
+
constructor(message, currencyCode) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.currencyCode = currencyCode;
|
|
43
|
+
this.name = "UnknownCurrencyError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class OverflowError extends MoneyError {
|
|
48
|
+
constructor(message = "Value exceeds safe integer range") {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "OverflowError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class ScaleMismatchError extends MoneyError {
|
|
55
|
+
currency;
|
|
56
|
+
scaleA;
|
|
57
|
+
scaleB;
|
|
58
|
+
constructor(message, currency, scaleA, scaleB) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.currency = currency;
|
|
61
|
+
this.scaleA = scaleA;
|
|
62
|
+
this.scaleB = scaleB;
|
|
63
|
+
this.name = "ScaleMismatchError";
|
|
64
|
+
}
|
|
65
|
+
static create(currency, scaleA, scaleB) {
|
|
66
|
+
return new ScaleMismatchError(`Scale mismatch for ${currency}: ${scaleA} vs ${scaleB}. Same currency must have same scale.`, currency, scaleA, scaleB);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/core/assertions.ts
|
|
71
|
+
function assertSameCurrency(a, b) {
|
|
72
|
+
if (a.currency !== b.currency) {
|
|
73
|
+
throw CurrencyMismatchError.create(a.currency, b.currency);
|
|
74
|
+
}
|
|
75
|
+
if (a.scale !== b.scale) {
|
|
76
|
+
throw ScaleMismatchError.create(a.currency, a.scale, b.scale);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function assertAllSameCurrency(moneys) {
|
|
80
|
+
if (moneys.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
const first = moneys[0];
|
|
83
|
+
for (let i = 1;i < moneys.length; i++) {
|
|
84
|
+
const current = moneys[i];
|
|
85
|
+
if (current.currency !== first.currency) {
|
|
86
|
+
throw CurrencyMismatchError.create(first.currency, current.currency);
|
|
87
|
+
}
|
|
88
|
+
if (current.scale !== first.scale) {
|
|
89
|
+
throw ScaleMismatchError.create(first.currency, first.scale, current.scale);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/core/rounding.ts
|
|
95
|
+
function bankersRound(value, divisor) {
|
|
96
|
+
if (divisor === 0n) {
|
|
97
|
+
throw new Error("Division by zero");
|
|
98
|
+
}
|
|
99
|
+
const isNegative = value < 0n !== divisor < 0n;
|
|
100
|
+
const absValue = value < 0n ? -value : value;
|
|
101
|
+
const absDivisor = divisor < 0n ? -divisor : divisor;
|
|
102
|
+
const quotient = absValue / absDivisor;
|
|
103
|
+
const remainder = absValue % absDivisor;
|
|
104
|
+
let result;
|
|
105
|
+
const isExactlyHalf = remainder * 2n === absDivisor;
|
|
106
|
+
if (isExactlyHalf) {
|
|
107
|
+
if (quotient % 2n === 0n) {
|
|
108
|
+
result = quotient;
|
|
109
|
+
} else {
|
|
110
|
+
result = quotient + 1n;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
const half = absDivisor / 2n;
|
|
114
|
+
if (remainder <= half) {
|
|
115
|
+
result = quotient;
|
|
116
|
+
} else {
|
|
117
|
+
result = quotient + 1n;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return isNegative ? -result : result;
|
|
121
|
+
}
|
|
122
|
+
var EXTENDED_PRECISION = 18;
|
|
123
|
+
var PRECISION_FACTOR = 10n ** BigInt(EXTENDED_PRECISION);
|
|
124
|
+
|
|
125
|
+
// src/core/internal.ts
|
|
126
|
+
function createMoney(amount, currency, scale) {
|
|
127
|
+
return Object.freeze({ amount, currency, scale });
|
|
128
|
+
}
|
|
129
|
+
function parseDecimalToMinorUnits(amountStr, scale, roundingMode = "truncate") {
|
|
130
|
+
const normalized = amountStr.trim();
|
|
131
|
+
const isNegative = normalized.startsWith("-");
|
|
132
|
+
const absStr = isNegative ? normalized.slice(1) : normalized;
|
|
133
|
+
const parts = absStr.split(".");
|
|
134
|
+
const intPart = parts[0] || "0";
|
|
135
|
+
const decPart = parts[1] || "";
|
|
136
|
+
if (!/^\d*$/.test(intPart) || !/^\d*$/.test(decPart)) {
|
|
137
|
+
throw new Error(`Invalid amount format: ${amountStr}`);
|
|
138
|
+
}
|
|
139
|
+
let amount;
|
|
140
|
+
if (decPart.length <= scale) {
|
|
141
|
+
const adjustedDecPart = decPart.padEnd(scale, "0");
|
|
142
|
+
const combined = intPart + adjustedDecPart;
|
|
143
|
+
amount = BigInt(combined);
|
|
144
|
+
} else if (roundingMode === "truncate") {
|
|
145
|
+
const adjustedDecPart = decPart.slice(0, scale);
|
|
146
|
+
const combined = intPart + adjustedDecPart;
|
|
147
|
+
amount = BigInt(combined);
|
|
148
|
+
} else {
|
|
149
|
+
const fullAmount = BigInt(intPart + decPart);
|
|
150
|
+
const extraDigits = decPart.length - scale;
|
|
151
|
+
const divisor = 10n ** BigInt(extraDigits);
|
|
152
|
+
amount = bankersRound(fullAmount, divisor);
|
|
153
|
+
}
|
|
154
|
+
return isNegative ? -amount : amount;
|
|
155
|
+
}
|
|
156
|
+
function minorUnitsToDecimal(amount, scale) {
|
|
157
|
+
if (scale === 0) {
|
|
158
|
+
return amount.toString();
|
|
159
|
+
}
|
|
160
|
+
const isNegative = amount < 0n;
|
|
161
|
+
const absAmount = isNegative ? -amount : amount;
|
|
162
|
+
const str = absAmount.toString().padStart(scale + 1, "0");
|
|
163
|
+
const intPart = str.slice(0, -scale) || "0";
|
|
164
|
+
const decPart = str.slice(-scale);
|
|
165
|
+
const result = `${intPart}.${decPart}`;
|
|
166
|
+
return isNegative ? `-${result}` : result;
|
|
167
|
+
}
|
|
168
|
+
function maxBigInt(a, b) {
|
|
169
|
+
return a > b ? a : b;
|
|
170
|
+
}
|
|
171
|
+
function minBigInt(a, b) {
|
|
172
|
+
return a < b ? a : b;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/currency/currencies.ts
|
|
176
|
+
var ISO_4217_CURRENCIES = {
|
|
177
|
+
USD: {
|
|
178
|
+
code: "USD",
|
|
179
|
+
numericCode: 840,
|
|
180
|
+
name: "US Dollar",
|
|
181
|
+
decimalPlaces: 2,
|
|
182
|
+
symbol: "$",
|
|
183
|
+
subunitName: "cent"
|
|
184
|
+
},
|
|
185
|
+
CAD: {
|
|
186
|
+
code: "CAD",
|
|
187
|
+
numericCode: 124,
|
|
188
|
+
name: "Canadian Dollar",
|
|
189
|
+
decimalPlaces: 2,
|
|
190
|
+
symbol: "C$",
|
|
191
|
+
subunitName: "cent"
|
|
192
|
+
},
|
|
193
|
+
MXN: {
|
|
194
|
+
code: "MXN",
|
|
195
|
+
numericCode: 484,
|
|
196
|
+
name: "Mexican Peso",
|
|
197
|
+
decimalPlaces: 2,
|
|
198
|
+
symbol: "$",
|
|
199
|
+
subunitName: "centavo"
|
|
200
|
+
},
|
|
201
|
+
BRL: {
|
|
202
|
+
code: "BRL",
|
|
203
|
+
numericCode: 986,
|
|
204
|
+
name: "Brazilian Real",
|
|
205
|
+
decimalPlaces: 2,
|
|
206
|
+
symbol: "R$",
|
|
207
|
+
subunitName: "centavo"
|
|
208
|
+
},
|
|
209
|
+
ARS: {
|
|
210
|
+
code: "ARS",
|
|
211
|
+
numericCode: 32,
|
|
212
|
+
name: "Argentine Peso",
|
|
213
|
+
decimalPlaces: 2,
|
|
214
|
+
symbol: "$",
|
|
215
|
+
subunitName: "centavo"
|
|
216
|
+
},
|
|
217
|
+
CLP: {
|
|
218
|
+
code: "CLP",
|
|
219
|
+
numericCode: 152,
|
|
220
|
+
name: "Chilean Peso",
|
|
221
|
+
decimalPlaces: 0,
|
|
222
|
+
symbol: "$"
|
|
223
|
+
},
|
|
224
|
+
COP: {
|
|
225
|
+
code: "COP",
|
|
226
|
+
numericCode: 170,
|
|
227
|
+
name: "Colombian Peso",
|
|
228
|
+
decimalPlaces: 2,
|
|
229
|
+
symbol: "$",
|
|
230
|
+
subunitName: "centavo"
|
|
231
|
+
},
|
|
232
|
+
PEN: {
|
|
233
|
+
code: "PEN",
|
|
234
|
+
numericCode: 604,
|
|
235
|
+
name: "Peruvian Sol",
|
|
236
|
+
decimalPlaces: 2,
|
|
237
|
+
symbol: "S/",
|
|
238
|
+
subunitName: "céntimo"
|
|
239
|
+
},
|
|
240
|
+
EUR: {
|
|
241
|
+
code: "EUR",
|
|
242
|
+
numericCode: 978,
|
|
243
|
+
name: "Euro",
|
|
244
|
+
decimalPlaces: 2,
|
|
245
|
+
symbol: "€",
|
|
246
|
+
subunitName: "cent"
|
|
247
|
+
},
|
|
248
|
+
GBP: {
|
|
249
|
+
code: "GBP",
|
|
250
|
+
numericCode: 826,
|
|
251
|
+
name: "Pound Sterling",
|
|
252
|
+
decimalPlaces: 2,
|
|
253
|
+
symbol: "£",
|
|
254
|
+
subunitName: "penny"
|
|
255
|
+
},
|
|
256
|
+
CHF: {
|
|
257
|
+
code: "CHF",
|
|
258
|
+
numericCode: 756,
|
|
259
|
+
name: "Swiss Franc",
|
|
260
|
+
decimalPlaces: 2,
|
|
261
|
+
symbol: "CHF",
|
|
262
|
+
subunitName: "rappen"
|
|
263
|
+
},
|
|
264
|
+
SEK: {
|
|
265
|
+
code: "SEK",
|
|
266
|
+
numericCode: 752,
|
|
267
|
+
name: "Swedish Krona",
|
|
268
|
+
decimalPlaces: 2,
|
|
269
|
+
symbol: "kr",
|
|
270
|
+
subunitName: "öre"
|
|
271
|
+
},
|
|
272
|
+
NOK: {
|
|
273
|
+
code: "NOK",
|
|
274
|
+
numericCode: 578,
|
|
275
|
+
name: "Norwegian Krone",
|
|
276
|
+
decimalPlaces: 2,
|
|
277
|
+
symbol: "kr",
|
|
278
|
+
subunitName: "øre"
|
|
279
|
+
},
|
|
280
|
+
DKK: {
|
|
281
|
+
code: "DKK",
|
|
282
|
+
numericCode: 208,
|
|
283
|
+
name: "Danish Krone",
|
|
284
|
+
decimalPlaces: 2,
|
|
285
|
+
symbol: "kr",
|
|
286
|
+
subunitName: "øre"
|
|
287
|
+
},
|
|
288
|
+
PLN: {
|
|
289
|
+
code: "PLN",
|
|
290
|
+
numericCode: 985,
|
|
291
|
+
name: "Polish Zloty",
|
|
292
|
+
decimalPlaces: 2,
|
|
293
|
+
symbol: "zł",
|
|
294
|
+
subunitName: "grosz"
|
|
295
|
+
},
|
|
296
|
+
CZK: {
|
|
297
|
+
code: "CZK",
|
|
298
|
+
numericCode: 203,
|
|
299
|
+
name: "Czech Koruna",
|
|
300
|
+
decimalPlaces: 2,
|
|
301
|
+
symbol: "Kč",
|
|
302
|
+
subunitName: "haléř"
|
|
303
|
+
},
|
|
304
|
+
HUF: {
|
|
305
|
+
code: "HUF",
|
|
306
|
+
numericCode: 348,
|
|
307
|
+
name: "Hungarian Forint",
|
|
308
|
+
decimalPlaces: 2,
|
|
309
|
+
symbol: "Ft",
|
|
310
|
+
subunitName: "fillér"
|
|
311
|
+
},
|
|
312
|
+
RON: {
|
|
313
|
+
code: "RON",
|
|
314
|
+
numericCode: 946,
|
|
315
|
+
name: "Romanian Leu",
|
|
316
|
+
decimalPlaces: 2,
|
|
317
|
+
symbol: "lei",
|
|
318
|
+
subunitName: "ban"
|
|
319
|
+
},
|
|
320
|
+
RUB: {
|
|
321
|
+
code: "RUB",
|
|
322
|
+
numericCode: 643,
|
|
323
|
+
name: "Russian Ruble",
|
|
324
|
+
decimalPlaces: 2,
|
|
325
|
+
symbol: "₽",
|
|
326
|
+
subunitName: "kopek"
|
|
327
|
+
},
|
|
328
|
+
TRY: {
|
|
329
|
+
code: "TRY",
|
|
330
|
+
numericCode: 949,
|
|
331
|
+
name: "Turkish Lira",
|
|
332
|
+
decimalPlaces: 2,
|
|
333
|
+
symbol: "₺",
|
|
334
|
+
subunitName: "kuruş"
|
|
335
|
+
},
|
|
336
|
+
ISK: {
|
|
337
|
+
code: "ISK",
|
|
338
|
+
numericCode: 352,
|
|
339
|
+
name: "Icelandic Krona",
|
|
340
|
+
decimalPlaces: 0,
|
|
341
|
+
symbol: "kr"
|
|
342
|
+
},
|
|
343
|
+
JPY: {
|
|
344
|
+
code: "JPY",
|
|
345
|
+
numericCode: 392,
|
|
346
|
+
name: "Japanese Yen",
|
|
347
|
+
decimalPlaces: 0,
|
|
348
|
+
symbol: "¥"
|
|
349
|
+
},
|
|
350
|
+
CNY: {
|
|
351
|
+
code: "CNY",
|
|
352
|
+
numericCode: 156,
|
|
353
|
+
name: "Chinese Yuan",
|
|
354
|
+
decimalPlaces: 2,
|
|
355
|
+
symbol: "¥",
|
|
356
|
+
subunitName: "fen"
|
|
357
|
+
},
|
|
358
|
+
HKD: {
|
|
359
|
+
code: "HKD",
|
|
360
|
+
numericCode: 344,
|
|
361
|
+
name: "Hong Kong Dollar",
|
|
362
|
+
decimalPlaces: 2,
|
|
363
|
+
symbol: "HK$",
|
|
364
|
+
subunitName: "cent"
|
|
365
|
+
},
|
|
366
|
+
TWD: {
|
|
367
|
+
code: "TWD",
|
|
368
|
+
numericCode: 901,
|
|
369
|
+
name: "New Taiwan Dollar",
|
|
370
|
+
decimalPlaces: 2,
|
|
371
|
+
symbol: "NT$",
|
|
372
|
+
subunitName: "cent"
|
|
373
|
+
},
|
|
374
|
+
KRW: {
|
|
375
|
+
code: "KRW",
|
|
376
|
+
numericCode: 410,
|
|
377
|
+
name: "South Korean Won",
|
|
378
|
+
decimalPlaces: 0,
|
|
379
|
+
symbol: "₩"
|
|
380
|
+
},
|
|
381
|
+
INR: {
|
|
382
|
+
code: "INR",
|
|
383
|
+
numericCode: 356,
|
|
384
|
+
name: "Indian Rupee",
|
|
385
|
+
decimalPlaces: 2,
|
|
386
|
+
symbol: "₹",
|
|
387
|
+
subunitName: "paisa"
|
|
388
|
+
},
|
|
389
|
+
IDR: {
|
|
390
|
+
code: "IDR",
|
|
391
|
+
numericCode: 360,
|
|
392
|
+
name: "Indonesian Rupiah",
|
|
393
|
+
decimalPlaces: 2,
|
|
394
|
+
symbol: "Rp",
|
|
395
|
+
subunitName: "sen"
|
|
396
|
+
},
|
|
397
|
+
MYR: {
|
|
398
|
+
code: "MYR",
|
|
399
|
+
numericCode: 458,
|
|
400
|
+
name: "Malaysian Ringgit",
|
|
401
|
+
decimalPlaces: 2,
|
|
402
|
+
symbol: "RM",
|
|
403
|
+
subunitName: "sen"
|
|
404
|
+
},
|
|
405
|
+
SGD: {
|
|
406
|
+
code: "SGD",
|
|
407
|
+
numericCode: 702,
|
|
408
|
+
name: "Singapore Dollar",
|
|
409
|
+
decimalPlaces: 2,
|
|
410
|
+
symbol: "S$",
|
|
411
|
+
subunitName: "cent"
|
|
412
|
+
},
|
|
413
|
+
THB: {
|
|
414
|
+
code: "THB",
|
|
415
|
+
numericCode: 764,
|
|
416
|
+
name: "Thai Baht",
|
|
417
|
+
decimalPlaces: 2,
|
|
418
|
+
symbol: "฿",
|
|
419
|
+
subunitName: "satang"
|
|
420
|
+
},
|
|
421
|
+
PHP: {
|
|
422
|
+
code: "PHP",
|
|
423
|
+
numericCode: 608,
|
|
424
|
+
name: "Philippine Peso",
|
|
425
|
+
decimalPlaces: 2,
|
|
426
|
+
symbol: "₱",
|
|
427
|
+
subunitName: "sentimo"
|
|
428
|
+
},
|
|
429
|
+
VND: {
|
|
430
|
+
code: "VND",
|
|
431
|
+
numericCode: 704,
|
|
432
|
+
name: "Vietnamese Dong",
|
|
433
|
+
decimalPlaces: 0,
|
|
434
|
+
symbol: "₫"
|
|
435
|
+
},
|
|
436
|
+
AUD: {
|
|
437
|
+
code: "AUD",
|
|
438
|
+
numericCode: 36,
|
|
439
|
+
name: "Australian Dollar",
|
|
440
|
+
decimalPlaces: 2,
|
|
441
|
+
symbol: "A$",
|
|
442
|
+
subunitName: "cent"
|
|
443
|
+
},
|
|
444
|
+
NZD: {
|
|
445
|
+
code: "NZD",
|
|
446
|
+
numericCode: 554,
|
|
447
|
+
name: "New Zealand Dollar",
|
|
448
|
+
decimalPlaces: 2,
|
|
449
|
+
symbol: "NZ$",
|
|
450
|
+
subunitName: "cent"
|
|
451
|
+
},
|
|
452
|
+
AED: {
|
|
453
|
+
code: "AED",
|
|
454
|
+
numericCode: 784,
|
|
455
|
+
name: "UAE Dirham",
|
|
456
|
+
decimalPlaces: 2,
|
|
457
|
+
symbol: "د.إ",
|
|
458
|
+
subunitName: "fils"
|
|
459
|
+
},
|
|
460
|
+
SAR: {
|
|
461
|
+
code: "SAR",
|
|
462
|
+
numericCode: 682,
|
|
463
|
+
name: "Saudi Riyal",
|
|
464
|
+
decimalPlaces: 2,
|
|
465
|
+
symbol: "﷼",
|
|
466
|
+
subunitName: "halala"
|
|
467
|
+
},
|
|
468
|
+
ILS: {
|
|
469
|
+
code: "ILS",
|
|
470
|
+
numericCode: 376,
|
|
471
|
+
name: "Israeli New Shekel",
|
|
472
|
+
decimalPlaces: 2,
|
|
473
|
+
symbol: "₪",
|
|
474
|
+
subunitName: "agora"
|
|
475
|
+
},
|
|
476
|
+
KWD: {
|
|
477
|
+
code: "KWD",
|
|
478
|
+
numericCode: 414,
|
|
479
|
+
name: "Kuwaiti Dinar",
|
|
480
|
+
decimalPlaces: 3,
|
|
481
|
+
symbol: "د.ك",
|
|
482
|
+
subunitName: "fils"
|
|
483
|
+
},
|
|
484
|
+
BHD: {
|
|
485
|
+
code: "BHD",
|
|
486
|
+
numericCode: 48,
|
|
487
|
+
name: "Bahraini Dinar",
|
|
488
|
+
decimalPlaces: 3,
|
|
489
|
+
symbol: ".د.ب",
|
|
490
|
+
subunitName: "fils"
|
|
491
|
+
},
|
|
492
|
+
OMR: {
|
|
493
|
+
code: "OMR",
|
|
494
|
+
numericCode: 512,
|
|
495
|
+
name: "Omani Rial",
|
|
496
|
+
decimalPlaces: 3,
|
|
497
|
+
symbol: "ر.ع.",
|
|
498
|
+
subunitName: "baisa"
|
|
499
|
+
},
|
|
500
|
+
ZAR: {
|
|
501
|
+
code: "ZAR",
|
|
502
|
+
numericCode: 710,
|
|
503
|
+
name: "South African Rand",
|
|
504
|
+
decimalPlaces: 2,
|
|
505
|
+
symbol: "R",
|
|
506
|
+
subunitName: "cent"
|
|
507
|
+
},
|
|
508
|
+
EGP: {
|
|
509
|
+
code: "EGP",
|
|
510
|
+
numericCode: 818,
|
|
511
|
+
name: "Egyptian Pound",
|
|
512
|
+
decimalPlaces: 2,
|
|
513
|
+
symbol: "E£",
|
|
514
|
+
subunitName: "piastre"
|
|
515
|
+
},
|
|
516
|
+
NGN: {
|
|
517
|
+
code: "NGN",
|
|
518
|
+
numericCode: 566,
|
|
519
|
+
name: "Nigerian Naira",
|
|
520
|
+
decimalPlaces: 2,
|
|
521
|
+
symbol: "₦",
|
|
522
|
+
subunitName: "kobo"
|
|
523
|
+
},
|
|
524
|
+
BTC: {
|
|
525
|
+
code: "BTC",
|
|
526
|
+
numericCode: 0,
|
|
527
|
+
name: "Bitcoin",
|
|
528
|
+
decimalPlaces: 8,
|
|
529
|
+
symbol: "₿",
|
|
530
|
+
subunitName: "satoshi"
|
|
531
|
+
},
|
|
532
|
+
ETH: {
|
|
533
|
+
code: "ETH",
|
|
534
|
+
numericCode: 0,
|
|
535
|
+
name: "Ethereum",
|
|
536
|
+
decimalPlaces: 18,
|
|
537
|
+
symbol: "Ξ",
|
|
538
|
+
subunitName: "wei"
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/currency/registry.ts
|
|
543
|
+
var customCurrencies = new Map;
|
|
544
|
+
function getCurrency(code) {
|
|
545
|
+
const upperCode = code.toUpperCase();
|
|
546
|
+
const custom = customCurrencies.get(upperCode);
|
|
547
|
+
if (custom)
|
|
548
|
+
return custom;
|
|
549
|
+
const iso = ISO_4217_CURRENCIES[upperCode];
|
|
550
|
+
if (iso)
|
|
551
|
+
return iso;
|
|
552
|
+
throw new UnknownCurrencyError(`Unknown currency: ${code}`, upperCode);
|
|
553
|
+
}
|
|
554
|
+
function registerCurrency(currency) {
|
|
555
|
+
customCurrencies.set(currency.code.toUpperCase(), currency);
|
|
556
|
+
}
|
|
557
|
+
function hasCurrency(code) {
|
|
558
|
+
const upperCode = code.toUpperCase();
|
|
559
|
+
return customCurrencies.has(upperCode) || upperCode in ISO_4217_CURRENCIES;
|
|
560
|
+
}
|
|
561
|
+
function getAllCurrencies() {
|
|
562
|
+
return {
|
|
563
|
+
...ISO_4217_CURRENCIES,
|
|
564
|
+
...Object.fromEntries(customCurrencies)
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function clearCustomCurrencies() {
|
|
568
|
+
customCurrencies.clear();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/core/money.ts
|
|
572
|
+
function hasLikelyPrecisionIssue(num) {
|
|
573
|
+
if (!Number.isFinite(num))
|
|
574
|
+
return false;
|
|
575
|
+
if (Number.isInteger(num))
|
|
576
|
+
return false;
|
|
577
|
+
const str = num.toString();
|
|
578
|
+
const decimalPart = str.split(".")[1];
|
|
579
|
+
if (!decimalPart)
|
|
580
|
+
return false;
|
|
581
|
+
if (decimalPart.length > 10) {
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
if (/0{4,}|9{4,}/.test(decimalPart)) {
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
function of(amount, currency, roundingMode) {
|
|
590
|
+
const curr = getCurrency(currency);
|
|
591
|
+
const scale = curr.decimalPlaces;
|
|
592
|
+
let amountStr;
|
|
593
|
+
if (typeof amount === "number") {
|
|
594
|
+
if (hasLikelyPrecisionIssue(amount)) {
|
|
595
|
+
console.warn(`Money.of() received number ${amount} which may have precision issues. ` + `Consider using string amounts: of("${amount}", "${currency}") instead.`);
|
|
596
|
+
}
|
|
597
|
+
amountStr = amount.toString();
|
|
598
|
+
} else {
|
|
599
|
+
amountStr = amount;
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
const minorUnits = parseDecimalToMinorUnits(amountStr, scale, roundingMode ?? "truncate");
|
|
603
|
+
return createMoney(minorUnits, currency.toUpperCase(), scale);
|
|
604
|
+
} catch {
|
|
605
|
+
throw new InvalidAmountError(`Invalid amount format: ${amountStr}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function ofRounded(amount, currency) {
|
|
609
|
+
return of(amount, currency, "round");
|
|
610
|
+
}
|
|
611
|
+
function fromMinorUnits(minorUnits, currency) {
|
|
612
|
+
const curr = getCurrency(currency);
|
|
613
|
+
if (typeof minorUnits === "number") {
|
|
614
|
+
if (!Number.isInteger(minorUnits)) {
|
|
615
|
+
throw new InvalidAmountError(`Minor units must be an integer, got: ${minorUnits}`);
|
|
616
|
+
}
|
|
617
|
+
if (!Number.isFinite(minorUnits)) {
|
|
618
|
+
throw new InvalidAmountError("Minor units must be finite");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const amount = typeof minorUnits === "number" ? BigInt(minorUnits) : minorUnits;
|
|
622
|
+
return createMoney(amount, currency.toUpperCase(), curr.decimalPlaces);
|
|
623
|
+
}
|
|
624
|
+
function zero(currency) {
|
|
625
|
+
const curr = getCurrency(currency);
|
|
626
|
+
return createMoney(0n, currency.toUpperCase(), curr.decimalPlaces);
|
|
627
|
+
}
|
|
628
|
+
function fromMajorUnits(amount, currency) {
|
|
629
|
+
return of(amount, currency);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/schemas.ts
|
|
633
|
+
import { z } from "zod";
|
|
634
|
+
var CurrencyCodeSchema = z.string().length(3, "Currency code must be exactly 3 characters").regex(/^[A-Z]{3}$/, "Currency code must be 3 uppercase letters").describe("ISO 4217 currency code");
|
|
635
|
+
var CurrencySchema = z.object({
|
|
636
|
+
code: CurrencyCodeSchema,
|
|
637
|
+
numericCode: z.number().int().min(0).describe("ISO 4217 numeric code (0 for custom)"),
|
|
638
|
+
name: z.string().min(1).describe("Full currency name"),
|
|
639
|
+
decimalPlaces: z.number().int().min(0).max(18).describe("Number of decimal places"),
|
|
640
|
+
symbol: z.string().optional().describe("Currency symbol"),
|
|
641
|
+
subunitName: z.string().optional().describe("Name of minor unit")
|
|
642
|
+
}).describe("Currency definition");
|
|
643
|
+
var AmountStringSchema = z.string().regex(/^-?\d+(\.\d+)?$/, "Amount must be a valid decimal string").describe("Decimal amount as string");
|
|
644
|
+
var MoneySchema = z.object({
|
|
645
|
+
amount: AmountStringSchema,
|
|
646
|
+
currency: CurrencyCodeSchema
|
|
647
|
+
}).describe("Money JSON representation");
|
|
648
|
+
var DatabaseMoneySchema = z.object({
|
|
649
|
+
amount: z.string().describe("Decimal amount as string"),
|
|
650
|
+
currency: CurrencyCodeSchema
|
|
651
|
+
}).describe("Database Money representation");
|
|
652
|
+
var MoneyInputSchema = z.object({
|
|
653
|
+
amount: z.union([z.string(), z.number()]).describe("Amount (string or number)"),
|
|
654
|
+
currency: CurrencyCodeSchema
|
|
655
|
+
}).describe("Money input from user");
|
|
656
|
+
var MoneyInternalSchema = z.object({
|
|
657
|
+
amount: z.bigint().describe("Amount in minor units as BigInt"),
|
|
658
|
+
currency: CurrencyCodeSchema,
|
|
659
|
+
scale: z.number().int().min(0).max(18).describe("Decimal places")
|
|
660
|
+
}).describe("Internal Money representation");
|
|
661
|
+
var FormatOptionsSchema = z.object({
|
|
662
|
+
locale: z.string().optional(),
|
|
663
|
+
notation: z.enum(["standard", "compact"]).optional(),
|
|
664
|
+
signDisplay: z.enum(["auto", "always", "never", "exceptZero"]).optional(),
|
|
665
|
+
currencyDisplay: z.enum(["symbol", "code", "name", "narrowSymbol"]).optional(),
|
|
666
|
+
hideSymbol: z.boolean().optional(),
|
|
667
|
+
minimumFractionDigits: z.number().int().min(0).optional(),
|
|
668
|
+
maximumFractionDigits: z.number().int().min(0).optional()
|
|
669
|
+
}).describe("Money formatting options");
|
|
670
|
+
var AllocationRatiosSchema = z.array(z.number().min(0, "Ratios cannot be negative")).min(1, "At least one ratio is required").refine((ratios) => ratios.some((r) => r > 0), {
|
|
671
|
+
message: "At least one ratio must be greater than zero"
|
|
672
|
+
}).describe("Allocation ratios");
|
|
673
|
+
|
|
674
|
+
// src/operations/comparison.ts
|
|
675
|
+
import { createOperator } from "@f-o-t/condition-evaluator";
|
|
676
|
+
import { z as z2 } from "zod";
|
|
677
|
+
function toMoney(value) {
|
|
678
|
+
if (value === null || value === undefined) {
|
|
679
|
+
throw new Error("Cannot convert null/undefined to Money");
|
|
680
|
+
}
|
|
681
|
+
if (typeof value === "object" && "amount" in value && "currency" in value && "scale" in value && typeof value.amount === "bigint") {
|
|
682
|
+
return value;
|
|
683
|
+
}
|
|
684
|
+
if (typeof value === "object" && "amount" in value && "currency" in value && typeof value.amount === "string") {
|
|
685
|
+
const json = value;
|
|
686
|
+
return of(json.amount, json.currency);
|
|
687
|
+
}
|
|
688
|
+
throw new Error(`Cannot convert value to Money: ${JSON.stringify(value)}`);
|
|
689
|
+
}
|
|
690
|
+
var moneyEqualsOperator = createOperator({
|
|
691
|
+
name: "money_eq",
|
|
692
|
+
type: "custom",
|
|
693
|
+
description: "Check if two Money values are equal",
|
|
694
|
+
evaluate: (actual, expected) => {
|
|
695
|
+
const a = toMoney(actual);
|
|
696
|
+
const b = toMoney(expected);
|
|
697
|
+
assertSameCurrency(a, b);
|
|
698
|
+
return a.amount === b.amount;
|
|
699
|
+
},
|
|
700
|
+
valueSchema: MoneySchema,
|
|
701
|
+
reasonGenerator: (passed, actual, expected, field) => {
|
|
702
|
+
const a = toMoney(actual);
|
|
703
|
+
const b = toMoney(expected);
|
|
704
|
+
if (passed) {
|
|
705
|
+
return `${field} equals ${minorUnitsToDecimal(b.amount, b.scale)} ${b.currency}`;
|
|
706
|
+
}
|
|
707
|
+
return `${field} (${minorUnitsToDecimal(a.amount, a.scale)} ${a.currency}) does not equal ${minorUnitsToDecimal(b.amount, b.scale)} ${b.currency}`;
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
var moneyNotEqualsOperator = createOperator({
|
|
711
|
+
name: "money_neq",
|
|
712
|
+
type: "custom",
|
|
713
|
+
description: "Check if two Money values are not equal",
|
|
714
|
+
evaluate: (actual, expected) => {
|
|
715
|
+
const a = toMoney(actual);
|
|
716
|
+
const b = toMoney(expected);
|
|
717
|
+
assertSameCurrency(a, b);
|
|
718
|
+
return a.amount !== b.amount;
|
|
719
|
+
},
|
|
720
|
+
valueSchema: MoneySchema
|
|
721
|
+
});
|
|
722
|
+
var moneyGreaterThanOperator = createOperator({
|
|
723
|
+
name: "money_gt",
|
|
724
|
+
type: "custom",
|
|
725
|
+
description: "Check if Money value is greater than expected",
|
|
726
|
+
evaluate: (actual, expected) => {
|
|
727
|
+
const a = toMoney(actual);
|
|
728
|
+
const b = toMoney(expected);
|
|
729
|
+
assertSameCurrency(a, b);
|
|
730
|
+
return a.amount > b.amount;
|
|
731
|
+
},
|
|
732
|
+
valueSchema: MoneySchema
|
|
733
|
+
});
|
|
734
|
+
var moneyGreaterThanOrEqualOperator = createOperator({
|
|
735
|
+
name: "money_gte",
|
|
736
|
+
type: "custom",
|
|
737
|
+
description: "Check if Money value is greater than or equal to expected",
|
|
738
|
+
evaluate: (actual, expected) => {
|
|
739
|
+
const a = toMoney(actual);
|
|
740
|
+
const b = toMoney(expected);
|
|
741
|
+
assertSameCurrency(a, b);
|
|
742
|
+
return a.amount >= b.amount;
|
|
743
|
+
},
|
|
744
|
+
valueSchema: MoneySchema
|
|
745
|
+
});
|
|
746
|
+
var moneyLessThanOperator = createOperator({
|
|
747
|
+
name: "money_lt",
|
|
748
|
+
type: "custom",
|
|
749
|
+
description: "Check if Money value is less than expected",
|
|
750
|
+
evaluate: (actual, expected) => {
|
|
751
|
+
const a = toMoney(actual);
|
|
752
|
+
const b = toMoney(expected);
|
|
753
|
+
assertSameCurrency(a, b);
|
|
754
|
+
return a.amount < b.amount;
|
|
755
|
+
},
|
|
756
|
+
valueSchema: MoneySchema
|
|
757
|
+
});
|
|
758
|
+
var moneyLessThanOrEqualOperator = createOperator({
|
|
759
|
+
name: "money_lte",
|
|
760
|
+
type: "custom",
|
|
761
|
+
description: "Check if Money value is less than or equal to expected",
|
|
762
|
+
evaluate: (actual, expected) => {
|
|
763
|
+
const a = toMoney(actual);
|
|
764
|
+
const b = toMoney(expected);
|
|
765
|
+
assertSameCurrency(a, b);
|
|
766
|
+
return a.amount <= b.amount;
|
|
767
|
+
},
|
|
768
|
+
valueSchema: MoneySchema
|
|
769
|
+
});
|
|
770
|
+
var moneyBetweenOperator = createOperator({
|
|
771
|
+
name: "money_between",
|
|
772
|
+
type: "custom",
|
|
773
|
+
description: "Check if Money value is between two values (inclusive)",
|
|
774
|
+
evaluate: (actual, expected) => {
|
|
775
|
+
if (!Array.isArray(expected) || expected.length !== 2) {
|
|
776
|
+
throw new Error("Expected value must be an array of two Money values");
|
|
777
|
+
}
|
|
778
|
+
const a = toMoney(actual);
|
|
779
|
+
const min = toMoney(expected[0]);
|
|
780
|
+
const max = toMoney(expected[1]);
|
|
781
|
+
assertSameCurrency(a, min);
|
|
782
|
+
assertSameCurrency(a, max);
|
|
783
|
+
return a.amount >= min.amount && a.amount <= max.amount;
|
|
784
|
+
},
|
|
785
|
+
valueSchema: z2.tuple([MoneySchema, MoneySchema])
|
|
786
|
+
});
|
|
787
|
+
var moneyPositiveOperator = createOperator({
|
|
788
|
+
name: "money_positive",
|
|
789
|
+
type: "custom",
|
|
790
|
+
description: "Check if Money value is positive (> 0)",
|
|
791
|
+
evaluate: (actual) => {
|
|
792
|
+
const a = toMoney(actual);
|
|
793
|
+
return a.amount > 0n;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
var moneyNegativeOperator = createOperator({
|
|
797
|
+
name: "money_negative",
|
|
798
|
+
type: "custom",
|
|
799
|
+
description: "Check if Money value is negative (< 0)",
|
|
800
|
+
evaluate: (actual) => {
|
|
801
|
+
const a = toMoney(actual);
|
|
802
|
+
return a.amount < 0n;
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
var moneyZeroOperator = createOperator({
|
|
806
|
+
name: "money_zero",
|
|
807
|
+
type: "custom",
|
|
808
|
+
description: "Check if Money value is zero",
|
|
809
|
+
evaluate: (actual) => {
|
|
810
|
+
const a = toMoney(actual);
|
|
811
|
+
return a.amount === 0n;
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
function equals(a, b) {
|
|
815
|
+
assertSameCurrency(a, b);
|
|
816
|
+
return a.amount === b.amount;
|
|
817
|
+
}
|
|
818
|
+
function greaterThan(a, b) {
|
|
819
|
+
assertSameCurrency(a, b);
|
|
820
|
+
return a.amount > b.amount;
|
|
821
|
+
}
|
|
822
|
+
function greaterThanOrEqual(a, b) {
|
|
823
|
+
assertSameCurrency(a, b);
|
|
824
|
+
return a.amount >= b.amount;
|
|
825
|
+
}
|
|
826
|
+
function lessThan(a, b) {
|
|
827
|
+
assertSameCurrency(a, b);
|
|
828
|
+
return a.amount < b.amount;
|
|
829
|
+
}
|
|
830
|
+
function lessThanOrEqual(a, b) {
|
|
831
|
+
assertSameCurrency(a, b);
|
|
832
|
+
return a.amount <= b.amount;
|
|
833
|
+
}
|
|
834
|
+
function isPositive(money) {
|
|
835
|
+
return money.amount > 0n;
|
|
836
|
+
}
|
|
837
|
+
function isNegative(money) {
|
|
838
|
+
return money.amount < 0n;
|
|
839
|
+
}
|
|
840
|
+
function isZero(money) {
|
|
841
|
+
return money.amount === 0n;
|
|
842
|
+
}
|
|
843
|
+
function compare(a, b) {
|
|
844
|
+
assertSameCurrency(a, b);
|
|
845
|
+
if (a.amount < b.amount)
|
|
846
|
+
return -1;
|
|
847
|
+
if (a.amount > b.amount)
|
|
848
|
+
return 1;
|
|
849
|
+
return 0;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export { MoneyError, CurrencyMismatchError, InvalidAmountError, DivisionByZeroError, UnknownCurrencyError, OverflowError, ScaleMismatchError, assertSameCurrency, assertAllSameCurrency, bankersRound, EXTENDED_PRECISION, PRECISION_FACTOR, createMoney, parseDecimalToMinorUnits, minorUnitsToDecimal, maxBigInt, minBigInt, ISO_4217_CURRENCIES, getCurrency, registerCurrency, hasCurrency, getAllCurrencies, clearCustomCurrencies, of, ofRounded, fromMinorUnits, zero, fromMajorUnits, CurrencyCodeSchema, CurrencySchema, AmountStringSchema, MoneySchema, DatabaseMoneySchema, MoneyInputSchema, MoneyInternalSchema, FormatOptionsSchema, AllocationRatiosSchema, moneyEqualsOperator, moneyNotEqualsOperator, moneyGreaterThanOperator, moneyGreaterThanOrEqualOperator, moneyLessThanOperator, moneyLessThanOrEqualOperator, moneyBetweenOperator, moneyPositiveOperator, moneyNegativeOperator, moneyZeroOperator, equals, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, isPositive, isNegative, isZero, compare };
|