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.
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/currencymap.json +536 -0
- package/dist/currency.d.ts +38 -0
- package/dist/currency.js +56 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +80 -0
- package/dist/exchange-rate-service.d.ts +96 -0
- package/dist/exchange-rate-service.js +174 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +58 -0
- package/dist/money-converter.d.ts +82 -0
- package/dist/money-converter.js +139 -0
- package/dist/money.d.ts +148 -0
- package/dist/money.js +360 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016 Conny Brunnkvist
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# subunit-money
|
|
2
|
+
|
|
3
|
+
A _TypeScript-first_ [value object](https://martinfowler.com/bliki/ValueObject.html) for dealing with money and currencies. Uses _currency subunits_ (cents, pence, etc.) internally, via BigInt, for precision-safe calculations.
|
|
4
|
+
|
|
5
|
+
> **Note**: This is a complete TypeScript rewrite of [`cbrunnkvist/es-money`](https://github.com/cbrunnkvist/es-money), modernized with BigInt internals, enhanced type safety, and currency conversion support.
|
|
6
|
+
|
|
7
|
+
## Basic Usage
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Money } from 'subunit-money'
|
|
11
|
+
|
|
12
|
+
const price = new Money('USD', '19.99')
|
|
13
|
+
const tax = price.multiply(0.0825)
|
|
14
|
+
const total = price.add(tax)
|
|
15
|
+
|
|
16
|
+
console.log(total.toString()) // "21.63 USD"
|
|
17
|
+
console.log(total.amount) // "21.63" (string, safe for JSON/DB)
|
|
18
|
+
console.log(total.toNumber()) // 21.63 (number, for calculations)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why Model Money as a Value Object?
|
|
22
|
+
|
|
23
|
+
Naive JavaScript math fails for monetary values in subtle but critical ways:
|
|
24
|
+
|
|
25
|
+
**Floating-Point Errors**: JavaScript represents decimals in binary, so values like 19.99 are approximations. Operations on approximations compound the error.
|
|
26
|
+
|
|
27
|
+
**Deferred Rounding**: The classic example is `0.1 + 0.2 === 0.3` returning `false`. But in accounting, deferred rounding is worse—you might accumulate errors silently across many transactions.
|
|
28
|
+
|
|
29
|
+
**The Split Penny Problem**: Imagine charging tax ($1.649175) on 10 items. If you round per-item (legally required on receipts), that's $1.65 × 10 = $16.50. But if you defer rounding, 10 × $1.649175 = $16.49. That missing penny is a real problem. Money objects round immediately after multiplication to prevent this.
|
|
30
|
+
|
|
31
|
+
This library uses BigInt internally to store currency in subunits (cents, satoshis, etc.), making all arithmetic exact.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Precision-safe**: BigInt internals prevent floating-point errors (`0.1 + 0.2` works correctly)
|
|
36
|
+
- **Type-safe**: Full TypeScript support with currency-branded types
|
|
37
|
+
- **Immutable**: All operations return new instances
|
|
38
|
+
- **Serialization-safe**: String amounts survive JSON/database round-trips
|
|
39
|
+
- **Zero dependencies**: Just Node.js 18+
|
|
40
|
+
- **Cryptocurrency ready**: Supports 8+ decimal places (Bitcoin, etc.)
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install subunit-money
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API Reference
|
|
49
|
+
|
|
50
|
+
### Creating Money
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// From string (recommended)
|
|
54
|
+
new Money('USD', '100.00')
|
|
55
|
+
|
|
56
|
+
// From number
|
|
57
|
+
new Money('USD', 100)
|
|
58
|
+
|
|
59
|
+
// From subunits (e.g., cents from database)
|
|
60
|
+
Money.fromSubunits(10000n, 'USD') // $100.00
|
|
61
|
+
|
|
62
|
+
// Zero amount
|
|
63
|
+
Money.zero('EUR') // 0.00 EUR
|
|
64
|
+
|
|
65
|
+
// From plain object (e.g., from JSON)
|
|
66
|
+
Money.fromObject({ currency: 'USD', amount: '50.00' })
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Arithmetic
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const a = new Money('USD', '100.00')
|
|
73
|
+
const b = new Money('USD', '25.00')
|
|
74
|
+
|
|
75
|
+
a.add(b) // 125.00 USD
|
|
76
|
+
a.subtract(b) // 75.00 USD
|
|
77
|
+
a.multiply(1.5) // 150.00 USD
|
|
78
|
+
|
|
79
|
+
// Allocate proportionally (never loses cents)
|
|
80
|
+
a.allocate([70, 30]) // [70.00 USD, 30.00 USD]
|
|
81
|
+
a.allocate([1, 1, 1]) // [33.34 USD, 33.33 USD, 33.33 USD]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Comparisons
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const a = new Money('USD', '100.00')
|
|
88
|
+
const b = new Money('USD', '50.00')
|
|
89
|
+
|
|
90
|
+
a.equalTo(b) // false
|
|
91
|
+
a.greaterThan(b) // true
|
|
92
|
+
a.lessThan(b) // false
|
|
93
|
+
a.isZero() // false
|
|
94
|
+
a.isPositive() // true
|
|
95
|
+
a.isNegative() // false
|
|
96
|
+
|
|
97
|
+
// For sorting
|
|
98
|
+
Money.compare(a, b) // 1 (positive = a > b)
|
|
99
|
+
[b, a].sort(Money.compare) // [50.00, 100.00]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Serialization
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const money = new Money('USD', '99.99')
|
|
106
|
+
|
|
107
|
+
// For JSON APIs
|
|
108
|
+
money.toJSON() // { currency: 'USD', amount: '99.99' }
|
|
109
|
+
JSON.stringify(money) // '{"currency":"USD","amount":"99.99"}'
|
|
110
|
+
|
|
111
|
+
// For display
|
|
112
|
+
money.toString() // "99.99 USD"
|
|
113
|
+
|
|
114
|
+
// For database storage (as integer)
|
|
115
|
+
money.toSubunits() // 9999n (BigInt)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Currency Conversion
|
|
119
|
+
|
|
120
|
+
For cross-currency operations, use `ExchangeRateService` and `MoneyConverter`:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { Money, ExchangeRateService, MoneyConverter } from 'subunit-money'
|
|
124
|
+
|
|
125
|
+
// Set up exchange rates
|
|
126
|
+
const rates = new ExchangeRateService()
|
|
127
|
+
rates.setRate('USD', 'EUR', '0.92')
|
|
128
|
+
rates.setRate('USD', 'GBP', '0.79')
|
|
129
|
+
|
|
130
|
+
// Create converter
|
|
131
|
+
const converter = new MoneyConverter(rates)
|
|
132
|
+
|
|
133
|
+
// Convert currencies
|
|
134
|
+
const dollars = new Money('USD', '100.00')
|
|
135
|
+
const euros = converter.convert(dollars, 'EUR')
|
|
136
|
+
console.log(euros.toString()) // "92.00 EUR"
|
|
137
|
+
|
|
138
|
+
// Cross-currency arithmetic
|
|
139
|
+
const usd = new Money('USD', '100.00')
|
|
140
|
+
const eur = new Money('EUR', '50.00')
|
|
141
|
+
const total = converter.add(usd, eur, 'USD') // Sum in USD
|
|
142
|
+
|
|
143
|
+
// Sum multiple currencies
|
|
144
|
+
const amounts = [
|
|
145
|
+
new Money('USD', '100.00'),
|
|
146
|
+
new Money('EUR', '50.00'),
|
|
147
|
+
new Money('GBP', '25.00')
|
|
148
|
+
]
|
|
149
|
+
converter.sum(amounts, 'USD') // Total in USD
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Exchange Rate Features
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Inverse rates are auto-created
|
|
156
|
+
rates.setRate('USD', 'EUR', '0.92')
|
|
157
|
+
rates.getRate('EUR', 'USD') // Returns ~1.087 automatically
|
|
158
|
+
|
|
159
|
+
// You can disable auto-inverse if you want to set both directions explicitly
|
|
160
|
+
rates.setRate('EUR', 'USD', '1.10', undefined, false) // Won't auto-create USD→EUR
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Custom Currencies
|
|
164
|
+
|
|
165
|
+
The module includes 120+ currencies. Add custom ones:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { registerCurrency, Money } from 'subunit-money'
|
|
169
|
+
|
|
170
|
+
// Add cryptocurrency
|
|
171
|
+
registerCurrency('BTC', 8)
|
|
172
|
+
const bitcoin = new Money('BTC', '0.00010000')
|
|
173
|
+
|
|
174
|
+
// Add custom token
|
|
175
|
+
registerCurrency('POINTS', 0)
|
|
176
|
+
const points = new Money('POINTS', '500')
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Error Handling
|
|
180
|
+
|
|
181
|
+
All errors extend `Error` with specific types:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import {
|
|
185
|
+
CurrencyMismatchError, // Adding USD + EUR without converter
|
|
186
|
+
CurrencyUnknownError, // Unknown currency code
|
|
187
|
+
SubunitError, // Too many decimal places
|
|
188
|
+
AmountError, // Invalid amount format
|
|
189
|
+
ExchangeRateError // Missing exchange rate
|
|
190
|
+
} from 'subunit-money'
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const usd = new Money('USD', '100.00')
|
|
194
|
+
const eur = new Money('EUR', '50.00')
|
|
195
|
+
usd.add(eur) // Throws CurrencyMismatchError
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (e instanceof CurrencyMismatchError) {
|
|
198
|
+
console.log(`Cannot mix ${e.fromCurrency} and ${e.toCurrency}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Design Philosophy
|
|
204
|
+
|
|
205
|
+
- **Minimal surface area**: A value object should be just a value object
|
|
206
|
+
- **Fail fast**: Errors thrown immediately, not silently propagated
|
|
207
|
+
- **No localization**: Formatting for display is your app's concern
|
|
208
|
+
- **No rounding rules**: Currency-specific rounding is application-specific
|
|
209
|
+
|
|
210
|
+
## Why String Amounts?
|
|
211
|
+
|
|
212
|
+
The `amount` property returns a string to prevent accidental precision loss:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const money = new Money('USD', '0.10')
|
|
216
|
+
|
|
217
|
+
// Safe: survives JSON round-trip
|
|
218
|
+
const json = JSON.stringify(money)
|
|
219
|
+
const restored = Money.fromObject(JSON.parse(json))
|
|
220
|
+
restored.equalTo(money) // true
|
|
221
|
+
|
|
222
|
+
// Safe: database storage
|
|
223
|
+
db.insert({ price: money.amount }) // "0.10"
|
|
224
|
+
const loaded = new Money('USD', row.price) // Works perfectly
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Use `toNumber()` only when you explicitly need numeric calculations.
|
|
228
|
+
|
|
229
|
+
## Requirements
|
|
230
|
+
|
|
231
|
+
- Node.js 18+
|
|
232
|
+
- TypeScript 5+ (for consumers using TypeScript)
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
Copyright (c) 2016-2025 Conny Brunnkvist. Licensed under the [MIT License](./LICENSE)
|