@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 ADDED
@@ -0,0 +1,689 @@
1
+ # @f-o-t/money
2
+
3
+ Type-safe money handling library with BigInt precision and ISO 4217 currency support.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@f-o-t/money.svg)](https://www.npmjs.com/package/@f-o-t/money)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - **Precision-First**: Uses BigInt for exact arithmetic, eliminating floating-point errors
11
+ - **Type Safety**: Full TypeScript support with Zod schema validation
12
+ - **ISO 4217**: Built-in support for all standard currencies with correct decimal places
13
+ - **Immutable**: All operations return new instances, preventing accidental mutations
14
+ - **Framework Agnostic**: Works with any JavaScript/TypeScript project
15
+ - **Banker's Rounding**: IEEE 754 compliant rounding for fair financial calculations
16
+ - **Rich API**: Comprehensive operations for arithmetic, comparison, allocation, and aggregation
17
+ - **Locale Support**: Format and parse amounts in any locale
18
+ - **Extensible**: Register custom currencies for specialized use cases
19
+ - **Condition Evaluator**: Built-in operators for rule-based systems
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # npm
25
+ npm install @f-o-t/money
26
+
27
+ # bun
28
+ bun add @f-o-t/money
29
+
30
+ # yarn
31
+ yarn add @f-o-t/money
32
+
33
+ # pnpm
34
+ pnpm add @f-o-t/money
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```typescript
40
+ import { of, add, multiply, format } from "@f-o-t/money";
41
+
42
+ // Create money values
43
+ const price = of("19.99", "USD");
44
+ const quantity = 3;
45
+
46
+ // Calculate total
47
+ const subtotal = multiply(price, quantity); // $59.97
48
+ const tax = multiply(subtotal, 0.08); // $4.80
49
+ const total = add(subtotal, tax); // $64.77
50
+
51
+ // Format for display
52
+ console.log(format(total, "en-US")); // "$64.77"
53
+ ```
54
+
55
+ ## Core Concepts
56
+
57
+ ### Money Object
58
+
59
+ The `Money` type represents a monetary value with three properties:
60
+
61
+ ```typescript
62
+ type Money = {
63
+ amount: bigint; // Amount in minor units (e.g., cents)
64
+ currency: string; // ISO 4217 currency code
65
+ scale: number; // Number of decimal places
66
+ };
67
+ ```
68
+
69
+ **Important**: Money objects are immutable. All operations return new instances.
70
+
71
+ ### Precision
72
+
73
+ Uses BigInt internally to avoid floating-point errors:
74
+
75
+ ```typescript
76
+ import { of, add, toDecimal } from "@f-o-t/money";
77
+
78
+ const a = of("0.1", "USD");
79
+ const b = of("0.2", "USD");
80
+ const result = add(a, b);
81
+
82
+ console.log(toDecimal(result)); // "0.30" ✓ (not 0.30000000000000004)
83
+ ```
84
+
85
+ ### Currency Support
86
+
87
+ All ISO 4217 currencies are supported with correct decimal places:
88
+
89
+ ```typescript
90
+ import { of } from "@f-o-t/money";
91
+
92
+ const usd = of("10.50", "USD"); // 2 decimal places
93
+ const jpy = of("1050", "JPY"); // 0 decimal places
94
+ const kwd = of("10.500", "KWD"); // 3 decimal places
95
+ ```
96
+
97
+ ## API Reference
98
+
99
+ ### Factory Functions
100
+
101
+ Create money values from various inputs:
102
+
103
+ ```typescript
104
+ import { of, ofRounded, fromMinorUnits, fromMajorUnits, zero } from "@f-o-t/money";
105
+
106
+ // From major units (dollars, euros, etc.)
107
+ const money1 = of("123.45", "USD");
108
+ const money2 = of(123.45, "USD"); // Also accepts numbers
109
+ const money3 = fromMajorUnits("123.45", "USD"); // Alias for of()
110
+
111
+ // From minor units (cents, pence, etc.)
112
+ const money4 = fromMinorUnits(12345, "USD"); // $123.45
113
+ const money5 = fromMinorUnits(12345n, "USD"); // Also accepts BigInt
114
+
115
+ // Zero value
116
+ const money6 = zero("USD"); // $0.00
117
+
118
+ // Handle excess decimal places
119
+ const truncated = of("10.999", "USD"); // $10.99 (truncated)
120
+ const rounded = of("10.999", "USD", "round"); // $11.00 (rounded)
121
+ const rounded2 = ofRounded("10.999", "USD"); // $11.00 (convenience function)
122
+ ```
123
+
124
+ ### Arithmetic Operations
125
+
126
+ ```typescript
127
+ import { add, subtract, multiply, divide, percentage, negate, absolute } from "@f-o-t/money";
128
+
129
+ const a = of("100.00", "USD");
130
+ const b = of("25.50", "USD");
131
+
132
+ add(a, b); // $125.50
133
+ subtract(a, b); // $74.50
134
+ multiply(a, 1.5); // $150.00 - accepts numbers
135
+ multiply(a, "1.5"); // $150.00 - strings for precision
136
+ divide(a, 4); // $25.00
137
+ percentage(a, 15); // $15.00 - 15% of $100
138
+ negate(a); // -$100.00
139
+ absolute(of("-50", "USD")); // $50.00
140
+ ```
141
+
142
+ **Note**: Division uses banker's rounding (round half to even) for fair distribution.
143
+
144
+ ### Comparison Operations
145
+
146
+ ```typescript
147
+ import {
148
+ equals,
149
+ greaterThan,
150
+ greaterThanOrEqual,
151
+ lessThan,
152
+ lessThanOrEqual,
153
+ isPositive,
154
+ isNegative,
155
+ isZero,
156
+ compare
157
+ } from "@f-o-t/money";
158
+
159
+ const a = of("100.00", "USD");
160
+ const b = of("50.00", "USD");
161
+
162
+ equals(a, b); // false
163
+ greaterThan(a, b); // true
164
+ greaterThanOrEqual(a, a); // true
165
+ lessThan(b, a); // true
166
+ lessThanOrEqual(a, a); // true
167
+
168
+ isPositive(a); // true
169
+ isNegative(of("-10", "USD")); // true
170
+ isZero(zero("USD")); // true
171
+
172
+ compare(a, b); // 1 (a > b)
173
+ compare(b, a); // -1 (b < a)
174
+ compare(a, a); // 0 (a === b)
175
+ ```
176
+
177
+ **Note**: All comparison operations throw `CurrencyMismatchError` if currencies differ.
178
+
179
+ ### Allocation
180
+
181
+ Distribute money amounts proportionally with exact precision:
182
+
183
+ ```typescript
184
+ import { allocate, split } from "@f-o-t/money";
185
+
186
+ // Allocate by ratios (e.g., revenue sharing)
187
+ const revenue = of("100.00", "USD");
188
+ const shares = allocate(revenue, [60, 25, 15]);
189
+ // shares[0]: $60.00 (60%)
190
+ // shares[1]: $25.00 (25%)
191
+ // shares[2]: $15.00 (15%)
192
+
193
+ // Handle remainders fairly using largest remainder method
194
+ const amount = of("10.00", "USD");
195
+ const parts = allocate(amount, [1, 1, 1]); // Split in thirds
196
+ // parts[0]: $3.34
197
+ // parts[1]: $3.33
198
+ // parts[2]: $3.33
199
+ // Total: $10.00 ✓
200
+
201
+ // Split evenly into N parts
202
+ const total = of("100.00", "USD");
203
+ const quarters = split(total, 4);
204
+ // Each part: $25.00
205
+ ```
206
+
207
+ ### Aggregation
208
+
209
+ Perform calculations on arrays of money values:
210
+
211
+ ```typescript
212
+ import { sum, sumOrZero, min, max, average, median } from "@f-o-t/money";
213
+
214
+ const amounts = [
215
+ of("10.00", "USD"),
216
+ of("20.00", "USD"),
217
+ of("30.00", "USD")
218
+ ];
219
+
220
+ sum(amounts); // $60.00
221
+ sumOrZero([], "USD"); // $0.00 - safe for empty arrays
222
+ min(amounts); // $10.00
223
+ max(amounts); // $30.00
224
+ average(amounts); // $20.00
225
+ median(amounts); // $20.00
226
+ ```
227
+
228
+ ### Formatting
229
+
230
+ Display money values in human-readable formats:
231
+
232
+ ```typescript
233
+ import { format, formatCompact, formatAmount, toDecimal } from "@f-o-t/money";
234
+
235
+ const money = of("1234.56", "USD");
236
+
237
+ // Standard formatting
238
+ format(money, "en-US"); // "$1,234.56"
239
+ format(money, "pt-BR"); // "US$ 1.234,56"
240
+ format(money, "ja-JP"); // "$1,234.56"
241
+
242
+ // Compact notation (for large amounts)
243
+ const large = of("1234567.89", "USD");
244
+ formatCompact(large, "en-US"); // "$1.2M"
245
+
246
+ // Amount only (no currency symbol)
247
+ formatAmount(money, "en-US"); // "1,234.56"
248
+
249
+ // Hide currency symbol
250
+ format(money, "en-US", { hideSymbol: true }); // "1,234.56"
251
+
252
+ // Plain decimal string
253
+ toDecimal(money); // "1234.56"
254
+ ```
255
+
256
+ ### Parsing
257
+
258
+ Parse formatted strings back into money values:
259
+
260
+ ```typescript
261
+ import { parse } from "@f-o-t/money";
262
+
263
+ // US format
264
+ parse("$1,234.56", "en-US", "USD");
265
+
266
+ // Brazilian format
267
+ parse("R$ 1.234,56", "pt-BR", "BRL");
268
+
269
+ // Negative amounts (supports parentheses or minus)
270
+ parse("($1,234.56)", "en-US", "USD"); // -$1,234.56
271
+ parse("-$1,234.56", "en-US", "USD"); // -$1,234.56
272
+ ```
273
+
274
+ ### Serialization
275
+
276
+ Convert money to and from various formats:
277
+
278
+ ```typescript
279
+ import {
280
+ toJSON,
281
+ fromJSON,
282
+ toDatabase,
283
+ fromDatabase,
284
+ serialize,
285
+ deserialize,
286
+ toMinorUnits,
287
+ toMinorUnitsBigInt,
288
+ toMajorUnits,
289
+ toMajorUnitsString,
290
+ toMinorUnitsString
291
+ } from "@f-o-t/money";
292
+
293
+ const money = of("123.45", "USD");
294
+
295
+ // JSON (for APIs)
296
+ const json = toJSON(money);
297
+ // { amount: "123.45", currency: "USD" }
298
+ fromJSON(json); // Recreates Money object
299
+
300
+ // Database storage (same as JSON)
301
+ const db = toDatabase(money);
302
+ // { amount: "123.45", currency: "USD" }
303
+ fromDatabase(db);
304
+
305
+ // String serialization
306
+ serialize(money); // "123.45 USD"
307
+ deserialize("123.45 USD"); // Recreates Money object
308
+
309
+ // Unit conversions
310
+ toMinorUnits(money); // 12345 (number)
311
+ toMinorUnitsBigInt(money); // 12345n (BigInt)
312
+ toMajorUnitsString(money); // "123.45" (string, precision-safe)
313
+ toMajorUnits(money); // 123.45 (number, deprecated - may lose precision)
314
+ toMinorUnitsString(money); // "12345" (string)
315
+ ```
316
+
317
+ ### Currency Registry
318
+
319
+ Access and manage currency information:
320
+
321
+ ```typescript
322
+ import {
323
+ getCurrency,
324
+ registerCurrency,
325
+ hasCurrency,
326
+ getAllCurrencies,
327
+ clearCustomCurrencies,
328
+ ISO_4217_CURRENCIES
329
+ } from "@f-o-t/money";
330
+
331
+ // Get currency info
332
+ const usd = getCurrency("USD");
333
+ // {
334
+ // code: "USD",
335
+ // numericCode: 840,
336
+ // name: "US Dollar",
337
+ // decimalPlaces: 2,
338
+ // symbol: "$"
339
+ // }
340
+
341
+ // Check if currency exists
342
+ hasCurrency("USD"); // true
343
+ hasCurrency("XXX"); // false
344
+
345
+ // Register custom currency
346
+ registerCurrency({
347
+ code: "BTC",
348
+ numericCode: 0,
349
+ name: "Bitcoin",
350
+ decimalPlaces: 8,
351
+ symbol: "₿"
352
+ });
353
+
354
+ // Get all currencies
355
+ const all = getAllCurrencies();
356
+
357
+ // Clear custom currencies (useful for testing)
358
+ clearCustomCurrencies();
359
+
360
+ // Access raw ISO 4217 data
361
+ console.log(ISO_4217_CURRENCIES.USD);
362
+ ```
363
+
364
+ ### Zod Schemas
365
+
366
+ Validate money data with Zod schemas:
367
+
368
+ ```typescript
369
+ import {
370
+ MoneySchema,
371
+ MoneyInputSchema,
372
+ CurrencyCodeSchema,
373
+ DatabaseMoneySchema,
374
+ AllocationRatiosSchema,
375
+ FormatOptionsSchema
376
+ } from "@f-o-t/money";
377
+
378
+ // Validate JSON input
379
+ const result = MoneySchema.safeParse({
380
+ amount: "123.45",
381
+ currency: "USD"
382
+ });
383
+
384
+ if (result.success) {
385
+ const money = fromJSON(result.data);
386
+ }
387
+
388
+ // User input (accepts strings or numbers)
389
+ MoneyInputSchema.parse({
390
+ amount: 123.45, // number is OK
391
+ currency: "USD"
392
+ });
393
+
394
+ // Currency code validation
395
+ CurrencyCodeSchema.parse("USD"); // ✓
396
+ CurrencyCodeSchema.parse("usd"); // ✗ - must be uppercase
397
+
398
+ // Allocation ratios
399
+ AllocationRatiosSchema.parse([60, 25, 15]); // ✓
400
+ AllocationRatiosSchema.parse([]); // ✗ - empty array
401
+ AllocationRatiosSchema.parse([-1, 1]); // ✗ - negative values
402
+ ```
403
+
404
+ ### Error Handling
405
+
406
+ The library provides specific error types:
407
+
408
+ ```typescript
409
+ import {
410
+ MoneyError,
411
+ CurrencyMismatchError,
412
+ ScaleMismatchError,
413
+ InvalidAmountError,
414
+ DivisionByZeroError,
415
+ UnknownCurrencyError,
416
+ OverflowError
417
+ } from "@f-o-t/money";
418
+
419
+ try {
420
+ add(of("10", "USD"), of("10", "EUR"));
421
+ } catch (error) {
422
+ if (error instanceof CurrencyMismatchError) {
423
+ console.log("Cannot add different currencies");
424
+ }
425
+ }
426
+
427
+ // Scale mismatch (same currency with different decimal places)
428
+ try {
429
+ const a = of("10.00", "USD");
430
+ const b = { amount: 1000n, currency: "USD", scale: 4 }; // Invalid scale
431
+ add(a, b);
432
+ } catch (error) {
433
+ if (error instanceof ScaleMismatchError) {
434
+ console.log("Scale mismatch:", error.scaleA, "vs", error.scaleB);
435
+ }
436
+ }
437
+
438
+ // All error types extend MoneyError
439
+ try {
440
+ divide(of("10", "USD"), 0);
441
+ } catch (error) {
442
+ if (error instanceof MoneyError) {
443
+ console.log("Money operation failed:", error.message);
444
+ }
445
+ }
446
+ ```
447
+
448
+ ## Advanced Usage
449
+
450
+ ### Condition Evaluator Integration
451
+
452
+ Use money operators with the `@f-o-t/condition-evaluator` package for rule-based systems:
453
+
454
+ ```typescript
455
+ import { createEvaluator } from "@f-o-t/condition-evaluator";
456
+ import { moneyOperators } from "@f-o-t/money/operators";
457
+
458
+ const evaluator = createEvaluator({
459
+ operators: moneyOperators
460
+ });
461
+
462
+ // Evaluate money conditions
463
+ const result = evaluator.evaluate(
464
+ {
465
+ type: "custom",
466
+ field: "transactionAmount",
467
+ operator: "money_gt",
468
+ value: { amount: "100.00", currency: "USD" }
469
+ },
470
+ {
471
+ data: {
472
+ transactionAmount: { amount: "150.00", currency: "USD" }
473
+ }
474
+ }
475
+ );
476
+ // result: true
477
+
478
+ // Available operators:
479
+ // - money_eq, money_neq
480
+ // - money_gt, money_gte, money_lt, money_lte
481
+ // - money_between
482
+ // - money_positive, money_negative, money_zero
483
+ ```
484
+
485
+ ### Custom Currencies
486
+
487
+ Register currencies not in ISO 4217:
488
+
489
+ ```typescript
490
+ import { registerCurrency, of } from "@f-o-t/money";
491
+
492
+ // Register cryptocurrency
493
+ registerCurrency({
494
+ code: "BTC",
495
+ numericCode: 0,
496
+ name: "Bitcoin",
497
+ decimalPlaces: 8,
498
+ symbol: "₿",
499
+ subunitName: "satoshi"
500
+ });
501
+
502
+ // Now you can use it
503
+ const bitcoin = of("0.00123456", "BTC");
504
+ ```
505
+
506
+ ### Assertions
507
+
508
+ Use assertions for type narrowing and validation:
509
+
510
+ ```typescript
511
+ import { assertSameCurrency, assertAllSameCurrency } from "@f-o-t/money";
512
+
513
+ function addMany(amounts: Money[]): Money {
514
+ assertAllSameCurrency(amounts);
515
+ // TypeScript now knows all amounts have the same currency
516
+ return sum(amounts);
517
+ }
518
+ ```
519
+
520
+ ### Low-Level Utilities
521
+
522
+ For advanced use cases:
523
+
524
+ ```typescript
525
+ import {
526
+ bankersRound,
527
+ EXTENDED_PRECISION,
528
+ PRECISION_FACTOR,
529
+ createMoney,
530
+ parseDecimalToMinorUnits,
531
+ minorUnitsToDecimal
532
+ } from "@f-o-t/money";
533
+
534
+ // Banker's rounding
535
+ bankersRound(150n, 100n); // 200n (rounds 1.5 to 2, even)
536
+ bankersRound(250n, 100n); // 200n (rounds 2.5 to 2, even)
537
+
538
+ // Extended precision for intermediate calculations
539
+ console.log(EXTENDED_PRECISION); // 1000000n
540
+ console.log(PRECISION_FACTOR); // 100n
541
+
542
+ // Direct money creation (bypasses validation)
543
+ createMoney(12345n, "USD", 2);
544
+
545
+ // Low-level parsing
546
+ parseDecimalToMinorUnits("123.45", 2); // 12345n
547
+ minorUnitsToDecimal(12345n, 2); // "123.45"
548
+ ```
549
+
550
+ ## Best Practices
551
+
552
+ ### 1. Always Use Strings for Precision
553
+
554
+ ```typescript
555
+ // Good - exact representation
556
+ const price = of("19.99", "USD");
557
+ const result = multiply(price, "1.08");
558
+
559
+ // Avoid - floating point errors may occur
560
+ const price = of(19.99, "USD");
561
+ const result = multiply(price, 1.08);
562
+ ```
563
+
564
+ ### 2. Handle Currency Mismatches
565
+
566
+ ```typescript
567
+ import { CurrencyMismatchError } from "@f-o-t/money";
568
+
569
+ function safeAdd(a: Money, b: Money): Money | null {
570
+ try {
571
+ return add(a, b);
572
+ } catch (error) {
573
+ if (error instanceof CurrencyMismatchError) {
574
+ return null; // Or handle conversion
575
+ }
576
+ throw error;
577
+ }
578
+ }
579
+ ```
580
+
581
+ ### 3. Use Allocation for Fair Distribution
582
+
583
+ ```typescript
584
+ // Don't use division for splitting money
585
+ const total = of("10.00", "USD");
586
+ const bad = divide(total, 3); // Loses precision
587
+
588
+ // Use split instead
589
+ const good = split(total, 3); // Ensures total is preserved
590
+ const sum = good.reduce((acc, m) => add(acc, m));
591
+ // sum equals total ✓
592
+ ```
593
+
594
+ ### 4. Store as Database-Friendly Format
595
+
596
+ ```typescript
597
+ import { toDatabase, fromDatabase } from "@f-o-t/money";
598
+
599
+ // In your database model
600
+ interface Product {
601
+ id: string;
602
+ name: string;
603
+ price: { amount: string; currency: string };
604
+ }
605
+
606
+ // When saving
607
+ const product: Product = {
608
+ id: "123",
609
+ name: "Widget",
610
+ price: toDatabase(of("19.99", "USD"))
611
+ };
612
+
613
+ // When loading
614
+ const price = fromDatabase(product.price);
615
+ ```
616
+
617
+ ### 5. Validate API Input with Zod
618
+
619
+ ```typescript
620
+ import { MoneyInputSchema, fromJSON } from "@f-o-t/money";
621
+ import { z } from "zod";
622
+
623
+ const CreateProductSchema = z.object({
624
+ name: z.string(),
625
+ price: MoneyInputSchema
626
+ });
627
+
628
+ function createProduct(input: unknown) {
629
+ const validated = CreateProductSchema.parse(input);
630
+ const price = fromJSON({
631
+ amount: String(validated.price.amount),
632
+ currency: validated.price.currency
633
+ });
634
+ // Use price...
635
+ }
636
+ ```
637
+
638
+ ## Performance
639
+
640
+ The library is optimized for high-performance financial calculations:
641
+
642
+ - **10,000 Money creations**: < 500ms
643
+ - **10,000 additions**: < 200ms
644
+ - **10,000 comparisons**: < 100ms
645
+ - **10,000 formats (with caching)**: < 500ms
646
+ - **1,000 allocations**: < 500ms
647
+
648
+ All tests run on modern hardware with Bun runtime.
649
+
650
+ ## TypeScript
651
+
652
+ Full TypeScript support with strict types:
653
+
654
+ ```typescript
655
+ import type { Money, MoneyJSON, Currency, FormatOptions } from "@f-o-t/money";
656
+
657
+ // Money is the core type
658
+ const money: Money = of("100", "USD");
659
+
660
+ // MoneyJSON for API contracts
661
+ const json: MoneyJSON = { amount: "100.00", currency: "USD" };
662
+
663
+ // Currency metadata
664
+ const currency: Currency = getCurrency("USD");
665
+
666
+ // Format configuration
667
+ const options: FormatOptions = {
668
+ notation: "compact",
669
+ hideSymbol: true
670
+ };
671
+ ```
672
+
673
+ ## Contributing
674
+
675
+ Contributions are welcome! Please check the repository for guidelines.
676
+
677
+ ## License
678
+
679
+ MIT License - see [LICENSE](./LICENSE) file for details.
680
+
681
+ ## Credits
682
+
683
+ Built by the Finance Tracker team as part of the Montte NX monorepo.
684
+
685
+ ## Links
686
+
687
+ - [GitHub Repository](https://github.com/F-O-T/montte-nx)
688
+ - [Issue Tracker](https://github.com/F-O-T/montte-nx/issues)
689
+ - [NPM Package](https://www.npmjs.com/package/@f-o-t/money)