subunit-money 3.1.0 → 3.2.1

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/dist/money.js DELETED
@@ -1,358 +0,0 @@
1
- /**
2
- * Money - An immutable value object for monetary amounts.
3
- *
4
- * Design principles:
5
- * - Immutable: all operations return new instances
6
- * - Type-safe: currency mismatches are caught at compile time (when possible) and runtime
7
- * - Precise: uses BigInt internally to avoid floating-point errors
8
- * - String-based API: amounts are strings to preserve precision in JSON/DB round-trips
9
- */
10
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
11
- if (kind === "m") throw new TypeError("Private method is not writable");
12
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
13
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
14
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
15
- };
16
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
17
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
18
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
19
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
20
- };
21
- var _Money_instances, _a, _Money_subunits, _Money_currencyDef, _Money_parseAmount, _Money_assertSameCurrency, _Money_getInternalValue, _Money_parseFactor, _Money_roundedDivide, _Money_createFromSubunits, _Money_formatSubunits;
22
- import { CurrencyMismatchError, CurrencyUnknownError, SubunitError, AmountError, } from './errors.js';
23
- import { getCurrency } from './currency.js';
24
- /**
25
- * Money class - represents a monetary amount in a specific currency.
26
- *
27
- * @typeParam C - The currency code type (enables compile-time currency checking)
28
- *
29
- * @example
30
- * const price = new Money('USD', '19.99')
31
- * const tax = price.multiply(0.08)
32
- * const total = price.add(tax)
33
- * console.log(total.amount) // "21.59"
34
- */
35
- export class Money {
36
- /**
37
- * Create a new Money instance.
38
- *
39
- * @param currency - ISO 4217 currency code (must be registered)
40
- * @param amount - The amount as a number or string
41
- * @throws {CurrencyUnknownError} If the currency is not registered
42
- * @throws {AmountError} If the amount is not a valid number
43
- * @throws {SubunitError} If the amount has more decimals than the currency allows
44
- */
45
- constructor(currency, amount) {
46
- _Money_instances.add(this);
47
- // Private BigInt storage - stores currency native subunits directly
48
- _Money_subunits.set(this, void 0);
49
- _Money_currencyDef.set(this, void 0);
50
- const currencyDef = getCurrency(currency);
51
- if (!currencyDef) {
52
- throw new CurrencyUnknownError(currency);
53
- }
54
- this.currency = currency;
55
- __classPrivateFieldSet(this, _Money_currencyDef, currencyDef, "f");
56
- __classPrivateFieldSet(this, _Money_subunits, __classPrivateFieldGet(this, _Money_instances, "m", _Money_parseAmount).call(this, amount), "f");
57
- }
58
- /**
59
- * The amount as a formatted string with correct decimal places.
60
- * @example
61
- * new Money('USD', 19.9).amount // "19.90"
62
- * new Money('JPY', 1000).amount // "1000"
63
- */
64
- get amount() {
65
- const decimals = __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits;
66
- const abs = __classPrivateFieldGet(this, _Money_subunits, "f") < 0n ? -__classPrivateFieldGet(this, _Money_subunits, "f") : __classPrivateFieldGet(this, _Money_subunits, "f");
67
- const isNegative = __classPrivateFieldGet(this, _Money_subunits, "f") < 0n;
68
- if (decimals === 0) {
69
- return `${isNegative ? '-' : ''}${abs}`;
70
- }
71
- const multiplier = 10n ** BigInt(decimals);
72
- const wholePart = abs / multiplier;
73
- const fracPart = abs % multiplier;
74
- const sign = isNegative ? '-' : '';
75
- return `${sign}${wholePart}.${fracPart.toString().padStart(decimals, '0')}`;
76
- }
77
- // ============ Arithmetic Operations ============
78
- /**
79
- * Add another Money amount.
80
- * @throws {CurrencyMismatchError} If currencies don't match
81
- */
82
- add(other) {
83
- __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
84
- const result = __classPrivateFieldGet(this, _Money_subunits, "f") + __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
85
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
86
- }
87
- /**
88
- * Subtract another Money amount.
89
- * @throws {CurrencyMismatchError} If currencies don't match
90
- */
91
- subtract(other) {
92
- __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
93
- const result = __classPrivateFieldGet(this, _Money_subunits, "f") - __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
94
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
95
- }
96
- /**
97
- * Multiply by a factor.
98
- *
99
- * DESIGN: Rounds immediately after multiplication using banker's rounding
100
- * (round half-to-even). This prevents the "split penny problem".
101
- */
102
- multiply(factor) {
103
- if (typeof factor !== 'number' || !Number.isFinite(factor)) {
104
- throw new TypeError(`Factor must be a finite number, got: ${factor}`);
105
- }
106
- const { value: factorValue, scale } = __classPrivateFieldGet(_a, _a, "m", _Money_parseFactor).call(_a, factor);
107
- const product = __classPrivateFieldGet(this, _Money_subunits, "f") * factorValue;
108
- const divisor = 10n ** scale;
109
- const result = __classPrivateFieldGet(_a, _a, "m", _Money_roundedDivide).call(_a, product, divisor);
110
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
111
- }
112
- /**
113
- * Allocate this amount proportionally.
114
- * Handles remainder distribution to avoid losing pennies.
115
- *
116
- * @param proportions - Array of proportions (e.g., [1, 1, 1] for three-way split)
117
- * @returns Array of Money objects that sum to the original amount
118
- */
119
- allocate(proportions) {
120
- if (!Array.isArray(proportions) || proportions.length === 0) {
121
- throw new TypeError('Proportions must be a non-empty array');
122
- }
123
- for (const p of proportions) {
124
- if (typeof p !== 'number' || !Number.isFinite(p) || p < 0) {
125
- throw new TypeError('All proportions must be non-negative finite numbers');
126
- }
127
- }
128
- const total = proportions.reduce((sum, p) => sum + p, 0);
129
- if (total <= 0) {
130
- throw new TypeError('Sum of proportions must be positive');
131
- }
132
- const totalSubunits = __classPrivateFieldGet(this, _Money_subunits, "f");
133
- // Calculate base allocations
134
- const allocations = proportions.map((p) => {
135
- return (totalSubunits * BigInt(Math.round(p * 1000000))) / BigInt(Math.round(total * 1000000));
136
- });
137
- // Distribute remainder
138
- let remainder = totalSubunits - allocations.reduce((sum, a) => sum + a, 0n);
139
- let i = 0;
140
- while (remainder > 0n) {
141
- allocations[i % allocations.length] += 1n;
142
- remainder -= 1n;
143
- i++;
144
- }
145
- while (remainder < 0n) {
146
- allocations[i % allocations.length] -= 1n;
147
- remainder += 1n;
148
- i++;
149
- }
150
- // Convert back to Money objects
151
- return allocations.map((subunits) => {
152
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, subunits, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
153
- });
154
- }
155
- // ============ Comparison Operations ============
156
- /**
157
- * Check if this amount equals another.
158
- * @throws {CurrencyMismatchError} If currencies don't match
159
- */
160
- equalTo(other) {
161
- __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
162
- return __classPrivateFieldGet(this, _Money_subunits, "f") === __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
163
- }
164
- /**
165
- * Check if this amount is greater than another.
166
- * @throws {CurrencyMismatchError} If currencies don't match
167
- */
168
- greaterThan(other) {
169
- __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
170
- return __classPrivateFieldGet(this, _Money_subunits, "f") > __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
171
- }
172
- /**
173
- * Check if this amount is less than another.
174
- * @throws {CurrencyMismatchError} If currencies don't match
175
- */
176
- lessThan(other) {
177
- __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
178
- return __classPrivateFieldGet(this, _Money_subunits, "f") < __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
179
- }
180
- /**
181
- * Check if this amount is greater than or equal to another.
182
- * @throws {CurrencyMismatchError} If currencies don't match
183
- */
184
- greaterThanOrEqual(other) {
185
- __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
186
- return __classPrivateFieldGet(this, _Money_subunits, "f") >= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
187
- }
188
- /**
189
- * Check if this amount is less than or equal to another.
190
- * @throws {CurrencyMismatchError} If currencies don't match
191
- */
192
- lessThanOrEqual(other) {
193
- __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
194
- return __classPrivateFieldGet(this, _Money_subunits, "f") <= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
195
- }
196
- /**
197
- * Check if this amount is zero.
198
- */
199
- isZero() {
200
- return __classPrivateFieldGet(this, _Money_subunits, "f") === 0n;
201
- }
202
- /**
203
- * Check if this amount is positive (greater than zero).
204
- */
205
- isPositive() {
206
- return __classPrivateFieldGet(this, _Money_subunits, "f") > 0n;
207
- }
208
- /**
209
- * Check if this amount is negative (less than zero).
210
- */
211
- isNegative() {
212
- return __classPrivateFieldGet(this, _Money_subunits, "f") < 0n;
213
- }
214
- // ============ Serialization ============
215
- /**
216
- * Convert to a plain object (safe for JSON).
217
- */
218
- toJSON() {
219
- return {
220
- currency: this.currency,
221
- amount: this.amount,
222
- };
223
- }
224
- /**
225
- * Convert to string representation.
226
- */
227
- toString() {
228
- return `${this.amount} ${this.currency}`;
229
- }
230
- /**
231
- * Get the amount as a number (may lose precision for large values).
232
- * Use with caution - prefer string-based operations.
233
- */
234
- toNumber() {
235
- return Number(this.amount);
236
- }
237
- /**
238
- * Get the amount in subunits (e.g., cents for USD).
239
- * Useful for database storage (Stripe-style integer storage).
240
- */
241
- toSubunits() {
242
- return __classPrivateFieldGet(this, _Money_subunits, "f");
243
- }
244
- // ============ Static Factory Methods ============
245
- /**
246
- * Create a Money instance from a plain object.
247
- */
248
- static fromObject(obj) {
249
- return new _a(obj.currency, obj.amount);
250
- }
251
- /**
252
- * Create a Money instance from subunits (e.g., cents).
253
- * Useful for loading from database (Stripe-style integer storage).
254
- */
255
- static fromSubunits(subunits, currency) {
256
- const currencyDef = getCurrency(currency);
257
- if (!currencyDef) {
258
- throw new CurrencyUnknownError(currency);
259
- }
260
- const bigintSubunits = typeof subunits === 'number' ? BigInt(subunits) : subunits;
261
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, bigintSubunits, currency, currencyDef);
262
- }
263
- /**
264
- * Compare two Money objects (for use with Array.sort).
265
- * @throws {CurrencyMismatchError} If currencies don't match
266
- */
267
- static compare(a, b) {
268
- if (a.currency !== b.currency) {
269
- throw new CurrencyMismatchError(a.currency, b.currency);
270
- }
271
- const aVal = __classPrivateFieldGet(a, _Money_instances, "m", _Money_getInternalValue).call(a);
272
- const bVal = __classPrivateFieldGet(b, _Money_instances, "m", _Money_getInternalValue).call(b);
273
- if (aVal < bVal)
274
- return -1;
275
- if (aVal > bVal)
276
- return 1;
277
- return 0;
278
- }
279
- /**
280
- * Create a zero amount in the specified currency.
281
- */
282
- static zero(currency) {
283
- return new _a(currency, '0');
284
- }
285
- }
286
- _a = Money, _Money_subunits = new WeakMap(), _Money_currencyDef = new WeakMap(), _Money_instances = new WeakSet(), _Money_parseAmount = function _Money_parseAmount(amount) {
287
- const str = typeof amount === 'number' ? String(amount) : amount;
288
- const match = str.match(/^(-)?(\d+)(?:\.(\d+))?$/);
289
- if (!match) {
290
- throw new AmountError(amount);
291
- }
292
- const [, sign, whole, frac = ''] = match;
293
- if (frac.length > __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits) {
294
- throw new SubunitError(this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits);
295
- }
296
- const paddedFrac = frac.padEnd(__classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits, '0');
297
- const combined = BigInt(whole + paddedFrac);
298
- return sign === '-' ? -combined : combined;
299
- }, _Money_assertSameCurrency = function _Money_assertSameCurrency(other) {
300
- if (this.currency !== other.currency) {
301
- throw new CurrencyMismatchError(this.currency, other.currency);
302
- }
303
- }, _Money_getInternalValue = function _Money_getInternalValue() {
304
- return __classPrivateFieldGet(this, _Money_subunits, "f");
305
- }, _Money_parseFactor = function _Money_parseFactor(factor) {
306
- const str = String(factor);
307
- const [base, exponent] = str.split('e');
308
- const baseMatch = base.match(/^(-)?(\d+)(?:\.(\d+))?$/);
309
- if (!baseMatch) {
310
- // Fallback for unlikely cases, though String(number) should strictly produce valid formats
311
- throw new TypeError(`Invalid factor format: ${str}`);
312
- }
313
- const [, sign, whole, frac = ''] = baseMatch;
314
- const baseValue = BigInt((sign || '') + whole + frac);
315
- const baseDecimals = frac.length;
316
- const exp = exponent ? Number(exponent) : 0;
317
- const netExp = exp - baseDecimals;
318
- if (netExp >= 0) {
319
- return { value: baseValue * 10n ** BigInt(netExp), scale: 0n };
320
- }
321
- else {
322
- return { value: baseValue, scale: BigInt(-netExp) };
323
- }
324
- }, _Money_roundedDivide = function _Money_roundedDivide(numerator, denominator) {
325
- if (denominator === 1n)
326
- return numerator;
327
- const quotient = numerator / denominator;
328
- const remainder = numerator % denominator;
329
- if (remainder === 0n)
330
- return quotient;
331
- const halfDenominator = denominator / 2n;
332
- const absRemainder = remainder < 0n ? -remainder : remainder;
333
- if (absRemainder > halfDenominator) {
334
- return numerator < 0n ? quotient - 1n : quotient + 1n;
335
- }
336
- if (absRemainder === halfDenominator) {
337
- const isQuotientEven = quotient % 2n === 0n;
338
- if (isQuotientEven) {
339
- return quotient;
340
- }
341
- return numerator < 0n ? quotient - 1n : quotient + 1n;
342
- }
343
- return quotient;
344
- }, _Money_createFromSubunits = function _Money_createFromSubunits(subunits, currency, currencyDef) {
345
- return new _a(currency, __classPrivateFieldGet(_a, _a, "m", _Money_formatSubunits).call(_a, subunits, currencyDef));
346
- }, _Money_formatSubunits = function _Money_formatSubunits(subunits, currencyDef) {
347
- const decimals = currencyDef.decimalDigits;
348
- const abs = subunits < 0n ? -subunits : subunits;
349
- const isNegative = subunits < 0n;
350
- if (decimals === 0) {
351
- return `${isNegative ? '-' : ''}${abs}`;
352
- }
353
- const multiplier = 10n ** BigInt(decimals);
354
- const wholePart = abs / multiplier;
355
- const fracPart = abs % multiplier;
356
- const sign = isNegative ? '-' : '';
357
- return `${sign}${wholePart}.${fracPart.toString().padStart(decimals, '0')}`;
358
- };