@blamejs/core 0.11.23 → 0.11.25

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/lib/money.js ADDED
@@ -0,0 +1,699 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.money
4
+ * @nav Domain
5
+ * @title Money
6
+ * @order 500
7
+ * @slug money
8
+ * @featured true
9
+ *
10
+ * @intro
11
+ * Decimal-safe money arithmetic. The framework primitive every
12
+ * billing / invoicing / shop consumer reaches for so the IEEE 754
13
+ * double-precision `0.1 + 0.2 !== 0.3` rounding error never reaches
14
+ * an invoice line, a tax cell, or a ledger row.
15
+ *
16
+ * ## Why not Number
17
+ *
18
+ * JavaScript's `Number` is a binary64 (IEEE 754 double). `0.10` and
19
+ * `0.20` are unrepresentable in binary fraction; the closest binary
20
+ * approximations sum to `0.30000000000000004`. Add 10,000 such
21
+ * approximations into a daily revenue total and the cumulative drift
22
+ * is large enough to fail a SOX 404 reconciliation. The framework's
23
+ * defense is to refuse `Number` at the boundary: `Money` values
24
+ * carry BigInt minor units (cents / pence / sen / yen / fils) and a
25
+ * currency tag pulled from the ISO 4217 catalog. Every arithmetic
26
+ * operation is integer BigInt math; rounding (where it must happen --
27
+ * FX conversion, weighted allocation) is bankers' (half-to-even)
28
+ * by default and explicit when not.
29
+ *
30
+ * ## What ships
31
+ *
32
+ * - `b.money.of(amount, currency)` -- accepts BigInt minor units OR a
33
+ * decimal-shaped string ("12.50"). Numbers refused at the boundary.
34
+ * - `b.money.fromMinorUnits(bigint, currency)` -- direct construction.
35
+ * - `b.money.parse("12.50 USD")` -- bidirectional shape parser
36
+ * (`<amount> <code>` AND `<code> <amount>`).
37
+ * - `b.money.zero(currency)` -- convenience zero.
38
+ * - `b.money.convert(money, toCurrency, fxRateProvider, opts?)` --
39
+ * conversion through an operator-injected rate provider. Framework
40
+ * NEVER bakes rates in.
41
+ * - `b.money.CURRENCIES` -- frozen ISO 4217 catalog (code -> exponent).
42
+ * - `b.money.MoneyError` -- typed refusal class.
43
+ *
44
+ * ## Allocation
45
+ *
46
+ * `m.allocate([w1, w2, ...])` uses the largest-remainder method:
47
+ * floor each weighted share, then distribute the remainder unit-by-
48
+ * unit to shares with the largest fractional remainder. `$10.00 /
49
+ * [1, 1, 1]` returns `[$3.34, $3.33, $3.33]` (sum exact). `$100.00 /
50
+ * [60, 40]` returns `[$60.00, $40.00]`. Deterministic; total
51
+ * preserved by construction.
52
+ *
53
+ * ## Rounding
54
+ *
55
+ * FX conversion rounds half-to-even (bankers'). Opt into half-up at
56
+ * the call site when an operator regime demands it.
57
+ *
58
+ * ## RFC / standards
59
+ *
60
+ * - ISO 4217 -- currency code + minor-unit catalog.
61
+ * - BCP 47 -- locale tags consumed by `format()` via Intl.NumberFormat.
62
+ * - IEEE 754 binary64 -- the binary fraction representation we
63
+ * refuse at the API boundary. Documented to make the refusal
64
+ * visible to auditors.
65
+ *
66
+ * @card
67
+ * BigInt minor units + ISO 4217 catalog + largest-remainder
68
+ * allocation. Numbers refused at the boundary; FX conversion rounds
69
+ * half-to-even.
70
+ */
71
+
72
+ var { defineClass } = require("./framework-error");
73
+
74
+ var MoneyError = defineClass("MoneyError", { alwaysPermanent: true });
75
+
76
+ // ---- ISO 4217 minor-unit catalog ---------------------------------------
77
+ //
78
+ // Sourced from the ISO 4217 maintenance agency's "current funds" table.
79
+ // Exponent is the number of digits after the decimal that compose one
80
+ // major unit (USD -> 2, JPY -> 0, KWD -> 3, CLF -> 4). The framework
81
+ // never embeds rates -- operators inject FX via the rateProvider
82
+ // contract.
83
+ //
84
+ // The catalog is intentionally narrow: the ~36 currencies operators
85
+ // actually price + settle in. Adding an entry is a one-line edit
86
+ // (code + exponent). Codes the catalog does NOT recognise are refused
87
+ // at construction time so operators catch typos at boot.
88
+
89
+ var _CURRENCIES = Object.freeze({
90
+ // exponent 2 (cents / pence / centavos / oere / etc.)
91
+ USD: 2, EUR: 2, GBP: 2, CHF: 2, CAD: 2, AUD: 2, NZD: 2,
92
+ HKD: 2, SGD: 2, CNY: 2, TWD: 2, INR: 2, BRL: 2, MXN: 2,
93
+ ZAR: 2, SEK: 2, NOK: 2, DKK: 2, PLN: 2, CZK: 2, HUF: 2,
94
+ RUB: 2, TRY: 2, AED: 2, SAR: 2, ILS: 2, THB: 2, IDR: 2,
95
+ PHP: 2, MYR: 2, VND: 2, ARS: 2, COP: 2, PEN: 2,
96
+ // exponent 0 (whole-unit minor -- yen / won / etc.)
97
+ JPY: 0, KRW: 0, CLP: 0,
98
+ // exponent 3 (fils / dinar fractions per ISO 4217)
99
+ KWD: 3, BHD: 3, JOD: 3, OMR: 3, TND: 3,
100
+ // exponent 4 (CLF -- UF unidad de fomento)
101
+ CLF: 4,
102
+ });
103
+
104
+ var CURRENCIES = _CURRENCIES; // exported alias; same frozen object
105
+
106
+ // _pow10(n) -- BigInt 10^n, computed once per call. Avoids any raw
107
+ // `10n ** Xn` literal in the source and centralises the negative-n
108
+ // refusal in one place.
109
+ function _pow10(n) {
110
+ var p = 1n;
111
+ for (var i = 0; i < n; i++) p = p * 10n;
112
+ return p;
113
+ }
114
+
115
+ function _isCurrencyCode(code) {
116
+ return typeof code === "string" &&
117
+ Object.prototype.hasOwnProperty.call(_CURRENCIES, code);
118
+ }
119
+
120
+ function _requireCurrency(code) {
121
+ if (!_isCurrencyCode(code)) {
122
+ throw new MoneyError("money/bad-currency",
123
+ "unknown ISO 4217 currency code: " + JSON.stringify(code));
124
+ }
125
+ }
126
+
127
+ function _exponentOf(code) {
128
+ return _CURRENCIES[code];
129
+ }
130
+
131
+ // _parseDecimalString -- accepts strings of the form `[-]<int>[.<frac>]`
132
+ // and returns a BigInt minor-unit count under the requested exponent.
133
+ // Fractional digits beyond the exponent ARE refused (silent truncation
134
+ // would erase audit-relevant precision; operators round at the call
135
+ // site if they need to).
136
+ var _DECIMAL_RE = /^(-)?(\d+)(?:\.(\d+))?$/;
137
+ function _parseDecimalString(amount, exponent) {
138
+ if (typeof amount !== "string" || amount.length === 0) {
139
+ throw new MoneyError("money/bad-amount",
140
+ "amount string must be non-empty");
141
+ }
142
+ // Strict shape: optional sign, integer part, optional fractional
143
+ // part. No exponent notation (`1e3`), no thousands separators
144
+ // (locale ambiguity -> operator pre-normalises), no whitespace.
145
+ var match = amount.match(_DECIMAL_RE);
146
+ if (!match) {
147
+ throw new MoneyError("money/bad-amount",
148
+ "amount string must match /^-?\\d+(\\.\\d+)?$/, got " +
149
+ JSON.stringify(amount));
150
+ }
151
+ var sign = match[1] === "-" ? -1n : 1n;
152
+ var intPart = match[2];
153
+ var fracPart = match[3] || "";
154
+ if (fracPart.length > exponent) {
155
+ throw new MoneyError("money/precision-loss",
156
+ "amount " + JSON.stringify(amount) + " has " + fracPart.length +
157
+ " fractional digit(s); currency allows " + exponent);
158
+ }
159
+ // Pad fractional part to the exponent so we can read as one BigInt.
160
+ while (fracPart.length < exponent) fracPart = fracPart + "0";
161
+ var minor = BigInt(intPart + fracPart);
162
+ return sign * minor;
163
+ }
164
+
165
+ // _formatMinorUnits -- render a BigInt minor-unit count as a decimal
166
+ // string with the requested exponent. Used by toString() and as the
167
+ // fallback for format() when Intl.NumberFormat isn't a fit.
168
+ function _formatMinorUnits(minor, exponent) {
169
+ var neg = minor < 0n;
170
+ var abs = neg ? -minor : minor;
171
+ if (exponent === 0) return (neg ? "-" : "") + abs.toString();
172
+ var s = abs.toString();
173
+ while (s.length <= exponent) s = "0" + s;
174
+ var head = s.slice(0, s.length - exponent);
175
+ var tail = s.slice(s.length - exponent);
176
+ return (neg ? "-" : "") + head + "." + tail;
177
+ }
178
+
179
+ // _requireSameCurrency -- throws on cross-currency arithmetic.
180
+ // Preserves the invariant; operators catch the bug at the call site.
181
+ function _requireSameCurrency(a, b, op) {
182
+ if (a.currency !== b.currency) {
183
+ throw new MoneyError("money/currency-mismatch",
184
+ "cannot " + op + " " + a.currency + " and " + b.currency +
185
+ "; convert first via b.money.convert(...)");
186
+ }
187
+ }
188
+
189
+ function _requireMoney(value, label) {
190
+ if (!(value instanceof Money)) {
191
+ throw new MoneyError("money/bad-operand",
192
+ label + " must be a Money instance");
193
+ }
194
+ }
195
+
196
+ // ---- Money value class -------------------------------------------------
197
+
198
+ /**
199
+ * @primitive b.money.Money
200
+ * @signature b.money.Money(minorUnits, currency)
201
+ * @since 0.11.25
202
+ * @status stable
203
+ * @related b.money.of, b.money.fromMinorUnits
204
+ *
205
+ * The immutable `Money` value class. Operators rarely construct
206
+ * directly -- reach for `b.money.of` (string or BigInt) or
207
+ * `b.money.fromMinorUnits` (BigInt) instead. The class is exported
208
+ * so `instance instanceof b.money.Money` is a stable type check
209
+ * when receiving Money values across module boundaries.
210
+ *
211
+ * Instance methods: `add`, `subtract`, `multiply`, `allocate`,
212
+ * `negate`, `abs`, `equals`, `lessThan`, `greaterThan`,
213
+ * `lessThanOrEqual`, `greaterThanOrEqual`, `isZero`, `isNegative`,
214
+ * `isPositive`, `toMinorUnits`, `toString`, `toJSON`, `format`.
215
+ *
216
+ * @example
217
+ * var m = new b.money.Money(1250n, "USD");
218
+ * m instanceof b.money.Money;
219
+ */
220
+ function Money(minorUnits, currency) {
221
+ if (typeof minorUnits !== "bigint") {
222
+ throw new MoneyError("money/bad-minor-units",
223
+ "minorUnits must be a BigInt; got " + (typeof minorUnits));
224
+ }
225
+ _requireCurrency(currency);
226
+ this._minor = minorUnits;
227
+ this.currency = currency;
228
+ Object.freeze(this);
229
+ }
230
+
231
+ Money.prototype.toMinorUnits = function () { return this._minor; };
232
+
233
+ Money.prototype.add = function (other) {
234
+ _requireMoney(other, "add operand");
235
+ _requireSameCurrency(this, other, "add");
236
+ return new Money(this._minor + other._minor, this.currency);
237
+ };
238
+
239
+ Money.prototype.subtract = function (other) {
240
+ _requireMoney(other, "subtract operand");
241
+ _requireSameCurrency(this, other, "subtract");
242
+ return new Money(this._minor - other._minor, this.currency);
243
+ };
244
+
245
+ // multiply -- scale by a rational. Accepts either a BigInt scalar
246
+ // (`m.multiply(3n)`), a `[numerator, denominator]` BigInt pair, or a
247
+ // decimal-shaped string (`"1.085"`). Refuses Number -- same boundary
248
+ // discipline as construction.
249
+ Money.prototype.multiply = function (factor, opts) {
250
+ opts = opts || {};
251
+ var rounding = opts.rounding === "half-up" ? "half-up" : "half-even";
252
+ var num;
253
+ var den;
254
+ if (typeof factor === "bigint") {
255
+ return new Money(this._minor * factor, this.currency);
256
+ }
257
+ if (Array.isArray(factor) && factor.length === 2 &&
258
+ typeof factor[0] === "bigint" && typeof factor[1] === "bigint") {
259
+ num = factor[0];
260
+ den = factor[1];
261
+ } else if (typeof factor === "string") {
262
+ var parsed = _rationalFromDecimalString(factor);
263
+ num = parsed.num;
264
+ den = parsed.den;
265
+ } else {
266
+ throw new MoneyError("money/bad-factor",
267
+ "multiply factor must be BigInt, [num, den] BigInt pair, or " +
268
+ "decimal string; refuse Number to keep the no-binary-fraction " +
269
+ "invariant intact");
270
+ }
271
+ if (den === 0n) {
272
+ throw new MoneyError("money/division-by-zero",
273
+ "multiply denominator is zero");
274
+ }
275
+ var product = this._minor * num;
276
+ var quotient = _divRound(product, den, rounding);
277
+ return new Money(quotient, this.currency);
278
+ };
279
+
280
+ // _rationalFromDecimalString -- `"1.085"` -> { num: 1085n, den: 1000n }.
281
+ // Strict shape; same refusals as _parseDecimalString.
282
+ function _rationalFromDecimalString(s) {
283
+ if (typeof s !== "string" || s.length === 0) {
284
+ throw new MoneyError("money/bad-factor",
285
+ "decimal factor must be a non-empty string");
286
+ }
287
+ var m = s.match(_DECIMAL_RE);
288
+ if (!m) {
289
+ throw new MoneyError("money/bad-factor",
290
+ "decimal factor must match /^-?\\d+(\\.\\d+)?$/, got " +
291
+ JSON.stringify(s));
292
+ }
293
+ var sign = m[1] === "-" ? -1n : 1n;
294
+ var intPart = m[2];
295
+ var fracPart = m[3] || "";
296
+ var num = sign * BigInt(intPart + fracPart);
297
+ var den = _pow10(fracPart.length);
298
+ return { num: num, den: den };
299
+ }
300
+
301
+ // _divRound -- integer-divide `n / d` and round per `rounding`.
302
+ // half-even: banker's rounding, the IEEE 754 default and the ISO 80000
303
+ // recommendation. half-up: rounds .5 away from zero.
304
+ function _divRound(n, d, rounding) {
305
+ if (d === 0n) {
306
+ throw new MoneyError("money/division-by-zero",
307
+ "divisor is zero in _divRound");
308
+ }
309
+ // Work with signed values; normalise sign to denominator-positive so
310
+ // the half-way arithmetic doesn't have to handle d < 0.
311
+ if (d < 0n) { n = -n; d = -d; }
312
+ var q = n / d;
313
+ var r = n - q * d;
314
+ if (r === 0n) return q;
315
+ var twiceRemAbs = r < 0n ? -r * 2n : r * 2n;
316
+ var cmp; // -1: below half, 0: exactly half, 1: above half
317
+ if (twiceRemAbs < d) cmp = -1;
318
+ else if (twiceRemAbs === d) cmp = 0;
319
+ else cmp = 1;
320
+ var bump;
321
+ if (cmp < 0) {
322
+ bump = 0n;
323
+ } else if (cmp > 0) {
324
+ bump = 1n;
325
+ } else if (rounding === "half-up") {
326
+ bump = 1n;
327
+ } else {
328
+ // half-even -- only bump if q is odd (after sign).
329
+ var qAbs = q < 0n ? -q : q;
330
+ bump = (qAbs % 2n === 1n) ? 1n : 0n;
331
+ }
332
+ if (bump === 0n) return q;
333
+ return r < 0n ? q - 1n : q + 1n;
334
+ }
335
+
336
+ // allocate -- split `this` into `weights.length` parts proportional to
337
+ // the weights, distributing every minor unit. Largest-remainder
338
+ // method: floor each share, then hand out the leftover units to the
339
+ // shares with the largest fractional remainder. Total preserved by
340
+ // construction; deterministic across runtimes.
341
+ Money.prototype.allocate = function (weights) {
342
+ if (!Array.isArray(weights) || weights.length === 0) {
343
+ throw new MoneyError("money/bad-weights",
344
+ "allocate requires a non-empty array of weights");
345
+ }
346
+ var sum = 0n;
347
+ var w = new Array(weights.length);
348
+ for (var i = 0; i < weights.length; i++) {
349
+ var wi = weights[i];
350
+ var wBig;
351
+ if (typeof wi === "bigint") wBig = wi;
352
+ else if (typeof wi === "number" && Number.isInteger(wi)) wBig = BigInt(wi);
353
+ else {
354
+ throw new MoneyError("money/bad-weight",
355
+ "weight[" + i + "] must be BigInt or integer Number; got " +
356
+ (typeof wi));
357
+ }
358
+ if (wBig < 0n) {
359
+ throw new MoneyError("money/bad-weight",
360
+ "weight[" + i + "] is negative; allocation refuses negative shares");
361
+ }
362
+ w[i] = wBig;
363
+ sum = sum + wBig;
364
+ }
365
+ if (sum === 0n) {
366
+ throw new MoneyError("money/bad-weights",
367
+ "allocate weights sum to zero");
368
+ }
369
+ var total = this._minor;
370
+ var shares = new Array(weights.length);
371
+ var remainders = new Array(weights.length);
372
+ var allocated = 0n;
373
+ for (var j = 0; j < weights.length; j++) {
374
+ // share = floor(total * w[j] / sum). For BigInt /, truncation is
375
+ // toward zero; we want floor (toward -infinity) so the leftover
376
+ // pass below distributes positively. Adjust when total < 0.
377
+ var num2 = total * w[j];
378
+ var q = num2 / sum;
379
+ var r = num2 - q * sum;
380
+ if (r !== 0n && total < 0n) {
381
+ q = q - 1n;
382
+ r = r + sum;
383
+ }
384
+ shares[j] = q;
385
+ remainders[j] = { idx: j, rem: r };
386
+ allocated = allocated + q;
387
+ }
388
+ var leftover = total - allocated;
389
+ // leftover is non-negative after the floor adjustment above (proof:
390
+ // for total >= 0, each share is floor toward zero so allocated <=
391
+ // total; for total < 0, each share is floored toward -infty so
392
+ // allocated <= total likewise). Distribute one unit at a time to
393
+ // the largest remainder. Ties broken by index (deterministic).
394
+ remainders.sort(function (a, b) {
395
+ if (a.rem === b.rem) return a.idx - b.idx;
396
+ return a.rem < b.rem ? 1 : -1;
397
+ });
398
+ var k = 0;
399
+ while (leftover > 0n) {
400
+ shares[remainders[k % remainders.length].idx] =
401
+ shares[remainders[k % remainders.length].idx] + 1n;
402
+ leftover = leftover - 1n;
403
+ k = k + 1;
404
+ }
405
+ var out = new Array(shares.length);
406
+ for (var s2 = 0; s2 < shares.length; s2++) {
407
+ out[s2] = new Money(shares[s2], this.currency);
408
+ }
409
+ return out;
410
+ };
411
+
412
+ Money.prototype.negate = function () {
413
+ return new Money(-this._minor, this.currency);
414
+ };
415
+
416
+ Money.prototype.abs = function () {
417
+ return new Money(this._minor < 0n ? -this._minor : this._minor, this.currency);
418
+ };
419
+
420
+ Money.prototype.equals = function (other) {
421
+ _requireMoney(other, "equals operand");
422
+ return this.currency === other.currency && this._minor === other._minor;
423
+ };
424
+
425
+ Money.prototype.lessThan = function (other) {
426
+ _requireMoney(other, "lessThan operand");
427
+ _requireSameCurrency(this, other, "compare");
428
+ return this._minor < other._minor;
429
+ };
430
+
431
+ Money.prototype.greaterThan = function (other) {
432
+ _requireMoney(other, "greaterThan operand");
433
+ _requireSameCurrency(this, other, "compare");
434
+ return this._minor > other._minor;
435
+ };
436
+
437
+ Money.prototype.lessThanOrEqual = function (other) {
438
+ _requireMoney(other, "lessThanOrEqual operand");
439
+ _requireSameCurrency(this, other, "compare");
440
+ return this._minor <= other._minor;
441
+ };
442
+
443
+ Money.prototype.greaterThanOrEqual = function (other) {
444
+ _requireMoney(other, "greaterThanOrEqual operand");
445
+ _requireSameCurrency(this, other, "compare");
446
+ return this._minor >= other._minor;
447
+ };
448
+
449
+ Money.prototype.isZero = function () {
450
+ return this._minor === 0n;
451
+ };
452
+
453
+ Money.prototype.isNegative = function () {
454
+ return this._minor < 0n;
455
+ };
456
+
457
+ Money.prototype.isPositive = function () {
458
+ return this._minor > 0n;
459
+ };
460
+
461
+ // toString -- canonical `<decimal-major> <code>` shape. Always
462
+ // 2-decimal for USD-shaped currencies, 0-decimal for JPY, 3-decimal
463
+ // for KWD, 4-decimal for CLF. The shape round-trips through
464
+ // `b.money.parse`.
465
+ Money.prototype.toString = function () {
466
+ var exp = _exponentOf(this.currency);
467
+ return _formatMinorUnits(this._minor, exp) + " " + this.currency;
468
+ };
469
+
470
+ // toJSON -- operator-side serialisation. `minorUnits` is rendered as a
471
+ // decimal string (BigInt isn't JSON-native; loss-free string survives
472
+ // every transport). Pair with `fromJSON` for round-trip.
473
+ Money.prototype.toJSON = function () {
474
+ return { minorUnits: this._minor.toString(), currency: this.currency };
475
+ };
476
+
477
+ // format -- locale-aware string via Intl.NumberFormat when a locale is
478
+ // supplied or when the host runtime exposes Intl (every Node ICU
479
+ // build does). Falls back to `toString()` on any host that lacks Intl
480
+ // or rejects the currency. Operators wanting strict-locale rendering
481
+ // reach for Intl.NumberFormat directly; this method is the convenience
482
+ // shape.
483
+ Money.prototype.format = function (locale) {
484
+ var exp = _exponentOf(this.currency);
485
+ // Compose the decimal value as a Number ONLY for the Intl call --
486
+ // never use it for arithmetic. JPY (exp=0) is exact; for larger
487
+ // exponents we pass a string-derived Number knowing the formatter
488
+ // re-quantises to the currency exponent via minimumFractionDigits.
489
+ var decStr = _formatMinorUnits(this._minor, exp);
490
+ var asNum = Number(decStr);
491
+ if (typeof Intl !== "undefined" && Intl.NumberFormat) {
492
+ try {
493
+ var fmt = new Intl.NumberFormat(locale || undefined, {
494
+ style: "currency",
495
+ currency: this.currency,
496
+ minimumFractionDigits: exp,
497
+ maximumFractionDigits: exp,
498
+ });
499
+ return fmt.format(asNum);
500
+ } catch (_e) {
501
+ // Locale or currency rejected by ICU -- fall through to canonical.
502
+ }
503
+ }
504
+ return this.toString();
505
+ };
506
+
507
+ // ---- Factories ---------------------------------------------------------
508
+
509
+ /**
510
+ * @primitive b.money.of
511
+ * @signature b.money.of(amount, currency)
512
+ * @since 0.11.25
513
+ * @status stable
514
+ * @related b.money.fromMinorUnits, b.money.parse
515
+ *
516
+ * Build a `Money` from `amount` (BigInt minor units OR decimal-shaped
517
+ * string) and an ISO 4217 currency code. Throws `MoneyError` on bad
518
+ * shape. Numbers are refused at the boundary -- the framework's
519
+ * defense against IEEE 754 binary-fraction drift.
520
+ *
521
+ * @example
522
+ * var price = b.money.of("12.50", "USD");
523
+ * var fee = b.money.of(250n, "USD");
524
+ * var tip = b.money.of("0", "JPY");
525
+ */
526
+ function of(amount, currency) {
527
+ _requireCurrency(currency);
528
+ var exp = _exponentOf(currency);
529
+ if (typeof amount === "bigint") {
530
+ return new Money(amount, currency);
531
+ }
532
+ if (typeof amount === "string") {
533
+ return new Money(_parseDecimalString(amount, exp), currency);
534
+ }
535
+ if (typeof amount === "number") {
536
+ throw new MoneyError("money/number-refused",
537
+ "Number amounts refused at the API boundary -- pass BigInt minor " +
538
+ "units (e.g. 250n) or a decimal-shaped string (\"2.50\"). " +
539
+ "Number values lose precision under IEEE 754 binary fractions.");
540
+ }
541
+ throw new MoneyError("money/bad-amount",
542
+ "amount must be BigInt minor units or decimal-shaped string; got " +
543
+ (typeof amount));
544
+ }
545
+
546
+ /**
547
+ * @primitive b.money.fromMinorUnits
548
+ * @signature b.money.fromMinorUnits(minorUnits, currency)
549
+ * @since 0.11.25
550
+ * @status stable
551
+ * @related b.money.of
552
+ *
553
+ * Build a `Money` directly from a BigInt minor-unit count. The
554
+ * lowest-overhead constructor; useful when restoring from a ledger
555
+ * row or a wire-shape `toJSON` payload.
556
+ *
557
+ * @example
558
+ * var due = b.money.fromMinorUnits(1250n, "USD");
559
+ */
560
+ function fromMinorUnits(minorUnits, currency) {
561
+ return new Money(minorUnits, currency);
562
+ }
563
+
564
+ // _PARSE_AMOUNT_FIRST_RE / _PARSE_CODE_FIRST_RE -- the two accepted shapes.
565
+ var _PARSE_AMOUNT_FIRST_RE = /^(-?\d+(?:\.\d+)?)\s+([A-Z]{3})$/;
566
+ var _PARSE_CODE_FIRST_RE = /^([A-Z]{3})\s+(-?\d+(?:\.\d+)?)$/;
567
+
568
+ /**
569
+ * @primitive b.money.parse
570
+ * @signature b.money.parse(input)
571
+ * @since 0.11.25
572
+ * @status stable
573
+ * @related b.money.of, Money.prototype.toString
574
+ *
575
+ * Parse a string of the form `<amount> <code>` OR `<code> <amount>`
576
+ * into a `Money`. The two shapes round-trip with `toString()` (which
577
+ * emits the amount-first canonical form). Whitespace between amount
578
+ * and code is required; locale-formatted strings (thousands separator,
579
+ * `$` glyph) are refused -- operators normalise at the call site.
580
+ *
581
+ * @example
582
+ * b.money.parse("12.50 USD");
583
+ * b.money.parse("USD 12.50");
584
+ * b.money.parse("12 JPY");
585
+ * b.money.parse("12.500 KWD");
586
+ */
587
+ function parse(input) {
588
+ if (typeof input !== "string") {
589
+ throw new MoneyError("money/bad-input",
590
+ "parse expects a string; got " + (typeof input));
591
+ }
592
+ var trimmed = input.trim();
593
+ var m1 = trimmed.match(_PARSE_AMOUNT_FIRST_RE);
594
+ if (m1) return of(m1[1], m1[2]);
595
+ var m2 = trimmed.match(_PARSE_CODE_FIRST_RE);
596
+ if (m2) return of(m2[2], m2[1]);
597
+ throw new MoneyError("money/bad-input",
598
+ "parse: input must match `<amount> <code>` or `<code> <amount>`, " +
599
+ "got " + JSON.stringify(input));
600
+ }
601
+
602
+ /**
603
+ * @primitive b.money.zero
604
+ * @signature b.money.zero(currency)
605
+ * @since 0.11.25
606
+ * @status stable
607
+ * @related b.money.of
608
+ *
609
+ * Return a zero-valued `Money` in the requested currency. Convenience
610
+ * for fold/sum accumulators.
611
+ *
612
+ * @example
613
+ * var total = items.reduce(function (acc, it) { return acc.add(it.price); },
614
+ * b.money.zero("USD"));
615
+ */
616
+ function zero(currency) {
617
+ _requireCurrency(currency);
618
+ return new Money(0n, currency);
619
+ }
620
+
621
+ /**
622
+ * @primitive b.money.convert
623
+ * @signature b.money.convert(money, toCurrency, fxRateProvider, opts?)
624
+ * @since 0.11.25
625
+ * @status stable
626
+ * @related b.money.of, Money.prototype.multiply
627
+ *
628
+ * Convert `money` to `toCurrency` through an operator-injected rate
629
+ * provider. The framework NEVER bakes in rates -- operators wire a
630
+ * provider that pulls from an external FX feed (ECB / OANDA / their
631
+ * internal treasury system) and refresh on whatever cadence their
632
+ * regime requires.
633
+ *
634
+ * The `fxRateProvider.rate(from, to)` contract returns a decimal-
635
+ * shaped string (`"1.085"`) -- never a Number. Conversion math runs
636
+ * in BigInt with the provider rate's denominator; rounding is
637
+ * half-to-even by default (operator opts into half-up via
638
+ * `opts.rounding`).
639
+ *
640
+ * @opts
641
+ * rounding: "half-even" | "half-up", // default "half-even" (bankers')
642
+ *
643
+ * @example
644
+ * var rates = { rate: function (from, to) { return "0.92"; } };
645
+ * var eur = b.money.convert(b.money.of("100.00", "USD"), "EUR", rates);
646
+ */
647
+ function convert(money, toCurrency, fxRateProvider, opts) {
648
+ _requireMoney(money, "convert source");
649
+ _requireCurrency(toCurrency);
650
+ if (!fxRateProvider || typeof fxRateProvider.rate !== "function") {
651
+ throw new MoneyError("money/bad-fx-provider",
652
+ "convert requires an fxRateProvider with a .rate(from, to) method");
653
+ }
654
+ opts = opts || {};
655
+ if (money.currency === toCurrency) {
656
+ // Identity conversion still flows through so the operator's audit
657
+ // hook (if any) sees the call. New instance -- Money is immutable.
658
+ return new Money(money._minor, money.currency);
659
+ }
660
+ var rateStr = fxRateProvider.rate(money.currency, toCurrency);
661
+ if (typeof rateStr !== "string") {
662
+ throw new MoneyError("money/bad-rate",
663
+ "fxRateProvider.rate must return a decimal-shaped string; got " +
664
+ (typeof rateStr));
665
+ }
666
+ var rational = _rationalFromDecimalString(rateStr);
667
+ if (rational.num < 0n) {
668
+ throw new MoneyError("money/bad-rate",
669
+ "fxRateProvider.rate returned a negative rate: " +
670
+ JSON.stringify(rateStr));
671
+ }
672
+ var fromExp = _exponentOf(money.currency);
673
+ var toExp = _exponentOf(toCurrency);
674
+ // Scale source minor units into the destination's exponent before
675
+ // applying the rate, so the rounding step happens once at the end.
676
+ // amount_to = amount_from * (10^toExp / 10^fromExp) * (num / den)
677
+ var rounding = opts.rounding === "half-up" ? "half-up" : "half-even";
678
+ var num = money._minor * rational.num;
679
+ var den = rational.den;
680
+ if (toExp > fromExp) {
681
+ num = num * _pow10(toExp - fromExp);
682
+ } else if (fromExp > toExp) {
683
+ den = den * _pow10(fromExp - toExp);
684
+ }
685
+ var minor = _divRound(num, den, rounding);
686
+ return new Money(minor, toCurrency);
687
+ }
688
+
689
+ module.exports = {
690
+ of: of,
691
+ create: of, // alias -- matches spec's "create or of"
692
+ fromMinorUnits: fromMinorUnits,
693
+ parse: parse,
694
+ zero: zero,
695
+ convert: convert,
696
+ CURRENCIES: CURRENCIES,
697
+ Money: Money,
698
+ MoneyError: MoneyError,
699
+ };