subunit-money 2.1.1 → 3.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/dist/money.js CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * Money - An immutable value object for monetary amounts.
4
3
  *
@@ -19,14 +18,9 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
19
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");
20
19
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
21
20
  };
22
- var _Money_instances, _a, _Money_value, _Money_currencyDef, _Money_parseAmount, _Money_assertSameCurrency, _Money_getInternalValue, _Money_roundedDivide, _Money_createFromInternal, _Money_formatInternalValue;
23
- Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.Money = void 0;
25
- const errors_js_1 = require("./errors.js");
26
- const currency_js_1 = require("./currency.js");
27
- // Internal precision: 8 decimal places (supports Bitcoin and most cryptocurrencies)
28
- const INTERNAL_PRECISION = 8;
29
- const PRECISION_MULTIPLIER = 10n ** BigInt(INTERNAL_PRECISION);
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';
30
24
  /**
31
25
  * Money class - represents a monetary amount in a specific currency.
32
26
  *
@@ -38,7 +32,7 @@ const PRECISION_MULTIPLIER = 10n ** BigInt(INTERNAL_PRECISION);
38
32
  * const total = price.add(tax)
39
33
  * console.log(total.amount) // "21.59"
40
34
  */
41
- class Money {
35
+ export class Money {
42
36
  /**
43
37
  * Create a new Money instance.
44
38
  *
@@ -50,16 +44,16 @@ class Money {
50
44
  */
51
45
  constructor(currency, amount) {
52
46
  _Money_instances.add(this);
53
- // Private BigInt storage - not exposed to prevent precision leaks
54
- _Money_value.set(this, void 0);
47
+ // Private BigInt storage - stores currency native subunits directly
48
+ _Money_subunits.set(this, void 0);
55
49
  _Money_currencyDef.set(this, void 0);
56
- const currencyDef = (0, currency_js_1.getCurrency)(currency);
50
+ const currencyDef = getCurrency(currency);
57
51
  if (!currencyDef) {
58
- throw new errors_js_1.CurrencyUnknownError(currency);
52
+ throw new CurrencyUnknownError(currency);
59
53
  }
60
54
  this.currency = currency;
61
55
  __classPrivateFieldSet(this, _Money_currencyDef, currencyDef, "f");
62
- __classPrivateFieldSet(this, _Money_value, __classPrivateFieldGet(this, _Money_instances, "m", _Money_parseAmount).call(this, amount), "f");
56
+ __classPrivateFieldSet(this, _Money_subunits, __classPrivateFieldGet(this, _Money_instances, "m", _Money_parseAmount).call(this, amount), "f");
63
57
  }
64
58
  /**
65
59
  * The amount as a formatted string with correct decimal places.
@@ -69,10 +63,8 @@ class Money {
69
63
  */
70
64
  get amount() {
71
65
  const decimals = __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits;
72
- const divisor = 10n ** BigInt(INTERNAL_PRECISION - decimals);
73
- const adjusted = __classPrivateFieldGet(this, _Money_value, "f") / divisor;
74
- const isNegative = adjusted < 0n;
75
- const abs = isNegative ? -adjusted : adjusted;
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;
76
68
  if (decimals === 0) {
77
69
  return `${isNegative ? '-' : ''}${abs}`;
78
70
  }
@@ -89,8 +81,8 @@ class Money {
89
81
  */
90
82
  add(other) {
91
83
  __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
92
- const result = __classPrivateFieldGet(this, _Money_value, "f") + __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
93
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromInternal).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
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"));
94
86
  }
95
87
  /**
96
88
  * Subtract another Money amount.
@@ -98,30 +90,24 @@ class Money {
98
90
  */
99
91
  subtract(other) {
100
92
  __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
101
- const result = __classPrivateFieldGet(this, _Money_value, "f") - __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
102
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromInternal).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
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"));
103
95
  }
104
96
  /**
105
97
  * Multiply by a factor.
106
98
  *
107
99
  * DESIGN: Rounds immediately after multiplication using banker's rounding
108
- * (round half-to-even). This prevents the "split penny problem" where
109
- * line-item rounding differs from deferred rounding:
110
- * Per-item: $1.65 tax × 10 items = $16.50 ✓ (matches receipt)
111
- * Deferred: 10 × $1.649175 = $16.49 ✗ (missing penny)
112
- *
113
- * For chained calculations without intermediate rounding, perform arithmetic
114
- * in Number space first, then create a Money object with the final result.
100
+ * (round half-to-even). This prevents the "split penny problem".
115
101
  */
116
102
  multiply(factor) {
117
103
  if (typeof factor !== 'number' || !Number.isFinite(factor)) {
118
104
  throw new TypeError(`Factor must be a finite number, got: ${factor}`);
119
105
  }
120
- const factorStr = factor.toFixed(INTERNAL_PRECISION);
121
- const factorBigInt = BigInt(factorStr.replace('.', ''));
122
- const product = __classPrivateFieldGet(this, _Money_value, "f") * factorBigInt;
123
- const result = __classPrivateFieldGet(_a, _a, "m", _Money_roundedDivide).call(_a, product, PRECISION_MULTIPLIER);
124
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromInternal).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
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"));
125
111
  }
126
112
  /**
127
113
  * Allocate this amount proportionally.
@@ -129,10 +115,6 @@ class Money {
129
115
  *
130
116
  * @param proportions - Array of proportions (e.g., [1, 1, 1] for three-way split)
131
117
  * @returns Array of Money objects that sum to the original amount
132
- *
133
- * @example
134
- * new Money('USD', '100').allocate([1, 1, 1])
135
- * // Returns: [Money('33.34'), Money('33.33'), Money('33.33')]
136
118
  */
137
119
  allocate(proportions) {
138
120
  if (!Array.isArray(proportions) || proportions.length === 0) {
@@ -147,10 +129,7 @@ class Money {
147
129
  if (total <= 0) {
148
130
  throw new TypeError('Sum of proportions must be positive');
149
131
  }
150
- // Work in currency subunits to avoid precision loss
151
- const decimals = __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits;
152
- const subunitMultiplier = 10n ** BigInt(INTERNAL_PRECISION - decimals);
153
- const totalSubunits = __classPrivateFieldGet(this, _Money_value, "f") / subunitMultiplier;
132
+ const totalSubunits = __classPrivateFieldGet(this, _Money_subunits, "f");
154
133
  // Calculate base allocations
155
134
  const allocations = proportions.map((p) => {
156
135
  return (totalSubunits * BigInt(Math.round(p * 1000000))) / BigInt(Math.round(total * 1000000));
@@ -170,8 +149,7 @@ class Money {
170
149
  }
171
150
  // Convert back to Money objects
172
151
  return allocations.map((subunits) => {
173
- const internalValue = subunits * subunitMultiplier;
174
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromInternal).call(_a, internalValue, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
152
+ return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, subunits, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
175
153
  });
176
154
  }
177
155
  // ============ Comparison Operations ============
@@ -181,7 +159,7 @@ class Money {
181
159
  */
182
160
  equalTo(other) {
183
161
  __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
184
- return __classPrivateFieldGet(this, _Money_value, "f") === __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
162
+ return __classPrivateFieldGet(this, _Money_subunits, "f") === __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
185
163
  }
186
164
  /**
187
165
  * Check if this amount is greater than another.
@@ -189,7 +167,7 @@ class Money {
189
167
  */
190
168
  greaterThan(other) {
191
169
  __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
192
- return __classPrivateFieldGet(this, _Money_value, "f") > __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
170
+ return __classPrivateFieldGet(this, _Money_subunits, "f") > __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
193
171
  }
194
172
  /**
195
173
  * Check if this amount is less than another.
@@ -197,7 +175,7 @@ class Money {
197
175
  */
198
176
  lessThan(other) {
199
177
  __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
200
- return __classPrivateFieldGet(this, _Money_value, "f") < __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
178
+ return __classPrivateFieldGet(this, _Money_subunits, "f") < __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
201
179
  }
202
180
  /**
203
181
  * Check if this amount is greater than or equal to another.
@@ -205,7 +183,7 @@ class Money {
205
183
  */
206
184
  greaterThanOrEqual(other) {
207
185
  __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
208
- return __classPrivateFieldGet(this, _Money_value, "f") >= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
186
+ return __classPrivateFieldGet(this, _Money_subunits, "f") >= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
209
187
  }
210
188
  /**
211
189
  * Check if this amount is less than or equal to another.
@@ -213,25 +191,25 @@ class Money {
213
191
  */
214
192
  lessThanOrEqual(other) {
215
193
  __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
216
- return __classPrivateFieldGet(this, _Money_value, "f") <= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
194
+ return __classPrivateFieldGet(this, _Money_subunits, "f") <= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
217
195
  }
218
196
  /**
219
197
  * Check if this amount is zero.
220
198
  */
221
199
  isZero() {
222
- return __classPrivateFieldGet(this, _Money_value, "f") === 0n;
200
+ return __classPrivateFieldGet(this, _Money_subunits, "f") === 0n;
223
201
  }
224
202
  /**
225
203
  * Check if this amount is positive (greater than zero).
226
204
  */
227
205
  isPositive() {
228
- return __classPrivateFieldGet(this, _Money_value, "f") > 0n;
206
+ return __classPrivateFieldGet(this, _Money_subunits, "f") > 0n;
229
207
  }
230
208
  /**
231
209
  * Check if this amount is negative (less than zero).
232
210
  */
233
211
  isNegative() {
234
- return __classPrivateFieldGet(this, _Money_value, "f") < 0n;
212
+ return __classPrivateFieldGet(this, _Money_subunits, "f") < 0n;
235
213
  }
236
214
  // ============ Serialization ============
237
215
  /**
@@ -261,9 +239,7 @@ class Money {
261
239
  * Useful for database storage (Stripe-style integer storage).
262
240
  */
263
241
  toSubunits() {
264
- const decimals = __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits;
265
- const divisor = 10n ** BigInt(INTERNAL_PRECISION - decimals);
266
- return __classPrivateFieldGet(this, _Money_value, "f") / divisor;
242
+ return __classPrivateFieldGet(this, _Money_subunits, "f");
267
243
  }
268
244
  // ============ Static Factory Methods ============
269
245
  /**
@@ -277,14 +253,12 @@ class Money {
277
253
  * Useful for loading from database (Stripe-style integer storage).
278
254
  */
279
255
  static fromSubunits(subunits, currency) {
280
- const currencyDef = (0, currency_js_1.getCurrency)(currency);
256
+ const currencyDef = getCurrency(currency);
281
257
  if (!currencyDef) {
282
- throw new errors_js_1.CurrencyUnknownError(currency);
258
+ throw new CurrencyUnknownError(currency);
283
259
  }
284
260
  const bigintSubunits = typeof subunits === 'number' ? BigInt(subunits) : subunits;
285
- const multiplier = 10n ** BigInt(INTERNAL_PRECISION - currencyDef.decimalDigits);
286
- const internalValue = bigintSubunits * multiplier;
287
- return __classPrivateFieldGet(_a, _a, "m", _Money_createFromInternal).call(_a, internalValue, currency, currencyDef);
261
+ return __classPrivateFieldGet(_a, _a, "m", _Money_createFromSubunits).call(_a, bigintSubunits, currency, currencyDef);
288
262
  }
289
263
  /**
290
264
  * Compare two Money objects (for use with Array.sort).
@@ -292,7 +266,7 @@ class Money {
292
266
  */
293
267
  static compare(a, b) {
294
268
  if (a.currency !== b.currency) {
295
- throw new errors_js_1.CurrencyMismatchError(a.currency, b.currency);
269
+ throw new CurrencyMismatchError(a.currency, b.currency);
296
270
  }
297
271
  const aVal = __classPrivateFieldGet(a, _Money_instances, "m", _Money_getInternalValue).call(a);
298
272
  const bVal = __classPrivateFieldGet(b, _Money_instances, "m", _Money_getInternalValue).call(b);
@@ -309,30 +283,44 @@ class Money {
309
283
  return new _a(currency, '0');
310
284
  }
311
285
  }
312
- exports.Money = Money;
313
- _a = Money, _Money_value = new WeakMap(), _Money_currencyDef = new WeakMap(), _Money_instances = new WeakSet(), _Money_parseAmount = function _Money_parseAmount(amount) {
314
- // Convert to string for consistent parsing
286
+ _a = Money, _Money_subunits = new WeakMap(), _Money_currencyDef = new WeakMap(), _Money_instances = new WeakSet(), _Money_parseAmount = function _Money_parseAmount(amount) {
315
287
  const str = typeof amount === 'number' ? String(amount) : amount;
316
- // Validate format: optional minus, digits, optional decimal part
317
288
  const match = str.match(/^(-)?(\d+)(?:\.(\d+))?$/);
318
289
  if (!match) {
319
- throw new errors_js_1.AmountError(amount);
290
+ throw new AmountError(amount);
320
291
  }
321
292
  const [, sign, whole, frac = ''] = match;
322
- // Check decimal places don't exceed currency limit
323
293
  if (frac.length > __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits) {
324
- throw new errors_js_1.SubunitError(this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits);
294
+ throw new SubunitError(this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits);
325
295
  }
326
- // Pad fraction to internal precision and combine
327
- const paddedFrac = frac.padEnd(INTERNAL_PRECISION, '0');
296
+ const paddedFrac = frac.padEnd(__classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits, '0');
328
297
  const combined = BigInt(whole + paddedFrac);
329
298
  return sign === '-' ? -combined : combined;
330
299
  }, _Money_assertSameCurrency = function _Money_assertSameCurrency(other) {
331
300
  if (this.currency !== other.currency) {
332
- throw new errors_js_1.CurrencyMismatchError(this.currency, other.currency);
301
+ throw new CurrencyMismatchError(this.currency, other.currency);
333
302
  }
334
303
  }, _Money_getInternalValue = function _Money_getInternalValue() {
335
- return __classPrivateFieldGet(this, _Money_value, "f");
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
+ }
336
324
  }, _Money_roundedDivide = function _Money_roundedDivide(numerator, denominator) {
337
325
  if (denominator === 1n)
338
326
  return numerator;
@@ -353,22 +341,12 @@ _a = Money, _Money_value = new WeakMap(), _Money_currencyDef = new WeakMap(), _M
353
341
  return numerator < 0n ? quotient - 1n : quotient + 1n;
354
342
  }
355
343
  return quotient;
356
- }, _Money_createFromInternal = function _Money_createFromInternal(value, currency, currencyDef) {
357
- const instance = Object.create(_a.prototype);
358
- // Use Object.defineProperties for proper initialization
359
- Object.defineProperties(instance, {
360
- currency: { value: currency, enumerable: true, writable: false },
361
- });
362
- // Access private fields via the class mechanism
363
- // This is a workaround since we can't directly set #private fields on Object.create instances
364
- // Instead, we'll use a different approach: call a private static method
365
- return new _a(currency, __classPrivateFieldGet(_a, _a, "m", _Money_formatInternalValue).call(_a, value, currencyDef));
366
- }, _Money_formatInternalValue = function _Money_formatInternalValue(value, currencyDef) {
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) {
367
347
  const decimals = currencyDef.decimalDigits;
368
- const divisor = 10n ** BigInt(INTERNAL_PRECISION - decimals);
369
- const adjusted = __classPrivateFieldGet(_a, _a, "m", _Money_roundedDivide).call(_a, value, divisor);
370
- const isNegative = adjusted < 0n;
371
- const abs = isNegative ? -adjusted : adjusted;
348
+ const abs = subunits < 0n ? -subunits : subunits;
349
+ const isNegative = subunits < 0n;
372
350
  if (decimals === 0) {
373
351
  return `${isNegative ? '-' : ''}${abs}`;
374
352
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "subunit-money",
3
- "version": "2.1.1",
3
+ "version": "3.1.0",
4
4
  "description": "A type-safe value object for monetary amounts with currency conversion support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,11 +18,12 @@
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "build": "tsc",
21
+ "build": "tsc && tsc --module commonjs --outDir dist/cjs",
22
22
  "test": "node --import tsx --test test/*.test.ts",
23
23
  "lint": "tsc --noEmit",
24
24
  "check-clean": "git diff --quiet && git diff --cached --quiet",
25
25
  "preversion": "npm run check-clean && npm test",
26
+ "version": "npm run build && git add dist",
26
27
  "postversion": "git push && git push --tags",
27
28
  "prepublishOnly": "npm run check-clean && npm run build",
28
29
  "release:patch": "npm version patch && npm publish",