@arki/money 0.0.0-stage → 0.0.4
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 +238 -2
- package/dist/bag.d.ts +48 -0
- package/dist/bag.d.ts.map +1 -0
- package/dist/bag.js +110 -0
- package/dist/bag.js.map +1 -0
- package/dist/currency.d.ts +64 -0
- package/dist/currency.d.ts.map +1 -0
- package/dist/currency.js +148 -0
- package/dist/currency.js.map +1 -0
- package/dist/data/iso4217.d.ts +9 -0
- package/dist/data/iso4217.d.ts.map +1 -0
- package/dist/data/iso4217.js +11 -0
- package/dist/data/iso4217.js.map +1 -0
- package/dist/drizzle/config.d.ts +27 -0
- package/dist/drizzle/config.d.ts.map +1 -0
- package/dist/drizzle/config.js +16 -0
- package/dist/drizzle/config.js.map +1 -0
- package/dist/drizzle/mysql.d.ts +36 -0
- package/dist/drizzle/mysql.d.ts.map +1 -0
- package/dist/drizzle/mysql.js +45 -0
- package/dist/drizzle/mysql.js.map +1 -0
- package/dist/drizzle/pg.d.ts +38 -0
- package/dist/drizzle/pg.d.ts.map +1 -0
- package/dist/drizzle/pg.js +49 -0
- package/dist/drizzle/pg.js.map +1 -0
- package/dist/drizzle/sqlite.d.ts +38 -0
- package/dist/drizzle/sqlite.d.ts.map +1 -0
- package/dist/drizzle/sqlite.js +45 -0
- package/dist/drizzle/sqlite.js.map +1 -0
- package/dist/drizzle.d.ts +56 -0
- package/dist/drizzle.d.ts.map +1 -0
- package/dist/drizzle.js +77 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/exact.d.ts +20 -0
- package/dist/exact.d.ts.map +1 -0
- package/dist/exact.js +23 -0
- package/dist/exact.js.map +1 -0
- package/dist/exchange.d.ts +84 -0
- package/dist/exchange.d.ts.map +1 -0
- package/dist/exchange.js +215 -0
- package/dist/exchange.js.map +1 -0
- package/dist/format.d.ts +41 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +122 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/columns.d.ts +25 -0
- package/dist/internal/columns.d.ts.map +1 -0
- package/dist/internal/columns.js +47 -0
- package/dist/internal/columns.js.map +1 -0
- package/dist/internal/units.d.ts +66 -0
- package/dist/internal/units.d.ts.map +1 -0
- package/dist/internal/units.js +205 -0
- package/dist/internal/units.js.map +1 -0
- package/dist/money.d.ts +180 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +577 -0
- package/dist/money.js.map +1 -0
- package/dist/parse.d.ts +53 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +222 -0
- package/dist/parse.js.map +1 -0
- package/dist/provider.d.ts +22 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +56 -0
- package/dist/provider.js.map +1 -0
- package/dist/rational.d.ts +67 -0
- package/dist/rational.d.ts.map +1 -0
- package/dist/rational.js +169 -0
- package/dist/rational.js.map +1 -0
- package/dist/stripe.d.ts +37 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +75 -0
- package/dist/stripe.js.map +1 -0
- package/dist/zod.d.ts +54 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +96 -0
- package/dist/zod.js.map +1 -0
- package/package.json +137 -4
- package/src/bag.ts +128 -0
- package/src/currency.ts +174 -0
- package/src/data/iso4217.ts +13 -0
- package/src/drizzle/config.ts +39 -0
- package/src/drizzle/mysql.ts +61 -0
- package/src/drizzle/pg.ts +64 -0
- package/src/drizzle/sqlite.ts +61 -0
- package/src/errors.ts +61 -0
- package/src/exact.ts +29 -0
- package/src/exchange.ts +280 -0
- package/src/format.ts +148 -0
- package/src/index.ts +16 -0
- package/src/internal/columns.ts +74 -0
- package/src/internal/units.ts +223 -0
- package/src/money.ts +692 -0
- package/src/parse.ts +278 -0
- package/src/provider.ts +67 -0
- package/src/rational.ts +214 -0
- package/src/stripe.ts +84 -0
- package/src/zod.ts +123 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ARKI Contributors
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,239 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @arki/money
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Exact, fast money for every JavaScript runtime.
|
|
4
|
+
|
|
5
|
+
- **Exact.** Amounts are whole numbers of units: `19.99 EUR` is `1999` units at 2 decimals. Nothing rounds unless you pass a rounding mode.
|
|
6
|
+
- **Fast.** Units are a plain `number` up to 2^53 and a `bigint` only beyond that. Adding two amounts takes about 4 ns, against 350–550 ns in currency.js and dinero.js.
|
|
7
|
+
- **Safe types.** `Money<'EUR'>` cannot be added to `Money<'USD'>`. The compiler stops it, and the runtime throws `CurrencyMismatchError` too.
|
|
8
|
+
- **Same answer everywhere.** The ISO 4217 table ships with the package (178 currencies, published 2026-09-17). `Intl` is only used for display, so a value has the same decimals on every device.
|
|
9
|
+
- **Runs everywhere.** Pure ESM with web-standard globals only. Tested on Node, Bun, Deno and Chromium; built for React Native (Hermes).
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { factor, money, sum } from '@arki/money';
|
|
13
|
+
|
|
14
|
+
const price = money('19.99', 'EUR');
|
|
15
|
+
const total = sum([price, money('5.01', 'EUR')]); // 25.00 EUR
|
|
16
|
+
price.times(3); // 59.97 EUR
|
|
17
|
+
price.times('1.19'); // throws RoundingNecessaryError: 23.7881 does not fit
|
|
18
|
+
price.times('1.19', 'halfEven'); // 23.79 EUR
|
|
19
|
+
money(100, 'EUR').allocate([1, 1, 1]); // [33.34, 33.33, 33.33] — adds back to 100.00
|
|
20
|
+
money(1, 'EUR').plus(money(1, 'USD')); // compile error, and CurrencyMismatchError at run time
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Creating money
|
|
24
|
+
|
|
25
|
+
| Call | Result |
|
|
26
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
27
|
+
| `money('19.99', 'EUR')` | From major units. Strings are the safest input. |
|
|
28
|
+
| `money(19.99, 'EUR')` | A number is read as written. `0.1 + 0.2` arrives as `0.30000000000000004` and throws. |
|
|
29
|
+
| `money('1.005', 'EUR', { rounding: 'halfEven' })` | Extra digits need a rounding mode. |
|
|
30
|
+
| `money('0.1234', 'EUR', { scale: 4 })` | Keep more decimals, for unit prices. |
|
|
31
|
+
| `Money.fromMinor(1999, 'EUR')` | From cents. Also takes a `bigint` or an integer string, as databases return `BIGINT`. |
|
|
32
|
+
| `Money.zero('EUR')` | Zero. |
|
|
33
|
+
| `Money.parse('19.99 EUR')` / `Money.fromJSON(json)` | Reads what `toString()` / `toJSON()` write. |
|
|
34
|
+
| `Money.of(m, 'EUR')` | Checks a `Money<string>` is EUR and narrows its type to `Money<'EUR'>`. |
|
|
35
|
+
|
|
36
|
+
`money()` also accepts a `Decimal` or a `Rational` from `@arki/math`.
|
|
37
|
+
|
|
38
|
+
## Rounding
|
|
39
|
+
|
|
40
|
+
Rounding modes: `up`, `down`, `ceiling`, `floor`, `halfUp`, `halfDown`, `halfEven`, `unnecessary`.
|
|
41
|
+
|
|
42
|
+
There is no default mode. `times`, `dividedBy`, `percentage`, `toScale` and `roundToStep` return the exact result when it fits, and throw `RoundingNecessaryError` when it does not and no mode was given. `plus`, `minus`, `allocate`, `split` and `sum` are always exact.
|
|
43
|
+
|
|
44
|
+
Cash rounding is a step, because it is set by each country's law, not by the currency:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
money('1.025', 'CHF', { scale: 3 }).roundToStep('0.05', 'halfUp'); // 1.050 CHF
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For a calculation with several steps, round once at the end through `@arki/math`:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { ratio } from '@arki/math/rational';
|
|
54
|
+
import { toRational } from '@arki/money/exact';
|
|
55
|
+
|
|
56
|
+
const share = toRational(price).times(ratio(1, 3)).plus(ratio(1, 7));
|
|
57
|
+
money(share, 'EUR', { rounding: 'halfEven' });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Splitting
|
|
61
|
+
|
|
62
|
+
`allocate(ratios, { remainder })` and `split(parts, { remainder })` always add back to the whole. Leftover units go to the parts whose exact share lost the most (`'largest'`, the default), or from the start (`'first'`) or end (`'last'`). A zero ratio never receives a unit. Ratios may be decimals: `allocate(['0.5', '0.25', '0.25'])`.
|
|
63
|
+
|
|
64
|
+
## Currencies
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Currency } from '@arki/money';
|
|
68
|
+
|
|
69
|
+
Currency.of('EUR'); // { code: 'EUR', numeric: '978', scale: 2, iso: true }
|
|
70
|
+
Currency.fromNumeric('978'); // EUR
|
|
71
|
+
Currency.of('ZZZ'); // throws UnknownCurrencyError (Intl would accept it)
|
|
72
|
+
const btc = Currency.custom({ code: 'BTC', scale: 8 });
|
|
73
|
+
money('0.00012', btc); // 0.00012000 BTC
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Custom currencies are not registered globally. Keep the object and pass it where you would pass a code. Codes that ISO defines without decimals (gold `XAU`, test code `XTS`) use 0.
|
|
77
|
+
|
|
78
|
+
## Output
|
|
79
|
+
|
|
80
|
+
| Call | Result |
|
|
81
|
+
| -------------------------------------------------------- | ------------------------------------------------------- |
|
|
82
|
+
| `toString()` | `'19.99 EUR'`, locale-free |
|
|
83
|
+
| `toJSON()` | `{ amount: '19.99', currency: 'EUR' }` |
|
|
84
|
+
| `toDecimalString()` | `'19.99'`, for `NUMERIC` columns |
|
|
85
|
+
| `toMinorUnits()` / `toMinorBigInt()` / `toMinorNumber()` | `1999`, for `BIGINT` columns and payment APIs |
|
|
86
|
+
| `toNumber()` | Nearest float, for charts only |
|
|
87
|
+
| `+price` | Throws. A `Money` never turns into a float by accident. |
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createMoneyFormatter, formatMoney } from '@arki/money/format';
|
|
91
|
+
|
|
92
|
+
formatMoney(money('1234.5', 'EUR'), { locale: 'de-DE' }); // '1.234,50 €'
|
|
93
|
+
formatMoney(money(-5, 'USD'), { accounting: true }); // '($5.00)'
|
|
94
|
+
const de = createMoneyFormatter({ locale: 'de-DE' }); // binds options once, for loops
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Formatting passes the amount to `Intl` as exact text, so digits past 2^53 survive. If the runtime lacks a feature you asked for (Hermes on iOS lacks some), it throws `UnsupportedFeatureError` instead of printing something else.
|
|
98
|
+
|
|
99
|
+
## Reading what people type
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { createMoneyParser, parseMoney } from '@arki/money/parse';
|
|
103
|
+
|
|
104
|
+
parseMoney('1.234,56 €', 'EUR', { locale: 'de-DE' }); // 1234.56 EUR
|
|
105
|
+
parseMoney('₹1,23,456.78', 'INR', { locale: 'en-IN' }); // 123456.78 INR
|
|
106
|
+
parseMoney('($1,234.56)', 'USD'); // -1234.56 USD
|
|
107
|
+
parseMoney('$10', 'CAD'); // 10.00 CAD — you say which currency, so '$' is never guessed
|
|
108
|
+
parseMoney('1,23,4', 'USD'); // throws NumberFormatError: grouping does not match the locale
|
|
109
|
+
|
|
110
|
+
const form = createMoneyParser('EUR', { locale: 'de-DE' }); // reads the locale once
|
|
111
|
+
form.tryParse('12,505 €'); // undefined — too many decimals, for form validation
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Separators, digits, minus sign and currency symbols come from `Intl` for the locale. Parsing is strict: wrong grouping, the other locale's decimal separator, two signs, mixed digit scripts or a different currency's symbol throw instead of being guessed. Text without grouping (`'1234.5'`) is always accepted. On runtimes without `formatToParts` (Hermes on iOS), pass `symbols: { decimal: ',', group: '.' }` instead of a locale.
|
|
115
|
+
|
|
116
|
+
## Converting currencies
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { convert, createConverter, exchangeRate, ratesFrom } from '@arki/money/exchange';
|
|
120
|
+
|
|
121
|
+
const rates = ratesFrom({ base: 'EUR', rates: { USD: '1.0832', GBP: '0.8571' } });
|
|
122
|
+
const fx = createConverter(rates, { inverse: true, via: 'EUR' });
|
|
123
|
+
fx.convert(money('1234.56', 'USD'), 'GBP', { rounding: 'halfEven' }); // 976.87 GBP, USD → EUR → GBP rounded once
|
|
124
|
+
fx.rate('USD', 'GBP').toString(); // 'USD/GBP 8571/10832'
|
|
125
|
+
|
|
126
|
+
convert(money(100, 'EUR'), exchangeRate('EUR', 'USD', '1.0832')); // 108.32 USD
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Rates are exact fractions, never floats, so inverse and cross rates add no error. A conversion rounds once, at the end; converting in two rounded steps above would give 976.86. Reversed rates (`inverse`) and routing through a base currency (`via`) are opt-in, because a bid rate reversed is not an ask rate. There is no network code: fetch rates from wherever you trust and pass them in.
|
|
130
|
+
|
|
131
|
+
## Payment providers
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { fromStripeAmount, toStripe, toStripeAmount } from '@arki/money/stripe';
|
|
135
|
+
|
|
136
|
+
toStripe(money('10.99', 'EUR')); // { amount: 1099, currency: 'eur' }
|
|
137
|
+
toStripeAmount(money(5, 'ISK')); // 500 — Stripe sends ISK with two decimals that are always 00
|
|
138
|
+
toStripeAmount(money('10.50', 'MGA')); // throws ProviderAmountError — MGA is zero-decimal at Stripe
|
|
139
|
+
toStripeAmount(money('10.45', 'HUF'), 'payout'); // throws — HUF payouts must be whole forints
|
|
140
|
+
fromStripeAmount(1099, 'usd'); // 10.99 USD
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Rules come from docs.stripe.com/currencies, checked on 2026-10-12 (`STRIPE_RULES_VERSION`). Stripe documents no rule for currencies with 3 or 4 decimals, so those throw instead of being guessed. For other providers, describe their units with `toProviderUnits(money, { scale, step })` and `fromProviderUnits` from `@arki/money/provider`.
|
|
144
|
+
|
|
145
|
+
## Exact steps and several currencies
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { MoneyBag } from '@arki/money/bag';
|
|
149
|
+
import { RationalMoney } from '@arki/money/rational';
|
|
150
|
+
|
|
151
|
+
const third = RationalMoney.of(100, 'EUR').dividedBy(3); // '100/3 EUR', no rounding yet
|
|
152
|
+
third.plus(third).plus(third).toMoney(); // 100.00 EUR — three rounded thirds would give 99.99
|
|
153
|
+
third.times('1.19').toMoney({ rounding: 'halfEven' }); // 39.67 EUR, rounded once
|
|
154
|
+
|
|
155
|
+
const wallet = MoneyBag.of(money('1.07', 'USD'), money('1.06', 'GBP')); // two balances, never one fake total
|
|
156
|
+
wallet.convert(fx, 'EUR', { rounding: 'halfEven' }); // 2.22 EUR; rounding each balance first gives 2.23
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A `RationalMoney` is an exact fraction of major units that keeps its currency, so EUR and USD still cannot mix halfway through a calculation. A `MoneyBag` holds one exact balance per currency and drops zeros.
|
|
160
|
+
|
|
161
|
+
## Validation and databases
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { anyMoneySchema, moneyInputSchema, moneySchema } from '@arki/money/zod';
|
|
165
|
+
|
|
166
|
+
const order = z.object({ total: moneySchema('EUR', { min: money(0, 'EUR') }) });
|
|
167
|
+
order.parse({ total: { amount: '19.99', currency: 'EUR' } }).total; // Money<'EUR'>
|
|
168
|
+
moneyInputSchema('EUR', { locale: 'de-DE' }).parse('1.234,56 €'); // form field
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { moneyDecimal, moneyJson, moneyMinor } from '@arki/money/drizzle/pg'; // or /drizzle/mysql, /drizzle/sqlite
|
|
173
|
+
|
|
174
|
+
const prices = pgTable('prices', {
|
|
175
|
+
total: moneyMinor('total', { currency: 'EUR' }), // bigint of cents ↔ Money<'EUR'>
|
|
176
|
+
unit: moneyDecimal('unit', { currency: 'EUR', scale: 4 }), // numeric(19, 4)
|
|
177
|
+
price: moneyJson('price'), // jsonb { amount, currency } ↔ Money in any currency
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Column | Postgres | MySQL / MariaDB | SQLite |
|
|
182
|
+
| -------------- | --------------- | --------------- | ------------------------------------ |
|
|
183
|
+
| `moneyMinor` | `bigint` | `bigint` | `integer` |
|
|
184
|
+
| `moneyDecimal` | `numeric(p, s)` | `decimal(p, s)` | `text` (SQLite has no exact decimal) |
|
|
185
|
+
| `moneyJson` | `jsonb` | `json` | `text` |
|
|
186
|
+
|
|
187
|
+
SQLite drivers usually return numbers, which are exact up to 2^53 minor units. A bigger stored amount throws on read instead of turning into a wrong value; switch the driver to big integers to read it (libSQL `intMode: 'bigint'`, better-sqlite3 `safeIntegers()`).
|
|
188
|
+
|
|
189
|
+
The schemas take the JSON shape as input, so tRPC and oRPC clients infer what to send. The columns read text from every driver, so no value passes through a float, and writing another currency or a value that needs rounding throws. `zod` (4.x) and `drizzle-orm` (1.x) are optional peer dependencies, used only by their subpaths.
|
|
190
|
+
|
|
191
|
+
## Errors
|
|
192
|
+
|
|
193
|
+
Every deliberate failure extends `MathError` from `@arki/math`, so one `instanceof` catches both libraries. Money-only errors extend `MoneyError`: `CurrencyMismatchError`, `UnknownCurrencyError`, `ExchangeRateError`, `ProviderAmountError`. Shared ones: `RoundingNecessaryError`, `DivisionByZeroError`, `InvalidArgumentError`, `NumberFormatError`, `NumericRangeError`, `ResourceLimitError`, `EmptyDataError`, `UnsupportedFeatureError`.
|
|
194
|
+
|
|
195
|
+
## Entry points
|
|
196
|
+
|
|
197
|
+
| Import | Contents |
|
|
198
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
199
|
+
| `@arki/money` | `Money`, `money`, `Currency`, `factor`, `sum`, `average`, `min`, `max`, errors |
|
|
200
|
+
| `@arki/money/currency` | `Currency` alone |
|
|
201
|
+
| `@arki/money/format` | `formatMoney`, `formatMoneyToParts`, `createMoneyFormatter` |
|
|
202
|
+
| `@arki/money/exact` | `toDecimal`, `toRational` bridges to `@arki/math` |
|
|
203
|
+
| `@arki/money/errors` | Error classes |
|
|
204
|
+
| `@arki/money/parse` | `parseMoney`, `createMoneyParser` |
|
|
205
|
+
| `@arki/money/exchange` | `exchangeRate`, `ratesFrom`, `convert`, `createConverter`, `Converter` |
|
|
206
|
+
| `@arki/money/provider` | `toProviderUnits`, `fromProviderUnits` |
|
|
207
|
+
| `@arki/money/rational` | `RationalMoney`, `isRationalMoney` |
|
|
208
|
+
| `@arki/money/bag` | `MoneyBag` |
|
|
209
|
+
| `@arki/money/zod` | `moneySchema`, `anyMoneySchema`, `moneyInputSchema`, `currencyCodeSchema` |
|
|
210
|
+
| `@arki/money/drizzle/pg`, `/mysql`, `/sqlite` | `moneyMinor`, `moneyDecimal`, `moneyJson` |
|
|
211
|
+
| `@arki/money/stripe` | `toStripe`, `toStripeAmount`, `fromStripeAmount`, `stripeRule` |
|
|
212
|
+
|
|
213
|
+
## Performance
|
|
214
|
+
|
|
215
|
+
`bun run bench` compares against dinero.js v1, currency.js, big.js and the old `@arki/math/money`. Node 22, Apple Silicon, ns/op:
|
|
216
|
+
|
|
217
|
+
| Operation | @arki/money | dinero.js v1 | currency.js | big.js |
|
|
218
|
+
| ------------------------------------------- | ----------: | -----------: | ----------: | -----: |
|
|
219
|
+
| add | 3.5 | 457 | 470 | 67 |
|
|
220
|
+
| × 1.0825, round half-even (prepared factor) | 37 | 680 | 347 | 267 |
|
|
221
|
+
| allocate into 3 | 172 | 1244 | 1045 | — |
|
|
222
|
+
| compare | 5.7 | 11.8 | — | 13.6 |
|
|
223
|
+
| format en-US | 557 | 20725 | 674 | — |
|
|
224
|
+
|
|
225
|
+
Amounts past 2^53 switch to `bigint` and stay exact; adding them costs about 29 ns. Converting with a cached rate takes about 170 ns, parsing German input about 1.1 µs, and `toStripeAmount` about 80 ns.
|
|
226
|
+
|
|
227
|
+
## Size
|
|
228
|
+
|
|
229
|
+
`bun run size` bundles each entry point as an app would (minified, gzipped, `@arki/math` included) and fails past its budget. The core (`Money`, `Currency` with the full ISO table, errors) is 6.3 KB. Parsing adds 0.9 KB, conversion 0.6 KB, Stripe 0.1 KB, and `RationalMoney` 3.2 KB (the exact `Rational` type from `@arki/math`).
|
|
230
|
+
|
|
231
|
+
## Runtime support
|
|
232
|
+
|
|
233
|
+
Node 20+, Bun, Deno, modern browsers and React Native on Hermes V1 (React Native 0.86). `bun run test:runtimes` runs the same checks on all five; the Hermes leg needs a `hermes` binary (`HERMES_BIN`, build steps in `scripts/smoke-hermes.mjs`) and says so loudly when it is missing.
|
|
234
|
+
|
|
235
|
+
On Hermes for Apple platforms, `Intl.NumberFormat` has no `formatToParts` and formats number strings through a float. So `parseMoney` with a `locale` throws `UnsupportedFeatureError` (pass `symbols` instead), `formatMoneyToParts` throws, and `formatMoney` is exact up to 15 significant digits and throws beyond. Arithmetic, allocation, conversion, Stripe units and JSON work everywhere.
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
package/dist/bag.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A wallet of several currencies: `10 EUR + 5 USD` stays two balances. It never
|
|
3
|
+
* pretends mixed amounts have one total until you convert it, and then it
|
|
4
|
+
* converts each balance exactly and rounds once.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const cart = MoneyBag.of(money(10, 'EUR'), money(5, 'USD')).plus(money('2.50', 'EUR'));
|
|
8
|
+
* cart.get('EUR').toString(); // '25/2 EUR'
|
|
9
|
+
* cart.convert(fx, 'EUR', { rounding: 'halfEven' }); // one EUR total
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import type { Currency, CurrencyLike } from './currency.js';
|
|
13
|
+
import type { Converter } from './exchange.js';
|
|
14
|
+
import type { MoneyJSON, MoneyOptions } from './money.js';
|
|
15
|
+
import type { ExactMoney } from './rational.js';
|
|
16
|
+
import { Money } from './money.js';
|
|
17
|
+
import { RationalMoney } from './rational.js';
|
|
18
|
+
/** An immutable set of balances, at most one per currency. Zero balances are dropped. */
|
|
19
|
+
export declare class MoneyBag {
|
|
20
|
+
private readonly balances;
|
|
21
|
+
private constructor();
|
|
22
|
+
static empty(): MoneyBag;
|
|
23
|
+
/** `MoneyBag.of(money(10, 'EUR'), money(5, 'USD'))`. */
|
|
24
|
+
static of(...items: (ExactMoney | MoneyBag)[]): MoneyBag;
|
|
25
|
+
/** Adds amounts or whole bags. Returns a new bag. */
|
|
26
|
+
plus(...items: (ExactMoney | MoneyBag)[]): MoneyBag;
|
|
27
|
+
/** Subtracts amounts or whole bags. Balances may go negative. Returns a new bag. */
|
|
28
|
+
minus(...items: (ExactMoney | MoneyBag)[]): MoneyBag;
|
|
29
|
+
/** The exact balance in one currency; zero when the bag holds none. */
|
|
30
|
+
get<const C extends string>(currency: CurrencyLike<C>): RationalMoney<C>;
|
|
31
|
+
/** The currencies with a non-zero balance, sorted by code. */
|
|
32
|
+
currencies(): Currency[];
|
|
33
|
+
/** The non-zero balances, sorted by currency code. */
|
|
34
|
+
entries(): RationalMoney[];
|
|
35
|
+
/** `true` when every balance is zero. */
|
|
36
|
+
isZero(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* One total in `currency`: each balance is converted exactly, the results are
|
|
39
|
+
* added, and the sum is rounded once. Missing rates throw `ExchangeRateError`.
|
|
40
|
+
*/
|
|
41
|
+
convert<const C extends string>(converter: Converter, currency: CurrencyLike<C>, options?: MoneyOptions): Money<C>;
|
|
42
|
+
/** `[{ amount: '25/2', currency: 'EUR' }, ...]`, sorted by currency code. */
|
|
43
|
+
toJSON(): MoneyJSON[];
|
|
44
|
+
/** `'12.5 EUR + 5 USD'`-style text with exact fractions: `'25/2 EUR + 5 USD'`. Empty bag: `'0'`. */
|
|
45
|
+
toString(): string;
|
|
46
|
+
private combine;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=bag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bag.d.ts","sourceRoot":"","sources":["../src/bag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAQ9C,yFAAyF;AACzF,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAE9D,OAAO,eAGN;IAED,MAAM,CAAC,KAAK,IAAI,QAAQ,CAEvB;IAED,wDAAwD;IACxD,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,UAAU,GAAG,QAAQ,CAAC,EAAE,GAAG,QAAQ,CAEvD;IAED,qDAAqD;IACrD,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,UAAU,GAAG,QAAQ,CAAC,EAAE,GAAG,QAAQ,CAElD;IAED,oFAAoF;IACpF,KAAK,CAAC,GAAG,KAAK,EAAE,CAAC,UAAU,GAAG,QAAQ,CAAC,EAAE,GAAG,QAAQ,CAEnD;IAED,uEAAuE;IACvE,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAMvE;IAED,8DAA8D;IAC9D,UAAU,IAAI,QAAQ,EAAE,CAEvB;IAED,sDAAsD;IACtD,OAAO,IAAI,aAAa,EAAE,CAEzB;IAED,yCAAyC;IACzC,MAAM,IAAI,OAAO,CAEhB;IAED;;;OAGG;IACH,OAAO,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,CAQjH;IAED,6EAA6E;IAC7E,MAAM,IAAI,SAAS,EAAE,CAEpB;IAED,oGAAoG;IACpG,QAAQ,IAAI,MAAM,CAGjB;IAUD,OAAO,CAAC,OAAO;CAchB"}
|
package/dist/bag.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A wallet of several currencies: `10 EUR + 5 USD` stays two balances. It never
|
|
3
|
+
* pretends mixed amounts have one total until you convert it, and then it
|
|
4
|
+
* converts each balance exactly and rounds once.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const cart = MoneyBag.of(money(10, 'EUR'), money(5, 'USD')).plus(money('2.50', 'EUR'));
|
|
8
|
+
* cart.get('EUR').toString(); // '25/2 EUR'
|
|
9
|
+
* cart.convert(fx, 'EUR', { rounding: 'halfEven' }); // one EUR total
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { ratio } from '@arki/math/rational';
|
|
13
|
+
import { assertSameCurrency, toCurrency } from './currency.js';
|
|
14
|
+
import { Money } from './money.js';
|
|
15
|
+
import { RationalMoney } from './rational.js';
|
|
16
|
+
const BRAND = Symbol('MoneyBag');
|
|
17
|
+
function exact(value) {
|
|
18
|
+
return value instanceof RationalMoney ? value : RationalMoney.from(value);
|
|
19
|
+
}
|
|
20
|
+
/** An immutable set of balances, at most one per currency. Zero balances are dropped. */
|
|
21
|
+
export class MoneyBag {
|
|
22
|
+
balances;
|
|
23
|
+
constructor(balances, brand) {
|
|
24
|
+
if (brand !== BRAND)
|
|
25
|
+
throw new TypeError('Use MoneyBag.of() instead of new MoneyBag()');
|
|
26
|
+
this.balances = balances;
|
|
27
|
+
}
|
|
28
|
+
static empty() {
|
|
29
|
+
return new MoneyBag(new Map(), BRAND);
|
|
30
|
+
}
|
|
31
|
+
/** `MoneyBag.of(money(10, 'EUR'), money(5, 'USD'))`. */
|
|
32
|
+
static of(...items) {
|
|
33
|
+
return MoneyBag.empty().plus(...items);
|
|
34
|
+
}
|
|
35
|
+
/** Adds amounts or whole bags. Returns a new bag. */
|
|
36
|
+
plus(...items) {
|
|
37
|
+
return this.combine(items, false);
|
|
38
|
+
}
|
|
39
|
+
/** Subtracts amounts or whole bags. Balances may go negative. Returns a new bag. */
|
|
40
|
+
minus(...items) {
|
|
41
|
+
return this.combine(items, true);
|
|
42
|
+
}
|
|
43
|
+
/** The exact balance in one currency; zero when the bag holds none. */
|
|
44
|
+
get(currency) {
|
|
45
|
+
const target = toCurrency(currency);
|
|
46
|
+
const found = this.balances.get(target.code);
|
|
47
|
+
if (found === undefined)
|
|
48
|
+
return RationalMoney.zero(target);
|
|
49
|
+
assertSameCurrency(found.currency, target);
|
|
50
|
+
return RationalMoney.create(found.amount, target);
|
|
51
|
+
}
|
|
52
|
+
/** The currencies with a non-zero balance, sorted by code. */
|
|
53
|
+
currencies() {
|
|
54
|
+
return this.entries().map(entry => entry.currency);
|
|
55
|
+
}
|
|
56
|
+
/** The non-zero balances, sorted by currency code. */
|
|
57
|
+
entries() {
|
|
58
|
+
return [...this.balances.values()].sort((a, b) => (a.currency.code < b.currency.code ? -1 : 1));
|
|
59
|
+
}
|
|
60
|
+
/** `true` when every balance is zero. */
|
|
61
|
+
isZero() {
|
|
62
|
+
return this.balances.size === 0;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One total in `currency`: each balance is converted exactly, the results are
|
|
66
|
+
* added, and the sum is rounded once. Missing rates throw `ExchangeRateError`.
|
|
67
|
+
*/
|
|
68
|
+
convert(converter, currency, options) {
|
|
69
|
+
const target = toCurrency(currency);
|
|
70
|
+
let total = ratio(0);
|
|
71
|
+
for (const balance of this.balances.values()) {
|
|
72
|
+
const rate = converter.rate(balance.currency, target);
|
|
73
|
+
total = total.plus(balance.amount.times(ratio(BigInt(rate.numerator), BigInt(rate.denominator))));
|
|
74
|
+
}
|
|
75
|
+
return Money.of(total, target, options);
|
|
76
|
+
}
|
|
77
|
+
/** `[{ amount: '25/2', currency: 'EUR' }, ...]`, sorted by currency code. */
|
|
78
|
+
toJSON() {
|
|
79
|
+
return this.entries().map(entry => entry.toJSON());
|
|
80
|
+
}
|
|
81
|
+
/** `'12.5 EUR + 5 USD'`-style text with exact fractions: `'25/2 EUR + 5 USD'`. Empty bag: `'0'`. */
|
|
82
|
+
toString() {
|
|
83
|
+
const entries = this.entries();
|
|
84
|
+
return entries.length === 0 ? '0' : entries.map(String).join(' + ');
|
|
85
|
+
}
|
|
86
|
+
[Symbol.for('nodejs.util.inspect.custom')]() {
|
|
87
|
+
return `MoneyBag(${this.toString()})`;
|
|
88
|
+
}
|
|
89
|
+
[Symbol.for('Deno.customInspect')]() {
|
|
90
|
+
return `MoneyBag(${this.toString()})`;
|
|
91
|
+
}
|
|
92
|
+
combine(items, subtract) {
|
|
93
|
+
const next = new Map(this.balances);
|
|
94
|
+
const add = (value) => {
|
|
95
|
+
const code = value.currency.code;
|
|
96
|
+
const current = next.get(code);
|
|
97
|
+
const delta = subtract ? value.negated() : value;
|
|
98
|
+
next.set(code, current === undefined ? delta : current.plus(delta));
|
|
99
|
+
};
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
if (item instanceof MoneyBag)
|
|
102
|
+
for (const balance of item.balances.values())
|
|
103
|
+
add(balance);
|
|
104
|
+
else
|
|
105
|
+
add(exact(item));
|
|
106
|
+
}
|
|
107
|
+
return new MoneyBag(new Map([...next].filter(([, balance]) => !balance.isZero())), BRAND);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=bag.js.map
|
package/dist/bag.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bag.js","sourceRoot":"","sources":["../src/bag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAM5C,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;AAEjC,SAAS,KAAK,CAAC,KAAiB;IAC9B,OAAO,KAAK,YAAY,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC;AAED,yFAAyF;AACzF,MAAM,OAAO,QAAQ;IACF,QAAQ,CAAqC;IAE9D,YAAoB,QAA4C,EAAE,KAAmB;QACnF,IAAI,KAAK,KAAK,KAAK;YAAE,MAAM,IAAI,SAAS,CAAC,6CAA6C,CAAC,CAAC;QACxF,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,MAAM,CAAC,KAAK;QACV,OAAO,IAAI,QAAQ,CAAC,IAAI,GAAG,EAAE,EAAE,KAAK,CAAC,CAAC;IACxC,CAAC;IAED,wDAAwD;IACxD,MAAM,CAAC,EAAE,CAAC,GAAG,KAAgC;QAC3C,OAAO,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;IACzC,CAAC;IAED,qDAAqD;IACrD,IAAI,CAAC,GAAG,KAAgC;QACtC,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,oFAAoF;IACpF,KAAK,CAAC,GAAG,KAAgC;QACvC,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,uEAAuE;IACvE,GAAG,CAAyB,QAAyB;QACnD,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3D,kBAAkB,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC3C,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpD,CAAC;IAED,8DAA8D;IAC9D,UAAU;QACR,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC;IAED,sDAAsD;IACtD,OAAO;QACL,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClG,CAAC;IAED,yCAAyC;IACzC,MAAM;QACJ,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,CAAC;IAClC,CAAC;IAED;;;OAGG;IACH,OAAO,CAAyB,SAAoB,EAAE,QAAyB,EAAE,OAAsB;QACrG,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACtD,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QACpG,CAAC;QACD,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED,6EAA6E;IAC7E,MAAM;QACJ,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,oGAAoG;IACpG,QAAQ;QACN,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtE,CAAC;IAED,CAAC,MAAM,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QACxC,OAAO,YAAY,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC;IACxC,CAAC;IAED,CAAC,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;QAChC,OAAO,YAAY,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC;IACxC,CAAC;IAEO,OAAO,CAAC,KAAyC,EAAE,QAAiB;QAC1E,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,GAAG,GAAG,CAAC,KAAoB,EAAQ,EAAE;YACzC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YACjC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;YACjD,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,CAAC,CAAC;QACF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,YAAY,QAAQ;gBAAE,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;oBAAE,GAAG,CAAC,OAAO,CAAC,CAAC;;gBACpF,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAC5F,CAAC;CACF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currencies. The ISO 4217 list ships with the package, so a currency's number
|
|
3
|
+
* of decimals is the same on every runtime and device. `Intl` is only used for
|
|
4
|
+
* display (see `@arki/money/format`); it accepts made-up codes such as `ZZZ`
|
|
5
|
+
* and its decimals can differ from ISO, so it never decides arithmetic.
|
|
6
|
+
*
|
|
7
|
+
* `Currency.of('EUR')` returns the same object every time, so comparing two
|
|
8
|
+
* currencies is a pointer check on the hot path.
|
|
9
|
+
*/
|
|
10
|
+
export { ISO_4217_VERSION } from './data/iso4217.js';
|
|
11
|
+
/** A currency, or the code of an ISO 4217 currency. */
|
|
12
|
+
export type CurrencyLike<C extends string = string> = C | Currency<C>;
|
|
13
|
+
/** What `Currency.custom` needs. */
|
|
14
|
+
export type CurrencyDefinition<C extends string = string> = {
|
|
15
|
+
/** Letters, digits and `. _ : -`, up to 32 characters, not an ISO 4217 code: `'BTC'`, `'crypto:ETH'`, `'points'`. */
|
|
16
|
+
code: C;
|
|
17
|
+
/** Number of decimals, 0–100: 8 for BTC, 18 for ETH, 0 for loyalty points. */
|
|
18
|
+
scale: number;
|
|
19
|
+
/** Optional three-digit numeric code. */
|
|
20
|
+
numeric?: string;
|
|
21
|
+
};
|
|
22
|
+
/** An immutable currency: its code, its ISO numeric code and its number of decimals. */
|
|
23
|
+
export declare class Currency<C extends string = string> {
|
|
24
|
+
/** `'EUR'`. */
|
|
25
|
+
readonly code: C;
|
|
26
|
+
/** Three-digit ISO numeric code as a string, so `'008'` keeps its zeros. `undefined` for most custom currencies. */
|
|
27
|
+
readonly numeric: string | undefined;
|
|
28
|
+
/** Number of decimals: 2 for EUR, 0 for JPY, 3 for KWD. ISO codes without minor units (XAU, XTS) use 0. */
|
|
29
|
+
readonly scale: number;
|
|
30
|
+
/** `true` for a currency from the shipped ISO 4217 table. */
|
|
31
|
+
readonly iso: boolean;
|
|
32
|
+
private constructor();
|
|
33
|
+
/**
|
|
34
|
+
* The ISO 4217 currency with this upper-case code: `Currency.of('EUR')`.
|
|
35
|
+
* Throws `UnknownCurrencyError` for anything not in the table, including `'eur'`.
|
|
36
|
+
*/
|
|
37
|
+
static of<const C extends string>(code: C): Currency<C>;
|
|
38
|
+
/** The ISO 4217 currency with this numeric code: `Currency.fromNumeric('978')` or `(978)` → EUR. */
|
|
39
|
+
static fromNumeric(numeric: string | number): Currency;
|
|
40
|
+
/**
|
|
41
|
+
* A currency that is not in ISO 4217: crypto, loyalty points, a withdrawn
|
|
42
|
+
* currency you still hold data in. It is not registered anywhere; keep the
|
|
43
|
+
* object and pass it where you would pass a code. Two custom currencies with
|
|
44
|
+
* the same code and scale are treated as the same currency.
|
|
45
|
+
*/
|
|
46
|
+
static custom<const C extends string>(definition: CurrencyDefinition<C>): Currency<C>;
|
|
47
|
+
/** `true` when `code` is in the shipped ISO 4217 table. */
|
|
48
|
+
static isKnown(code: string): boolean;
|
|
49
|
+
/** Every ISO 4217 code in the shipped table, sorted. */
|
|
50
|
+
static codes(): string[];
|
|
51
|
+
/** Same code and same scale. */
|
|
52
|
+
eq(other: Currency): boolean;
|
|
53
|
+
toString(): C;
|
|
54
|
+
toJSON(): C;
|
|
55
|
+
/** Decodes the packed table on first use. */
|
|
56
|
+
private static table;
|
|
57
|
+
}
|
|
58
|
+
/** Accepts a `Currency` or an ISO code. */
|
|
59
|
+
export declare function toCurrency<C extends string>(currency: CurrencyLike<C>): Currency<C>;
|
|
60
|
+
/** Finds a currency by code among `custom`, then in the ISO table. */
|
|
61
|
+
export declare function resolveCurrency(code: string, custom: readonly Currency[] | undefined): Currency;
|
|
62
|
+
/** Throws `CurrencyMismatchError` unless the two are the same currency. */
|
|
63
|
+
export declare function assertSameCurrency(a: Currency, b: Currency): void;
|
|
64
|
+
//# sourceMappingURL=currency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"currency.d.ts","sourceRoot":"","sources":["../src/currency.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAQrD,uDAAuD;AACvD,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEtE,oCAAoC;AACpC,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC1D,qHAAqH;IACrH,IAAI,EAAE,CAAC,CAAC;IACR,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wFAAwF;AACxF,qBAAa,QAAQ,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM;IAC7C,eAAe;IACf,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,oHAAoH;IACpH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,2GAA2G;IAC3G,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IAEtB,OAAO,eAON;IAED;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAItD;IAED,oGAAoG;IACpG,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAWrD;IAED;;;;;OAKG;IACH,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,UAAU,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAiBpF;IAED,2DAA2D;IAC3D,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpC;IAED,wDAAwD;IACxD,MAAM,CAAC,KAAK,IAAI,MAAM,EAAE,CAEvB;IAED,gCAAgC;IAChC,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAE3B;IAED,QAAQ,IAAI,CAAC,CAEZ;IAED,MAAM,IAAI,CAAC,CAEV;IAUD,6CAA6C;IAC7C,OAAO,CAAC,MAAM,CAAC,KAAK;CAWrB;AAMD,2CAA2C;AAC3C,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAInF;AAED,sEAAsE;AACtE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,QAAQ,EAAE,GAAG,SAAS,GAAG,QAAQ,CAK/F;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAEjE"}
|