subunit-money 2.0.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.
@@ -0,0 +1,148 @@
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
+ /**
11
+ * Serialized form of a Money object, safe for JSON.
12
+ */
13
+ export interface MoneyObject<C extends string = string> {
14
+ currency: C;
15
+ amount: string;
16
+ }
17
+ /**
18
+ * Money class - represents a monetary amount in a specific currency.
19
+ *
20
+ * @typeParam C - The currency code type (enables compile-time currency checking)
21
+ *
22
+ * @example
23
+ * const price = new Money('USD', '19.99')
24
+ * const tax = price.multiply(0.08)
25
+ * const total = price.add(tax)
26
+ * console.log(total.amount) // "21.59"
27
+ */
28
+ export declare class Money<C extends string = string> {
29
+ #private;
30
+ readonly currency: C;
31
+ /**
32
+ * Create a new Money instance.
33
+ *
34
+ * @param currency - ISO 4217 currency code (must be registered)
35
+ * @param amount - The amount as a number or string
36
+ * @throws {CurrencyUnknownError} If the currency is not registered
37
+ * @throws {AmountError} If the amount is not a valid number
38
+ * @throws {SubunitError} If the amount has more decimals than the currency allows
39
+ */
40
+ constructor(currency: C, amount: number | string);
41
+ /**
42
+ * The amount as a formatted string with correct decimal places.
43
+ * @example
44
+ * new Money('USD', 19.9).amount // "19.90"
45
+ * new Money('JPY', 1000).amount // "1000"
46
+ */
47
+ get amount(): string;
48
+ /**
49
+ * Add another Money amount.
50
+ * @throws {CurrencyMismatchError} If currencies don't match
51
+ */
52
+ add(other: Money<C>): Money<C>;
53
+ /**
54
+ * Subtract another Money amount.
55
+ * @throws {CurrencyMismatchError} If currencies don't match
56
+ */
57
+ subtract(other: Money<C>): Money<C>;
58
+ /**
59
+ * Multiply by a factor.
60
+ * Result is rounded to the currency's decimal places using banker's rounding.
61
+ */
62
+ multiply(factor: number): Money<C>;
63
+ /**
64
+ * Allocate this amount proportionally.
65
+ * Handles remainder distribution to avoid losing pennies.
66
+ *
67
+ * @param proportions - Array of proportions (e.g., [1, 1, 1] for three-way split)
68
+ * @returns Array of Money objects that sum to the original amount
69
+ *
70
+ * @example
71
+ * new Money('USD', '100').allocate([1, 1, 1])
72
+ * // Returns: [Money('33.34'), Money('33.33'), Money('33.33')]
73
+ */
74
+ allocate(proportions: number[]): Money<C>[];
75
+ /**
76
+ * Check if this amount equals another.
77
+ * @throws {CurrencyMismatchError} If currencies don't match
78
+ */
79
+ equalTo(other: Money<C>): boolean;
80
+ /**
81
+ * Check if this amount is greater than another.
82
+ * @throws {CurrencyMismatchError} If currencies don't match
83
+ */
84
+ greaterThan(other: Money<C>): boolean;
85
+ /**
86
+ * Check if this amount is less than another.
87
+ * @throws {CurrencyMismatchError} If currencies don't match
88
+ */
89
+ lessThan(other: Money<C>): boolean;
90
+ /**
91
+ * Check if this amount is greater than or equal to another.
92
+ * @throws {CurrencyMismatchError} If currencies don't match
93
+ */
94
+ greaterThanOrEqual(other: Money<C>): boolean;
95
+ /**
96
+ * Check if this amount is less than or equal to another.
97
+ * @throws {CurrencyMismatchError} If currencies don't match
98
+ */
99
+ lessThanOrEqual(other: Money<C>): boolean;
100
+ /**
101
+ * Check if this amount is zero.
102
+ */
103
+ isZero(): boolean;
104
+ /**
105
+ * Check if this amount is positive (greater than zero).
106
+ */
107
+ isPositive(): boolean;
108
+ /**
109
+ * Check if this amount is negative (less than zero).
110
+ */
111
+ isNegative(): boolean;
112
+ /**
113
+ * Convert to a plain object (safe for JSON).
114
+ */
115
+ toJSON(): MoneyObject<C>;
116
+ /**
117
+ * Convert to string representation.
118
+ */
119
+ toString(): string;
120
+ /**
121
+ * Get the amount as a number (may lose precision for large values).
122
+ * Use with caution - prefer string-based operations.
123
+ */
124
+ toNumber(): number;
125
+ /**
126
+ * Get the amount in subunits (e.g., cents for USD).
127
+ * Useful for database storage (Stripe-style integer storage).
128
+ */
129
+ toSubunits(): bigint;
130
+ /**
131
+ * Create a Money instance from a plain object.
132
+ */
133
+ static fromObject<C extends string>(obj: MoneyObject<C>): Money<C>;
134
+ /**
135
+ * Create a Money instance from subunits (e.g., cents).
136
+ * Useful for loading from database (Stripe-style integer storage).
137
+ */
138
+ static fromSubunits<C extends string>(subunits: bigint | number, currency: C): Money<C>;
139
+ /**
140
+ * Compare two Money objects (for use with Array.sort).
141
+ * @throws {CurrencyMismatchError} If currencies don't match
142
+ */
143
+ static compare<C extends string>(a: Money<C>, b: Money<C>): -1 | 0 | 1;
144
+ /**
145
+ * Create a zero amount in the specified currency.
146
+ */
147
+ static zero<C extends string>(currency: C): Money<C>;
148
+ }
package/dist/money.js ADDED
@@ -0,0 +1,360 @@
1
+ "use strict";
2
+ /**
3
+ * Money - An immutable value object for monetary amounts.
4
+ *
5
+ * Design principles:
6
+ * - Immutable: all operations return new instances
7
+ * - Type-safe: currency mismatches are caught at compile time (when possible) and runtime
8
+ * - Precise: uses BigInt internally to avoid floating-point errors
9
+ * - String-based API: amounts are strings to preserve precision in JSON/DB round-trips
10
+ */
11
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
12
+ if (kind === "m") throw new TypeError("Private method is not writable");
13
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
14
+ 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");
15
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
16
+ };
17
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
18
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
19
+ 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
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
21
+ };
22
+ var _Money_instances, _a, _Money_value, _Money_currencyDef, _Money_parseAmount, _Money_fromInternal, _Money_assertSameCurrency, _Money_getInternalValue, _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);
30
+ /**
31
+ * Money class - represents a monetary amount in a specific currency.
32
+ *
33
+ * @typeParam C - The currency code type (enables compile-time currency checking)
34
+ *
35
+ * @example
36
+ * const price = new Money('USD', '19.99')
37
+ * const tax = price.multiply(0.08)
38
+ * const total = price.add(tax)
39
+ * console.log(total.amount) // "21.59"
40
+ */
41
+ class Money {
42
+ /**
43
+ * Create a new Money instance.
44
+ *
45
+ * @param currency - ISO 4217 currency code (must be registered)
46
+ * @param amount - The amount as a number or string
47
+ * @throws {CurrencyUnknownError} If the currency is not registered
48
+ * @throws {AmountError} If the amount is not a valid number
49
+ * @throws {SubunitError} If the amount has more decimals than the currency allows
50
+ */
51
+ constructor(currency, amount) {
52
+ _Money_instances.add(this);
53
+ // Private BigInt storage - not exposed to prevent precision leaks
54
+ _Money_value.set(this, void 0);
55
+ _Money_currencyDef.set(this, void 0);
56
+ const currencyDef = (0, currency_js_1.getCurrency)(currency);
57
+ if (!currencyDef) {
58
+ throw new errors_js_1.CurrencyUnknownError(currency);
59
+ }
60
+ this.currency = currency;
61
+ __classPrivateFieldSet(this, _Money_currencyDef, currencyDef, "f");
62
+ __classPrivateFieldSet(this, _Money_value, __classPrivateFieldGet(this, _Money_instances, "m", _Money_parseAmount).call(this, amount), "f");
63
+ }
64
+ /**
65
+ * The amount as a formatted string with correct decimal places.
66
+ * @example
67
+ * new Money('USD', 19.9).amount // "19.90"
68
+ * new Money('JPY', 1000).amount // "1000"
69
+ */
70
+ get amount() {
71
+ 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;
76
+ if (decimals === 0) {
77
+ return `${isNegative ? '-' : ''}${abs}`;
78
+ }
79
+ const multiplier = 10n ** BigInt(decimals);
80
+ const wholePart = abs / multiplier;
81
+ const fracPart = abs % multiplier;
82
+ const sign = isNegative ? '-' : '';
83
+ return `${sign}${wholePart}.${fracPart.toString().padStart(decimals, '0')}`;
84
+ }
85
+ // ============ Arithmetic Operations ============
86
+ /**
87
+ * Add another Money amount.
88
+ * @throws {CurrencyMismatchError} If currencies don't match
89
+ */
90
+ add(other) {
91
+ __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"));
94
+ }
95
+ /**
96
+ * Subtract another Money amount.
97
+ * @throws {CurrencyMismatchError} If currencies don't match
98
+ */
99
+ subtract(other) {
100
+ __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"));
103
+ }
104
+ /**
105
+ * Multiply by a factor.
106
+ * Result is rounded to the currency's decimal places using banker's rounding.
107
+ */
108
+ multiply(factor) {
109
+ if (typeof factor !== 'number' || !Number.isFinite(factor)) {
110
+ throw new TypeError(`Factor must be a finite number, got: ${factor}`);
111
+ }
112
+ // Convert factor to BigInt-compatible form
113
+ const factorStr = factor.toFixed(INTERNAL_PRECISION);
114
+ const factorBigInt = BigInt(factorStr.replace('.', ''));
115
+ const result = (__classPrivateFieldGet(this, _Money_value, "f") * factorBigInt) / PRECISION_MULTIPLIER;
116
+ return __classPrivateFieldGet(_a, _a, "m", _Money_createFromInternal).call(_a, result, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
117
+ }
118
+ /**
119
+ * Allocate this amount proportionally.
120
+ * Handles remainder distribution to avoid losing pennies.
121
+ *
122
+ * @param proportions - Array of proportions (e.g., [1, 1, 1] for three-way split)
123
+ * @returns Array of Money objects that sum to the original amount
124
+ *
125
+ * @example
126
+ * new Money('USD', '100').allocate([1, 1, 1])
127
+ * // Returns: [Money('33.34'), Money('33.33'), Money('33.33')]
128
+ */
129
+ allocate(proportions) {
130
+ if (!Array.isArray(proportions) || proportions.length === 0) {
131
+ throw new TypeError('Proportions must be a non-empty array');
132
+ }
133
+ const total = proportions.reduce((sum, p) => sum + p, 0);
134
+ if (total <= 0) {
135
+ throw new TypeError('Sum of proportions must be positive');
136
+ }
137
+ // Work in currency subunits to avoid precision loss
138
+ const decimals = __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits;
139
+ const subunitMultiplier = 10n ** BigInt(INTERNAL_PRECISION - decimals);
140
+ const totalSubunits = __classPrivateFieldGet(this, _Money_value, "f") / subunitMultiplier;
141
+ // Calculate base allocations
142
+ const allocations = proportions.map((p) => {
143
+ return (totalSubunits * BigInt(Math.round(p * 1000000))) / BigInt(Math.round(total * 1000000));
144
+ });
145
+ // Distribute remainder
146
+ let remainder = totalSubunits - allocations.reduce((sum, a) => sum + a, 0n);
147
+ let i = 0;
148
+ while (remainder > 0n) {
149
+ allocations[i % allocations.length] += 1n;
150
+ remainder -= 1n;
151
+ i++;
152
+ }
153
+ while (remainder < 0n) {
154
+ allocations[i % allocations.length] -= 1n;
155
+ remainder += 1n;
156
+ i++;
157
+ }
158
+ // Convert back to Money objects
159
+ return allocations.map((subunits) => {
160
+ const internalValue = subunits * subunitMultiplier;
161
+ return __classPrivateFieldGet(_a, _a, "m", _Money_createFromInternal).call(_a, internalValue, this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f"));
162
+ });
163
+ }
164
+ // ============ Comparison Operations ============
165
+ /**
166
+ * Check if this amount equals another.
167
+ * @throws {CurrencyMismatchError} If currencies don't match
168
+ */
169
+ equalTo(other) {
170
+ __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
171
+ return __classPrivateFieldGet(this, _Money_value, "f") === __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
172
+ }
173
+ /**
174
+ * Check if this amount is greater than another.
175
+ * @throws {CurrencyMismatchError} If currencies don't match
176
+ */
177
+ greaterThan(other) {
178
+ __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
179
+ return __classPrivateFieldGet(this, _Money_value, "f") > __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
180
+ }
181
+ /**
182
+ * Check if this amount is less than another.
183
+ * @throws {CurrencyMismatchError} If currencies don't match
184
+ */
185
+ lessThan(other) {
186
+ __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
187
+ return __classPrivateFieldGet(this, _Money_value, "f") < __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
188
+ }
189
+ /**
190
+ * Check if this amount is greater than or equal to another.
191
+ * @throws {CurrencyMismatchError} If currencies don't match
192
+ */
193
+ greaterThanOrEqual(other) {
194
+ __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
195
+ return __classPrivateFieldGet(this, _Money_value, "f") >= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
196
+ }
197
+ /**
198
+ * Check if this amount is less than or equal to another.
199
+ * @throws {CurrencyMismatchError} If currencies don't match
200
+ */
201
+ lessThanOrEqual(other) {
202
+ __classPrivateFieldGet(this, _Money_instances, "m", _Money_assertSameCurrency).call(this, other);
203
+ return __classPrivateFieldGet(this, _Money_value, "f") <= __classPrivateFieldGet(other, _Money_instances, "m", _Money_getInternalValue).call(other);
204
+ }
205
+ /**
206
+ * Check if this amount is zero.
207
+ */
208
+ isZero() {
209
+ return __classPrivateFieldGet(this, _Money_value, "f") === 0n;
210
+ }
211
+ /**
212
+ * Check if this amount is positive (greater than zero).
213
+ */
214
+ isPositive() {
215
+ return __classPrivateFieldGet(this, _Money_value, "f") > 0n;
216
+ }
217
+ /**
218
+ * Check if this amount is negative (less than zero).
219
+ */
220
+ isNegative() {
221
+ return __classPrivateFieldGet(this, _Money_value, "f") < 0n;
222
+ }
223
+ // ============ Serialization ============
224
+ /**
225
+ * Convert to a plain object (safe for JSON).
226
+ */
227
+ toJSON() {
228
+ return {
229
+ currency: this.currency,
230
+ amount: this.amount,
231
+ };
232
+ }
233
+ /**
234
+ * Convert to string representation.
235
+ */
236
+ toString() {
237
+ return `${this.amount} ${this.currency}`;
238
+ }
239
+ /**
240
+ * Get the amount as a number (may lose precision for large values).
241
+ * Use with caution - prefer string-based operations.
242
+ */
243
+ toNumber() {
244
+ return Number(this.amount);
245
+ }
246
+ /**
247
+ * Get the amount in subunits (e.g., cents for USD).
248
+ * Useful for database storage (Stripe-style integer storage).
249
+ */
250
+ toSubunits() {
251
+ const decimals = __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits;
252
+ const divisor = 10n ** BigInt(INTERNAL_PRECISION - decimals);
253
+ return __classPrivateFieldGet(this, _Money_value, "f") / divisor;
254
+ }
255
+ // ============ Static Factory Methods ============
256
+ /**
257
+ * Create a Money instance from a plain object.
258
+ */
259
+ static fromObject(obj) {
260
+ return new _a(obj.currency, obj.amount);
261
+ }
262
+ /**
263
+ * Create a Money instance from subunits (e.g., cents).
264
+ * Useful for loading from database (Stripe-style integer storage).
265
+ */
266
+ static fromSubunits(subunits, currency) {
267
+ const currencyDef = (0, currency_js_1.getCurrency)(currency);
268
+ if (!currencyDef) {
269
+ throw new errors_js_1.CurrencyUnknownError(currency);
270
+ }
271
+ const bigintSubunits = typeof subunits === 'number' ? BigInt(subunits) : subunits;
272
+ const multiplier = 10n ** BigInt(INTERNAL_PRECISION - currencyDef.decimalDigits);
273
+ const internalValue = bigintSubunits * multiplier;
274
+ return __classPrivateFieldGet(_a, _a, "m", _Money_createFromInternal).call(_a, internalValue, currency, currencyDef);
275
+ }
276
+ /**
277
+ * Compare two Money objects (for use with Array.sort).
278
+ * @throws {CurrencyMismatchError} If currencies don't match
279
+ */
280
+ static compare(a, b) {
281
+ if (a.currency !== b.currency) {
282
+ throw new errors_js_1.CurrencyMismatchError(a.currency, b.currency);
283
+ }
284
+ const aVal = __classPrivateFieldGet(a, _Money_instances, "m", _Money_getInternalValue).call(a);
285
+ const bVal = __classPrivateFieldGet(b, _Money_instances, "m", _Money_getInternalValue).call(b);
286
+ if (aVal < bVal)
287
+ return -1;
288
+ if (aVal > bVal)
289
+ return 1;
290
+ return 0;
291
+ }
292
+ /**
293
+ * Create a zero amount in the specified currency.
294
+ */
295
+ static zero(currency) {
296
+ return new _a(currency, '0');
297
+ }
298
+ }
299
+ exports.Money = Money;
300
+ _a = Money, _Money_value = new WeakMap(), _Money_currencyDef = new WeakMap(), _Money_instances = new WeakSet(), _Money_parseAmount = function _Money_parseAmount(amount) {
301
+ // Convert to string for consistent parsing
302
+ const str = typeof amount === 'number' ? String(amount) : amount;
303
+ // Validate format: optional minus, digits, optional decimal part
304
+ const match = str.match(/^(-)?(\d+)(?:\.(\d+))?$/);
305
+ if (!match) {
306
+ throw new errors_js_1.AmountError(amount);
307
+ }
308
+ const [, sign, whole, frac = ''] = match;
309
+ // Check decimal places don't exceed currency limit
310
+ if (frac.length > __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits) {
311
+ throw new errors_js_1.SubunitError(this.currency, __classPrivateFieldGet(this, _Money_currencyDef, "f").decimalDigits);
312
+ }
313
+ // Pad fraction to internal precision and combine
314
+ const paddedFrac = frac.padEnd(INTERNAL_PRECISION, '0');
315
+ const combined = BigInt(whole + paddedFrac);
316
+ return sign === '-' ? -combined : combined;
317
+ }, _Money_fromInternal = function _Money_fromInternal(value, currency) {
318
+ const currencyDef = (0, currency_js_1.getCurrency)(currency);
319
+ if (!currencyDef) {
320
+ throw new errors_js_1.CurrencyUnknownError(currency);
321
+ }
322
+ // Create instance without parsing
323
+ const instance = Object.create(_a.prototype);
324
+ Object.defineProperty(instance, 'currency', { value: currency, enumerable: true });
325
+ Object.defineProperty(instance, '#value', { value });
326
+ Object.defineProperty(instance, '#currencyDef', { value: currencyDef });
327
+ instance['#value'] = value;
328
+ instance['#currencyDef'] = currencyDef;
329
+ return instance;
330
+ }, _Money_assertSameCurrency = function _Money_assertSameCurrency(other) {
331
+ if (this.currency !== other.currency) {
332
+ throw new errors_js_1.CurrencyMismatchError(this.currency, other.currency);
333
+ }
334
+ }, _Money_getInternalValue = function _Money_getInternalValue() {
335
+ return __classPrivateFieldGet(this, _Money_value, "f");
336
+ }, _Money_createFromInternal = function _Money_createFromInternal(value, currency, currencyDef) {
337
+ const instance = Object.create(_a.prototype);
338
+ // Use Object.defineProperties for proper initialization
339
+ Object.defineProperties(instance, {
340
+ currency: { value: currency, enumerable: true, writable: false },
341
+ });
342
+ // Access private fields via the class mechanism
343
+ // This is a workaround since we can't directly set #private fields on Object.create instances
344
+ // Instead, we'll use a different approach: call a private static method
345
+ return new _a(currency, __classPrivateFieldGet(_a, _a, "m", _Money_formatInternalValue).call(_a, value, currencyDef));
346
+ }, _Money_formatInternalValue = function _Money_formatInternalValue(value, currencyDef) {
347
+ const decimals = currencyDef.decimalDigits;
348
+ const divisor = 10n ** BigInt(INTERNAL_PRECISION - decimals);
349
+ const adjusted = value / divisor;
350
+ const isNegative = adjusted < 0n;
351
+ const abs = isNegative ? -adjusted : adjusted;
352
+ if (decimals === 0) {
353
+ return `${isNegative ? '-' : ''}${abs}`;
354
+ }
355
+ const multiplier = 10n ** BigInt(decimals);
356
+ const wholePart = abs / multiplier;
357
+ const fracPart = abs % multiplier;
358
+ const sign = isNegative ? '-' : '';
359
+ return `${sign}${wholePart}.${fracPart.toString().padStart(decimals, '0')}`;
360
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "subunit-money",
3
+ "version": "2.0.0",
4
+ "description": "A type-safe value object for monetary amounts with currency conversion support",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "currencymap.json"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "test": "node --import tsx --test test/*.test.ts",
22
+ "lint": "tsc --noEmit",
23
+ "tag": "git tag v$(jq -r '.version' package.json)",
24
+ "prepare-release": "npm run build && git add dist && git commit -m \"Build $(jq -r '.version' package.json)\" && npm run tag",
25
+ "preversion": "npm run test && npm run lint",
26
+ "version": "npm run build && git add -A dist",
27
+ "postversion": "git push && git push --tags",
28
+ "prepublishOnly": "npm run test && npm run build"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/cbrunnkvist/subunit-money.git"
33
+ },
34
+ "keywords": [
35
+ "money",
36
+ "currency",
37
+ "value-object",
38
+ "typescript",
39
+ "bigint",
40
+ "financial",
41
+ "exchange-rate",
42
+ "subunit",
43
+ "precision"
44
+ ],
45
+ "author": "Conny Brunnkvist <cbrunnkvist@gmail.com>",
46
+ "license": "MIT",
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^20.0.0",
52
+ "tsx": "^4.0.0",
53
+ "typescript": "^5.0.0"
54
+ }
55
+ }