@vielzeug/coins 1.0.5 → 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.
Files changed (80) hide show
  1. package/README.md +50 -222
  2. package/dist/aggregate.cjs +2 -0
  3. package/dist/aggregate.cjs.map +1 -0
  4. package/dist/aggregate.d.ts +11 -0
  5. package/dist/aggregate.d.ts.map +1 -0
  6. package/dist/aggregate.js +44 -0
  7. package/dist/aggregate.js.map +1 -0
  8. package/dist/coins.cjs +1 -1
  9. package/dist/coins.cjs.map +1 -1
  10. package/dist/coins.iife.js +1 -1
  11. package/dist/coins.iife.js.map +1 -1
  12. package/dist/coins.js +1 -1
  13. package/dist/coins.js.map +1 -1
  14. package/dist/currency.cjs +2 -0
  15. package/dist/currency.cjs.map +1 -0
  16. package/dist/currency.d.ts +38 -0
  17. package/dist/currency.d.ts.map +1 -0
  18. package/dist/currency.js +55 -0
  19. package/dist/currency.js.map +1 -0
  20. package/dist/decimal.cjs +2 -0
  21. package/dist/decimal.cjs.map +1 -0
  22. package/dist/decimal.d.ts +7 -0
  23. package/dist/decimal.d.ts.map +1 -0
  24. package/dist/decimal.js +52 -0
  25. package/dist/decimal.js.map +1 -0
  26. package/dist/errors.cjs +1 -1
  27. package/dist/errors.cjs.map +1 -1
  28. package/dist/errors.d.ts +6 -42
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/errors.js +7 -6
  31. package/dist/errors.js.map +1 -1
  32. package/dist/exchange.cjs +1 -1
  33. package/dist/exchange.cjs.map +1 -1
  34. package/dist/exchange.d.ts +9 -27
  35. package/dist/exchange.d.ts.map +1 -1
  36. package/dist/exchange.js +24 -12
  37. package/dist/exchange.js.map +1 -1
  38. package/dist/format.cjs +1 -1
  39. package/dist/format.cjs.map +1 -1
  40. package/dist/format.d.ts +2 -39
  41. package/dist/format.d.ts.map +1 -1
  42. package/dist/format.js +57 -94
  43. package/dist/format.js.map +1 -1
  44. package/dist/index.cjs +1 -1
  45. package/dist/index.d.ts +8 -4
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -5
  48. package/dist/money.cjs +1 -1
  49. package/dist/money.cjs.map +1 -1
  50. package/dist/money.d.ts +26 -245
  51. package/dist/money.d.ts.map +1 -1
  52. package/dist/money.js +71 -184
  53. package/dist/money.js.map +1 -1
  54. package/dist/serialization.cjs +2 -0
  55. package/dist/serialization.cjs.map +1 -0
  56. package/dist/serialization.d.ts +6 -0
  57. package/dist/serialization.d.ts.map +1 -0
  58. package/dist/serialization.js +45 -0
  59. package/dist/serialization.js.map +1 -0
  60. package/dist/types.d.ts +35 -52
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +3 -3
  63. package/dist/_cache.cjs +0 -2
  64. package/dist/_cache.cjs.map +0 -1
  65. package/dist/_cache.d.ts +0 -6
  66. package/dist/_cache.d.ts.map +0 -1
  67. package/dist/_cache.js +0 -14
  68. package/dist/_cache.js.map +0 -1
  69. package/dist/_dev.cjs +0 -2
  70. package/dist/_dev.cjs.map +0 -1
  71. package/dist/_dev.d.ts +0 -2
  72. package/dist/_dev.d.ts.map +0 -1
  73. package/dist/_dev.js +0 -9
  74. package/dist/_dev.js.map +0 -1
  75. package/dist/utils.cjs +0 -2
  76. package/dist/utils.cjs.map +0 -1
  77. package/dist/utils.d.ts +0 -66
  78. package/dist/utils.d.ts.map +0 -1
  79. package/dist/utils.js +0 -66
  80. package/dist/utils.js.map +0 -1
package/README.md CHANGED
@@ -1,262 +1,90 @@
1
1
  # @vielzeug/coins
2
2
 
3
- > Zero-dependency TypeScript utilities for precise monetary arithmetic
3
+ Exact monetary arithmetic for TypeScript. Coins stores bigint minor units, uses explicit currency definitions, and accepts decimal strings at every exact-input boundary.
4
4
 
5
- Bigint-based money creation, arithmetic, allocation, formatting, exchange rate conversion, and serialization — with no floating-point rounding errors.
6
-
7
- ## Why Coins?
8
-
9
- Float arithmetic silently loses precision:
10
-
11
- ```ts
12
- // number (float)
13
- 10.1 + 10.2; // 20.299999999999997
14
-
15
- // @vielzeug/coins (bigint)
16
- import { add, money } from '@vielzeug/coins';
17
- add(money('10.10', 'USD'), money('10.20', 'USD')); // { amount: 2030n, currency: 'USD' } — exact
18
- ```
19
-
20
- Coins stores every value as bigint **minor units** (cents for USD, whole units for JPY, fils for KWD), so arithmetic and allocation are always lossless.
21
-
22
- ## Installation
5
+ ## Install
23
6
 
24
7
  ```sh
25
8
  pnpm add @vielzeug/coins
26
9
  ```
27
10
 
28
- ## Quick Start
29
-
30
- ```ts
31
- import { add, allocate, exchange, format, money, multiply } from '@vielzeug/coins';
32
- import type { ExchangeRate, Money } from '@vielzeug/coins';
33
-
34
- // Create money from a decimal string (lossless) or bigint minor units
35
- const price: Money = money('19.99', 'USD'); // { amount: 1999n, currency: 'USD' }
36
- const tax: Money = money('1.60', 'USD');
37
- const total: Money = add(price, tax); // { amount: 3559n, currency: 'USD' }
38
-
39
- // Arithmetic with explicit rounding mode
40
- multiply(total, '1.1'); // $39.15 (half-away-from-zero, default)
41
- multiply(total, '1.1', 'floor'); // $39.14 (explicit mode)
42
-
43
- // Lossless allocation — no minor unit is ever lost or gained
44
- allocate(money('10.00', 'USD'), [1, 1, 1]); // [$3.34, $3.33, $3.33]
45
-
46
- // Locale-aware formatting
47
- format(total); // '$35.59'
48
- format(total, { locale: 'de-DE' }); // '35,59 $'
49
- format(total, { style: 'code' }); // 'USD 35.59'
50
-
51
- // Currency exchange — ExchangeRate.from/to are plain strings; rate is a decimal string or number
52
- const rate: ExchangeRate = { from: 'USD', rate: '0.92', to: 'EUR' };
53
- exchange(total, rate); // { amount: 3274n, currency: 'EUR' }
54
- ```
55
-
56
- ## API
57
-
58
- ### `money(amount, currency)`
59
-
60
- Creates a `Money` value. Validates the currency code against `Intl.NumberFormat`; throws `InvalidCurrencyError` for unrecognised ISO 4217 codes.
61
-
62
- | `amount` type | Behaviour |
63
- | -------------------- | ---------------------------------------------------------------------- |
64
- | `string` `'1234.56'` | Parsed losslessly; excess digits rounded half-away-from-zero |
65
- | `number` `1234.56` | Converted via `String()` first — IEEE-754 limits apply; prefer strings |
66
- | `bigint` `123456n` | Used as-is (already in minor units) |
11
+ ## Start Here
67
12
 
68
13
  ```ts
69
- money('1234.56', 'USD'); // { amount: 123456n, currency: 'USD' }
70
- money(0n, 'USD'); // { amount: 0n, currency: 'USD' } — zero accumulator
71
- money('1.00', 'FAKE'); // throws InvalidCurrencyError
72
- ```
73
-
74
- ### Arithmetic
14
+ import { USD, add, money, toDecimal } from '@vielzeug/coins';
75
15
 
76
- All binary functions (`add`, `subtract`) throw `CurrencyMismatchError` on currency mismatch.
16
+ const subtotal = add(money('12.50', USD), money('7.25', USD));
77
17
 
78
- ```ts
79
- add(a, b); // → Money
80
- subtract(a, b); // → Money
81
- multiply(money, factor, mode?); // → Money (factor: number | string)
82
- divide(money, divisor, mode?); // → Money (throws RangeError on division by zero)
83
- abs(money); // → Money (absolute value)
84
- negate(money); // → Money (sign flip)
18
+ console.log(toDecimal(subtotal)); // '19.75'
85
19
  ```
86
20
 
87
- ### Allocation
88
-
89
- Uses the **Largest Remainder Method** — every output sums exactly to the input, no minor unit is ever lost or gained.
90
-
91
- ```ts
92
- allocate(money, ratios); // → [Money, ...Money[]] (weighted; use strings for lossless decimal ratios)
93
- splitEvenly(money, parts); // → [Money, ...Money[]] (equal shares; sugar over allocate)
94
- ```
95
-
96
- Both throw `RangeError` on empty ratios, negative ratios, or non-positive parts.
97
-
98
- ### Aggregates
99
-
100
- ```ts
101
- sum(moneys); // → Money (throws RangeError if empty; CurrencyMismatchError on mismatch)
102
- min(moneys); // → Money (non-empty array; CurrencyMismatchError on mismatch)
103
- max(moneys); // → Money (non-empty array; CurrencyMismatchError on mismatch)
104
- clamp(m, lower, upper); // → Money (CurrencyMismatchError on mismatch; RangeError if lower > upper)
105
- ```
106
-
107
- ### Comparison
108
-
109
- `isEqual` returns `false` on currency mismatch (safe for `.filter()`). All other comparison functions throw `CurrencyMismatchError` on mismatch.
21
+ `money()` has one common path and one explicit low-level path:
110
22
 
111
23
  ```ts
112
- compare(a, b); // -1 | 0 | 1
113
- isEqual(a, b); // boolean (false on currency mismatch — no throw)
114
- greaterThan(a, b); // boolean
115
- greaterThanOrEqual(a, b); // → boolean
116
- lessThan(a, b); // → boolean
117
- lessThanOrEqual(a, b); // → boolean
118
- isZero(m); // → boolean
119
- isPositive(m); // → boolean
120
- isNegative(m); // → boolean
121
- isNonNegative(m); // → boolean (>= 0)
122
- isNonPositive(m); // → boolean (<= 0)
24
+ money('19.99', USD); // exact decimal major units
25
+ money('19.999', USD, { rounding: 'halfEven' });
26
+ money(1999n, USD, { unit: 'minor' }); // stored minor units
123
27
  ```
124
28
 
125
- ### Serialization
29
+ ## Core API
126
30
 
127
31
  ```ts
128
- toDecimal(money); // → string e.g. '1234.56' (round-trips losslessly with money())
129
- toNumber(money); // → number e.g. 1234.56 (lossy — display/charting only)
130
- toJSON(money); // → MoneyJSON { amount: '123456', currency: 'USD' }
131
- fromJSON(json); // → Money (validates currency; throws on invalid input)
32
+ import {
33
+ EUR,
34
+ USD,
35
+ allocate,
36
+ decimal,
37
+ exchange,
38
+ exchangeRate,
39
+ format,
40
+ money,
41
+ multiply,
42
+ sum,
43
+ toDecimal,
44
+ } from '@vielzeug/coins';
45
+
46
+ const items = [money('12.50', USD), money('7.25', USD)];
47
+ const subtotal = sum(items, { currency: USD });
48
+ const total = multiply(subtotal, '1.08', { rounding: 'halfEven' });
49
+ const shares = allocate(total, 3);
50
+
51
+ const usdToEur = exchangeRate({ from: USD, to: EUR, value: '0.9234' });
52
+ const eurTotal = exchange(total, usdToEur, { rounding: 'halfEven' });
53
+
54
+ console.log(format(eurTotal, { locale: 'de-DE' }));
55
+ console.log(shares.map(toDecimal));
56
+ console.log(decimal('1.075'));
132
57
  ```
133
58
 
134
- `bigint` cannot be serialized by `JSON.stringify`. Use `JSON.stringify(toJSON(price))` and `fromJSON(JSON.parse(raw))`.
59
+ ## Currency Definitions
135
60
 
136
- ### `roundTo(money, places, mode?)`
61
+ Built-in definitions: `USD`, `EUR`, `GBP`, `JPY`, `KRW`, `BHD`, `KWD`.
137
62
 
138
- Rounds a `Money` value to fewer decimal places than the currency default useful for display (whole dollars, chart axes). `places` must be in `0..currencyDecimals`.
63
+ Define a business currency explicitly when required:
139
64
 
140
65
  ```ts
141
- roundTo(money('1234.56', 'USD'), 0); // { amount: 1235n, currency: 'USD' } ($1,235)
142
- roundTo(money('1234.56', 'USD'), 1, 'floor'); // { amount: 12345n, currency: 'USD' } ($1,234.5)
143
- ```
144
-
145
- ### `format(money, options?)`
146
-
147
- Formats a `Money` value as a locale-aware currency string. Uses bigint arithmetic throughout — no floating-point precision loss.
66
+ import { defineCurrency, money } from '@vielzeug/coins';
148
67
 
149
- | Option | Type | Default | Description |
150
- | ----------------------- | ------------------------------------------------- | ---------------- | ---------------------------- |
151
- | `locale` | `string` | `'en-US'` | BCP 47 language tag |
152
- | `style` | `'symbol' \| 'code' \| 'name' \| 'narrowSymbol'` | `'symbol'` | Display style |
153
- | `minimumFractionDigits` | `number` | currency default | Minimum decimal places shown |
154
- | `maximumFractionDigits` | `number` | currency default | Maximum decimal places shown |
155
-
156
- ```ts
157
- format(money('1234.56', 'USD')); // '$1,234.56'
158
- format(money('1234.56', 'USD'), { locale: 'de-DE' }); // '1.234,56 $'
159
- format(money('1234.56', 'USD'), { style: 'code' }); // 'USD 1,234.56'
68
+ const POINTS = defineCurrency({ code: 'PTS', minorUnit: 0 });
69
+ const balance = money('250', POINTS);
160
70
  ```
161
71
 
162
- ### `formatParts(money, options?)`
72
+ ## Errors
163
73
 
164
- Same options as `format()`. Returns a `MoneyFormatPart[]` array of semantic segments — useful for applying separate styles to symbol, integer, decimal, and fraction parts.
165
-
166
- Joining all `value` fields produces the same string as `format()`.
167
-
168
- ### `exchange(money, rate, mode?)`
169
-
170
- Converts a `Money` value using an `ExchangeRate`. `ExchangeRate.from` and `.to` are plain strings; `rate.rate` is a decimal string or number (`number | string`, for symmetry with `multiply()`/`divide()` — prefer a string for lossless bigint arithmetic). Throws `CurrencyMismatchError` if `money.currency !== rate.from`.
74
+ All failures extend `CoinsError` and expose `code`.
171
75
 
172
76
  ```ts
173
- import type { ExchangeRate } from '@vielzeug/coins';
174
-
175
- const rate: ExchangeRate = { from: 'USD', rate: '0.92', to: 'EUR' };
176
- exchange(money('100.00', 'USD'), rate); // { amount: 9200n, currency: 'EUR' }
177
- exchange(money('100.00', 'USD'), rate, 'floor'); // explicit rounding mode
178
- ```
179
-
180
- ### Typed Errors
181
-
182
- ```ts
183
- import { CurrencyMismatchError, InvalidCurrencyError } from '@vielzeug/coins';
184
-
185
- try {
186
- add(money('1.00', 'USD'), money('1.00', 'EUR'));
187
- } catch (e) {
188
- if (e instanceof CurrencyMismatchError) {
189
- console.log(e.expected, e.received); // 'USD' 'EUR'
190
- }
191
- }
77
+ import { CoinsError, money, USD } from '@vielzeug/coins';
192
78
 
193
79
  try {
194
- money('1.00', 'FAKE');
195
- } catch (e) {
196
- if (e instanceof InvalidCurrencyError) {
197
- console.log(e.code); // 'FAKE'
80
+ money(100n, USD);
81
+ } catch (error) {
82
+ if (error instanceof CoinsError && error.code === 'INVALID_MONEY') {
83
+ // Bigint construction requires { unit: 'minor' }.
198
84
  }
199
85
  }
200
86
  ```
201
87
 
202
- - `CurrencyMismatchError` extends `TypeError` — existing `instanceof TypeError` catch blocks still work.
203
- - `InvalidCurrencyError` extends `RangeError` — existing `instanceof RangeError` catch blocks still work.
204
-
205
- ### Rounding Modes
206
-
207
- Used by `multiply`, `divide`, `exchange`, and `roundTo`.
208
-
209
- | Mode | Description |
210
- | ----------------------- | ---------------------------------------------- |
211
- | `'half-away-from-zero'` | Round half away from zero **(default)** |
212
- | `'half-even'` | Banker's rounding — minimises cumulative error |
213
- | `'down'` | Truncate toward zero |
214
- | `'up'` | Away from zero |
215
- | `'floor'` | Toward −∞ |
216
- | `'ceiling'` | Toward +∞ |
217
-
218
- ### Key Types
219
-
220
- ```ts
221
- type Money = {
222
- readonly amount: bigint; // minor units (cents for USD, whole units for JPY)
223
- readonly currency: string; // validated ISO 4217 code
224
- };
225
-
226
- type ExchangeRate = {
227
- readonly from: string; // source currency code
228
- readonly rate: number | string; // decimal multiplier, e.g. '0.92' or 0.92
229
- readonly to: string; // target currency code
230
- };
231
-
232
- type RoundingMode = 'ceiling' | 'down' | 'floor' | 'half-away-from-zero' | 'half-even' | 'up';
233
-
234
- type FormatOptions = {
235
- locale?: string;
236
- maximumFractionDigits?: number;
237
- minimumFractionDigits?: number;
238
- style?: 'code' | 'name' | 'narrowSymbol' | 'symbol';
239
- };
240
-
241
- type MoneyFormatPart = {
242
- type: 'currency' | 'decimal' | 'fraction' | 'integer' | 'literal' | 'minusSign';
243
- value: string;
244
- };
245
-
246
- type MoneyJSON = { amount: string; currency: string };
247
- ```
248
-
249
- ## Documentation
250
-
251
- - **[Full Guide](https://vielzeug.dev/coins/)** — Overview, concepts, quick start
252
- - **[Usage Guide](https://vielzeug.dev/coins/usage/)** — Common patterns and best practices
253
- - **[API Reference](https://vielzeug.dev/coins/api/)** — All types and function signatures
254
- - **[Examples](https://vielzeug.dev/coins/examples/)** — Real-world integration examples
255
-
256
- ## TypeScript
257
-
258
- Requires TypeScript 5.0+ with `strict: true`.
259
-
260
88
  ## License
261
89
 
262
- MIT
90
+ MIT © Helmuth Saatkamp
@@ -0,0 +1,2 @@
1
+ const e=require("./errors.cjs"),t=require("./decimal.cjs"),n=require("./money.cjs");function r(t,r){let i=0n;for(let a of t){if(!n.isMoney(a))throw new e.CoinsError(`INVALID_MONEY`,`sum() requires canonical money values`);if(a.currency!==r.currency)throw new e.CurrencyMismatchError(r.currency.code,a.currency.code);i+=a.amount}return n.withMinor(i,r.currency)}function i(r,i){if(!n.isMoney(r))throw new e.CoinsError(`INVALID_MONEY`,`allocate() requires canonical money`);if(typeof i==`number`)return o(r,i);if(i.length===0)throw new e.CoinsError(`INVALID_ALLOCATION`,`Allocation weights cannot be empty`);let a=i.map(t.decimal);if(a.some(e=>e.numerator<0n))throw new e.CoinsError(`INVALID_ALLOCATION`,`Allocation weights cannot be negative`);let s=a.reduce((e,n)=>t.lcm(e,n.denominator),1n),c=a.map(e=>e.numerator*(s/e.denominator)),l=c.reduce((e,t)=>e+t,0n);if(l===0n)throw new e.CoinsError(`INVALID_ALLOCATION`,`Allocation weights must include a positive value`);let u=r.amount<0n?-1n:1n,d=r.amount<0n?-r.amount:r.amount,f=c.map(e=>d*e/l),p=d-f.reduce((e,t)=>e+t,0n),m=c.map((e,t)=>({index:t,remainder:d*e%l})).sort((e,t)=>e.remainder===t.remainder?e.index-t.index:e.remainder>t.remainder?-1:1);for(let e of m){if(p===0n)break;f[e.index]+=1n,p-=1n}return f.map(e=>n.withMinor(e*u,r.currency))}function a(t,r){if(n.compare(r.min,r.max)===1)throw new e.CoinsError(`INVALID_ALLOCATION`,`Clamp minimum cannot exceed maximum`);return n.compare(t,r.min)===-1?r.min:n.compare(t,r.max)===1?r.max:t}function o(t,r){if(!Number.isInteger(r)||r<1)throw new e.CoinsError(`INVALID_ALLOCATION`,`Allocation count must be a positive integer`);let i=BigInt(r),a=t.amount<0n?-1n:1n,o=t.amount<0n?-t.amount:t.amount,s=o/i,c=o%i;return Array.from({length:r},(e,r)=>n.withMinor((s+(BigInt(r)<c?1n:0n))*a,t.currency))}exports.allocate=i,exports.clamp=a,exports.sum=r;
2
+ //# sourceMappingURL=aggregate.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.cjs","names":[],"sources":["../src/aggregate.ts"],"sourcesContent":["import type { Currency, Money } from './types';\n\nimport { decimal, lcm } from './decimal';\nimport { CoinsError, CurrencyMismatchError } from './errors';\nimport { compare, isMoney, withMinor } from './money';\n\nexport function sum<C extends Currency>(values: Iterable<Money<C>>, options: { currency: C }): Money<C> {\n let amount = 0n;\n\n for (const value of values) {\n if (!isMoney(value)) throw new CoinsError('INVALID_MONEY', 'sum() requires canonical money values');\n\n if (value.currency !== options.currency)\n throw new CurrencyMismatchError(options.currency.code, value.currency.code);\n\n amount += value.amount;\n }\n\n return withMinor(amount, options.currency);\n}\n\nexport function allocate<C extends Currency>(value: Money<C>, count: number): Money<C>[];\nexport function allocate<C extends Currency>(value: Money<C>, weights: readonly string[]): Money<C>[];\nexport function allocate<C extends Currency>(value: Money<C>, weightsOrCount: number | readonly string[]): Money<C>[] {\n if (!isMoney(value)) throw new CoinsError('INVALID_MONEY', 'allocate() requires canonical money');\n\n if (typeof weightsOrCount === 'number') return allocateEvenly(value, weightsOrCount);\n\n if (weightsOrCount.length === 0) throw new CoinsError('INVALID_ALLOCATION', 'Allocation weights cannot be empty');\n\n const weights = weightsOrCount.map(decimal);\n\n if (weights.some((weight) => weight.numerator < 0n)) {\n throw new CoinsError('INVALID_ALLOCATION', 'Allocation weights cannot be negative');\n }\n\n const commonDenominator = weights.reduce((result, weight) => lcm(result, weight.denominator), 1n);\n const scaledWeights = weights.map((weight) => weight.numerator * (commonDenominator / weight.denominator));\n const totalWeight = scaledWeights.reduce((result, weight) => result + weight, 0n);\n\n if (totalWeight === 0n)\n throw new CoinsError('INVALID_ALLOCATION', 'Allocation weights must include a positive value');\n\n const sign = value.amount < 0n ? -1n : 1n;\n const absolute = value.amount < 0n ? -value.amount : value.amount;\n const shares = scaledWeights.map((weight) => (absolute * weight) / totalWeight);\n let remainder = absolute - shares.reduce((result, amount) => result + amount, 0n);\n const ranked = scaledWeights\n .map((weight, index) => ({ index, remainder: (absolute * weight) % totalWeight }))\n .sort((left, right) =>\n left.remainder === right.remainder ? left.index - right.index : left.remainder > right.remainder ? -1 : 1,\n );\n\n for (const entry of ranked) {\n if (remainder === 0n) break;\n\n shares[entry.index]! += 1n;\n remainder -= 1n;\n }\n\n return shares.map((amount) => withMinor(amount * sign, value.currency));\n}\n\nexport function clamp<C extends Currency>(\n value: Money<C>,\n options: { max: Money<NoInfer<C>>; min: Money<NoInfer<C>> },\n): Money<C> {\n if (compare(options.min, options.max) === 1) {\n throw new CoinsError('INVALID_ALLOCATION', 'Clamp minimum cannot exceed maximum');\n }\n\n return compare(value, options.min) === -1 ? options.min : compare(value, options.max) === 1 ? options.max : value;\n}\n\nfunction allocateEvenly<C extends Currency>(value: Money<C>, countValue: number): Money<C>[] {\n if (!Number.isInteger(countValue) || countValue < 1) {\n throw new CoinsError('INVALID_ALLOCATION', 'Allocation count must be a positive integer');\n }\n\n const count = BigInt(countValue);\n const sign = value.amount < 0n ? -1n : 1n;\n const absolute = value.amount < 0n ? -value.amount : value.amount;\n const base = absolute / count;\n const remainder = absolute % count;\n\n return Array.from({ length: countValue }, (_, index) =>\n withMinor((base + (BigInt(index) < remainder ? 1n : 0n)) * sign, value.currency),\n );\n}\n"],"mappings":"oFAMA,SAAgB,EAAwB,EAA4B,EAAoC,CACtG,IAAI,EAAS,GAEb,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,CAAC,EAAA,QAAQ,CAAK,EAAG,MAAM,IAAI,EAAA,WAAW,gBAAiB,uCAAuC,EAElG,GAAI,EAAM,WAAa,EAAQ,SAC7B,MAAM,IAAI,EAAA,sBAAsB,EAAQ,SAAS,KAAM,EAAM,SAAS,IAAI,EAE5E,GAAU,EAAM,MAClB,CAEA,OAAO,EAAA,UAAU,EAAQ,EAAQ,QAAQ,CAC3C,CAIA,SAAgB,EAA6B,EAAiB,EAAwD,CACpH,GAAI,CAAC,EAAA,QAAQ,CAAK,EAAG,MAAM,IAAI,EAAA,WAAW,gBAAiB,qCAAqC,EAEhG,GAAI,OAAO,GAAmB,SAAU,OAAO,EAAe,EAAO,CAAc,EAEnF,GAAI,EAAe,SAAW,EAAG,MAAM,IAAI,EAAA,WAAW,qBAAsB,oCAAoC,EAEhH,IAAM,EAAU,EAAe,IAAI,EAAA,OAAO,EAE1C,GAAI,EAAQ,KAAM,GAAW,EAAO,UAAY,EAAE,EAChD,MAAM,IAAI,EAAA,WAAW,qBAAsB,uCAAuC,EAGpF,IAAM,EAAoB,EAAQ,QAAQ,EAAQ,IAAW,EAAA,IAAI,EAAQ,EAAO,WAAW,EAAG,EAAE,EAC1F,EAAgB,EAAQ,IAAK,GAAW,EAAO,WAAa,EAAoB,EAAO,YAAY,EACnG,EAAc,EAAc,QAAQ,EAAQ,IAAW,EAAS,EAAQ,EAAE,EAEhF,GAAI,IAAgB,GAClB,MAAM,IAAI,EAAA,WAAW,qBAAsB,kDAAkD,EAE/F,IAAM,EAAO,EAAM,OAAS,GAAK,CAAC,GAAK,GACjC,EAAW,EAAM,OAAS,GAAK,CAAC,EAAM,OAAS,EAAM,OACrD,EAAS,EAAc,IAAK,GAAY,EAAW,EAAU,CAAW,EAC1E,EAAY,EAAW,EAAO,QAAQ,EAAQ,IAAW,EAAS,EAAQ,EAAE,EAC1E,EAAS,EACZ,KAAK,EAAQ,KAAW,CAAE,QAAO,UAAY,EAAW,EAAU,CAAY,EAAE,CAAC,CACjF,MAAM,EAAM,IACX,EAAK,YAAc,EAAM,UAAY,EAAK,MAAQ,EAAM,MAAQ,EAAK,UAAY,EAAM,UAAY,GAAK,CAC1G,EAEF,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,IAAc,GAAI,MAEtB,EAAO,EAAM,QAAW,GACxB,GAAa,EACf,CAEA,OAAO,EAAO,IAAK,GAAW,EAAA,UAAU,EAAS,EAAM,EAAM,QAAQ,CAAC,CACxE,CAEA,SAAgB,EACd,EACA,EACU,CACV,GAAI,EAAA,QAAQ,EAAQ,IAAK,EAAQ,GAAG,IAAM,EACxC,MAAM,IAAI,EAAA,WAAW,qBAAsB,qCAAqC,EAGlF,OAAO,EAAA,QAAQ,EAAO,EAAQ,GAAG,IAAM,GAAK,EAAQ,IAAM,EAAA,QAAQ,EAAO,EAAQ,GAAG,IAAM,EAAI,EAAQ,IAAM,CAC9G,CAEA,SAAS,EAAmC,EAAiB,EAAgC,CAC3F,GAAI,CAAC,OAAO,UAAU,CAAU,GAAK,EAAa,EAChD,MAAM,IAAI,EAAA,WAAW,qBAAsB,6CAA6C,EAG1F,IAAM,EAAQ,OAAO,CAAU,EACzB,EAAO,EAAM,OAAS,GAAK,CAAC,GAAK,GACjC,EAAW,EAAM,OAAS,GAAK,CAAC,EAAM,OAAS,EAAM,OACrD,EAAO,EAAW,EAClB,EAAY,EAAW,EAE7B,OAAO,MAAM,KAAK,CAAE,OAAQ,CAAW,GAAI,EAAG,IAC5C,EAAA,WAAW,GAAQ,OAAO,CAAK,EAAI,EAAY,GAAK,KAAO,EAAM,EAAM,QAAQ,CACjF,CACF"}
@@ -0,0 +1,11 @@
1
+ import type { Currency, Money } from './types';
2
+ export declare function sum<C extends Currency>(values: Iterable<Money<C>>, options: {
3
+ currency: C;
4
+ }): Money<C>;
5
+ export declare function allocate<C extends Currency>(value: Money<C>, count: number): Money<C>[];
6
+ export declare function allocate<C extends Currency>(value: Money<C>, weights: readonly string[]): Money<C>[];
7
+ export declare function clamp<C extends Currency>(value: Money<C>, options: {
8
+ max: Money<NoInfer<C>>;
9
+ min: Money<NoInfer<C>>;
10
+ }): Money<C>;
11
+ //# sourceMappingURL=aggregate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAM/C,wBAAgB,GAAG,CAAC,CAAC,SAAS,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE;IAAE,QAAQ,EAAE,CAAC,CAAA;CAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAatG;AAED,wBAAgB,QAAQ,CAAC,CAAC,SAAS,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AACzF,wBAAgB,QAAQ,CAAC,CAAC,SAAS,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAyCtG,wBAAgB,KAAK,CAAC,CAAC,SAAS,QAAQ,EACtC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EACf,OAAO,EAAE;IAAE,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAAC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;CAAE,GAC1D,KAAK,CAAC,CAAC,CAAC,CAMV"}
@@ -0,0 +1,44 @@
1
+ import { CoinsError as e, CurrencyMismatchError as t } from "./errors.js";
2
+ import { decimal as n, lcm as r } from "./decimal.js";
3
+ import { compare as i, isMoney as a, withMinor as o } from "./money.js";
4
+ //#region src/aggregate.ts
5
+ function s(n, r) {
6
+ let i = 0n;
7
+ for (let o of n) {
8
+ if (!a(o)) throw new e("INVALID_MONEY", "sum() requires canonical money values");
9
+ if (o.currency !== r.currency) throw new t(r.currency.code, o.currency.code);
10
+ i += o.amount;
11
+ }
12
+ return o(i, r.currency);
13
+ }
14
+ function c(t, i) {
15
+ if (!a(t)) throw new e("INVALID_MONEY", "allocate() requires canonical money");
16
+ if (typeof i == "number") return u(t, i);
17
+ if (i.length === 0) throw new e("INVALID_ALLOCATION", "Allocation weights cannot be empty");
18
+ let s = i.map(n);
19
+ if (s.some((e) => e.numerator < 0n)) throw new e("INVALID_ALLOCATION", "Allocation weights cannot be negative");
20
+ let c = s.reduce((e, t) => r(e, t.denominator), 1n), l = s.map((e) => e.numerator * (c / e.denominator)), d = l.reduce((e, t) => e + t, 0n);
21
+ if (d === 0n) throw new e("INVALID_ALLOCATION", "Allocation weights must include a positive value");
22
+ let f = t.amount < 0n ? -1n : 1n, p = t.amount < 0n ? -t.amount : t.amount, m = l.map((e) => p * e / d), h = p - m.reduce((e, t) => e + t, 0n), g = l.map((e, t) => ({
23
+ index: t,
24
+ remainder: p * e % d
25
+ })).sort((e, t) => e.remainder === t.remainder ? e.index - t.index : e.remainder > t.remainder ? -1 : 1);
26
+ for (let e of g) {
27
+ if (h === 0n) break;
28
+ m[e.index] += 1n, h -= 1n;
29
+ }
30
+ return m.map((e) => o(e * f, t.currency));
31
+ }
32
+ function l(t, n) {
33
+ if (i(n.min, n.max) === 1) throw new e("INVALID_ALLOCATION", "Clamp minimum cannot exceed maximum");
34
+ return i(t, n.min) === -1 ? n.min : i(t, n.max) === 1 ? n.max : t;
35
+ }
36
+ function u(t, n) {
37
+ if (!Number.isInteger(n) || n < 1) throw new e("INVALID_ALLOCATION", "Allocation count must be a positive integer");
38
+ let r = BigInt(n), i = t.amount < 0n ? -1n : 1n, a = t.amount < 0n ? -t.amount : t.amount, s = a / r, c = a % r;
39
+ return Array.from({ length: n }, (e, n) => o((s + (BigInt(n) < c ? 1n : 0n)) * i, t.currency));
40
+ }
41
+ //#endregion
42
+ export { c as allocate, l as clamp, s as sum };
43
+
44
+ //# sourceMappingURL=aggregate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.js","names":[],"sources":["../src/aggregate.ts"],"sourcesContent":["import type { Currency, Money } from './types';\n\nimport { decimal, lcm } from './decimal';\nimport { CoinsError, CurrencyMismatchError } from './errors';\nimport { compare, isMoney, withMinor } from './money';\n\nexport function sum<C extends Currency>(values: Iterable<Money<C>>, options: { currency: C }): Money<C> {\n let amount = 0n;\n\n for (const value of values) {\n if (!isMoney(value)) throw new CoinsError('INVALID_MONEY', 'sum() requires canonical money values');\n\n if (value.currency !== options.currency)\n throw new CurrencyMismatchError(options.currency.code, value.currency.code);\n\n amount += value.amount;\n }\n\n return withMinor(amount, options.currency);\n}\n\nexport function allocate<C extends Currency>(value: Money<C>, count: number): Money<C>[];\nexport function allocate<C extends Currency>(value: Money<C>, weights: readonly string[]): Money<C>[];\nexport function allocate<C extends Currency>(value: Money<C>, weightsOrCount: number | readonly string[]): Money<C>[] {\n if (!isMoney(value)) throw new CoinsError('INVALID_MONEY', 'allocate() requires canonical money');\n\n if (typeof weightsOrCount === 'number') return allocateEvenly(value, weightsOrCount);\n\n if (weightsOrCount.length === 0) throw new CoinsError('INVALID_ALLOCATION', 'Allocation weights cannot be empty');\n\n const weights = weightsOrCount.map(decimal);\n\n if (weights.some((weight) => weight.numerator < 0n)) {\n throw new CoinsError('INVALID_ALLOCATION', 'Allocation weights cannot be negative');\n }\n\n const commonDenominator = weights.reduce((result, weight) => lcm(result, weight.denominator), 1n);\n const scaledWeights = weights.map((weight) => weight.numerator * (commonDenominator / weight.denominator));\n const totalWeight = scaledWeights.reduce((result, weight) => result + weight, 0n);\n\n if (totalWeight === 0n)\n throw new CoinsError('INVALID_ALLOCATION', 'Allocation weights must include a positive value');\n\n const sign = value.amount < 0n ? -1n : 1n;\n const absolute = value.amount < 0n ? -value.amount : value.amount;\n const shares = scaledWeights.map((weight) => (absolute * weight) / totalWeight);\n let remainder = absolute - shares.reduce((result, amount) => result + amount, 0n);\n const ranked = scaledWeights\n .map((weight, index) => ({ index, remainder: (absolute * weight) % totalWeight }))\n .sort((left, right) =>\n left.remainder === right.remainder ? left.index - right.index : left.remainder > right.remainder ? -1 : 1,\n );\n\n for (const entry of ranked) {\n if (remainder === 0n) break;\n\n shares[entry.index]! += 1n;\n remainder -= 1n;\n }\n\n return shares.map((amount) => withMinor(amount * sign, value.currency));\n}\n\nexport function clamp<C extends Currency>(\n value: Money<C>,\n options: { max: Money<NoInfer<C>>; min: Money<NoInfer<C>> },\n): Money<C> {\n if (compare(options.min, options.max) === 1) {\n throw new CoinsError('INVALID_ALLOCATION', 'Clamp minimum cannot exceed maximum');\n }\n\n return compare(value, options.min) === -1 ? options.min : compare(value, options.max) === 1 ? options.max : value;\n}\n\nfunction allocateEvenly<C extends Currency>(value: Money<C>, countValue: number): Money<C>[] {\n if (!Number.isInteger(countValue) || countValue < 1) {\n throw new CoinsError('INVALID_ALLOCATION', 'Allocation count must be a positive integer');\n }\n\n const count = BigInt(countValue);\n const sign = value.amount < 0n ? -1n : 1n;\n const absolute = value.amount < 0n ? -value.amount : value.amount;\n const base = absolute / count;\n const remainder = absolute % count;\n\n return Array.from({ length: countValue }, (_, index) =>\n withMinor((base + (BigInt(index) < remainder ? 1n : 0n)) * sign, value.currency),\n );\n}\n"],"mappings":";;;;AAMA,SAAgB,EAAwB,GAA4B,GAAoC;CACtG,IAAI,IAAS;CAEb,KAAK,IAAM,KAAS,GAAQ;EAC1B,IAAI,CAAC,EAAQ,CAAK,GAAG,MAAM,IAAI,EAAW,iBAAiB,uCAAuC;EAElG,IAAI,EAAM,aAAa,EAAQ,UAC7B,MAAM,IAAI,EAAsB,EAAQ,SAAS,MAAM,EAAM,SAAS,IAAI;EAE5E,KAAU,EAAM;CAClB;CAEA,OAAO,EAAU,GAAQ,EAAQ,QAAQ;AAC3C;AAIA,SAAgB,EAA6B,GAAiB,GAAwD;CACpH,IAAI,CAAC,EAAQ,CAAK,GAAG,MAAM,IAAI,EAAW,iBAAiB,qCAAqC;CAEhG,IAAI,OAAO,KAAmB,UAAU,OAAO,EAAe,GAAO,CAAc;CAEnF,IAAI,EAAe,WAAW,GAAG,MAAM,IAAI,EAAW,sBAAsB,oCAAoC;CAEhH,IAAM,IAAU,EAAe,IAAI,CAAO;CAE1C,IAAI,EAAQ,MAAM,MAAW,EAAO,YAAY,EAAE,GAChD,MAAM,IAAI,EAAW,sBAAsB,uCAAuC;CAGpF,IAAM,IAAoB,EAAQ,QAAQ,GAAQ,MAAW,EAAI,GAAQ,EAAO,WAAW,GAAG,EAAE,GAC1F,IAAgB,EAAQ,KAAK,MAAW,EAAO,aAAa,IAAoB,EAAO,YAAY,GACnG,IAAc,EAAc,QAAQ,GAAQ,MAAW,IAAS,GAAQ,EAAE;CAEhF,IAAI,MAAgB,IAClB,MAAM,IAAI,EAAW,sBAAsB,kDAAkD;CAE/F,IAAM,IAAO,EAAM,SAAS,KAAK,CAAC,KAAK,IACjC,IAAW,EAAM,SAAS,KAAK,CAAC,EAAM,SAAS,EAAM,QACrD,IAAS,EAAc,KAAK,MAAY,IAAW,IAAU,CAAW,GAC1E,IAAY,IAAW,EAAO,QAAQ,GAAQ,MAAW,IAAS,GAAQ,EAAE,GAC1E,IAAS,EACZ,KAAK,GAAQ,OAAW;EAAE;EAAO,WAAY,IAAW,IAAU;CAAY,EAAE,CAAC,CACjF,MAAM,GAAM,MACX,EAAK,cAAc,EAAM,YAAY,EAAK,QAAQ,EAAM,QAAQ,EAAK,YAAY,EAAM,YAAY,KAAK,CAC1G;CAEF,KAAK,IAAM,KAAS,GAAQ;EAC1B,IAAI,MAAc,IAAI;EAGtB,AADA,EAAO,EAAM,UAAW,IACxB,KAAa;CACf;CAEA,OAAO,EAAO,KAAK,MAAW,EAAU,IAAS,GAAM,EAAM,QAAQ,CAAC;AACxE;AAEA,SAAgB,EACd,GACA,GACU;CACV,IAAI,EAAQ,EAAQ,KAAK,EAAQ,GAAG,MAAM,GACxC,MAAM,IAAI,EAAW,sBAAsB,qCAAqC;CAGlF,OAAO,EAAQ,GAAO,EAAQ,GAAG,MAAM,KAAK,EAAQ,MAAM,EAAQ,GAAO,EAAQ,GAAG,MAAM,IAAI,EAAQ,MAAM;AAC9G;AAEA,SAAS,EAAmC,GAAiB,GAAgC;CAC3F,IAAI,CAAC,OAAO,UAAU,CAAU,KAAK,IAAa,GAChD,MAAM,IAAI,EAAW,sBAAsB,6CAA6C;CAG1F,IAAM,IAAQ,OAAO,CAAU,GACzB,IAAO,EAAM,SAAS,KAAK,CAAC,KAAK,IACjC,IAAW,EAAM,SAAS,KAAK,CAAC,EAAM,SAAS,EAAM,QACrD,IAAO,IAAW,GAClB,IAAY,IAAW;CAE7B,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAW,IAAI,GAAG,MAC5C,GAAW,KAAQ,OAAO,CAAK,IAAI,IAAY,KAAK,OAAO,GAAM,EAAM,QAAQ,CACjF;AACF"}
package/dist/coins.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},t=class extends e{expected;received;constructor(e,t){super(`Currency mismatch: ${e} and ${t}`),this.expected=e,this.received=t}},n=class extends e{code;constructor(e){super(`Invalid ISO 4217 currency code: "${e}"`),this.code=e}};function r(e){let t=new Map;return{get:e=>t.get(e),set(n,r){t.size>=e&&t.delete(t.keys().next().value),t.set(n,r)}}}var i=r(512);function a(e){let t=i.get(e);if(t!==void 0)return t;let r;try{r=new Intl.NumberFormat(`en`,{currency:e,style:`currency`}).resolvedOptions().maximumFractionDigits}catch{throw new n(e)}if(r===void 0)throw new n(e);return i.set(e,r),r}function o(e){return a(e),e}function s(e){return 10n**BigInt(e)}var c=/^-?\d+(\.\d+)?$/,l=/^(-?\d+\.?\d*)[eE]([+-]?\d+)$/,u=1e3;function d(t){let n=l.exec(t),r=n[1],i=parseInt(n[2],10);if(Math.abs(i)>u)throw new e(`Scientific notation exponent too large: "${t}" (max ±${u})`);let a=r.startsWith(`-`),o=a?r.slice(1):r,s=o.indexOf(`.`),c=s===-1?o:o.replace(`.`,``),d=i-(s===-1?0:o.length-s-1),f;if(d>=0)f=c+`0`.repeat(d);else{let e=c.length+d;f=e<=0?`0.`+`0`.repeat(-e)+c:c.slice(0,e)+`.`+c.slice(e)}return a?`-${f}`:f}function f(t){let n=l.test(t)?d(t):t;if(!c.test(n))throw new e(`Invalid decimal string: "${t}"`);let r=n.startsWith(`-`),i=r?n.slice(1):n,a=i.indexOf(`.`),o=a===-1?i:i.slice(0,a),u=a===-1?``:i.slice(a+1),f=s(u.length);return{denominator:f,negative:r,numerator:BigInt(o||`0`)*f+BigInt(u||`0`)}}function p(t,n,r,i,a=!1){if(n===0n)return t;switch(i){case`ceiling`:return a?t:t+1n;case`down`:return t;case`floor`:return a?t+1n:t;case`half-away-from-zero`:return n*2n>=r?t+1n:t;case`half-even`:{let e=n*2n;return e<r?t:e>r?t+1n:t%2n==0n?t:t+1n}case`up`:return t+1n;default:throw new e(`Unknown rounding mode: ${String(i)}`)}}function m(n,r,i=`half-away-from-zero`){if(n.currency!==r.from)throw new t(n.currency,r.from);if(r.rate===``)throw new e(`Exchange rate must be a non-empty decimal string`);let{denominator:a,negative:s,numerator:c}=f(String(r.rate));if(s)throw new e(`Exchange rate must be non-negative`);let l=n.amount<0n,u=(n.amount<0n?-n.amount:n.amount)*c,d=p(u/a,u%a,a,i,l);return{amount:l?-d:d,currency:o(r.to)}}var h=r(512),g=r(32);function _(t,n){let{locale:r=`en-US`,maximumFractionDigits:i,minimumFractionDigits:o,style:c=`symbol`}=n,l=a(t.currency),u=b(`maximumFractionDigits`,i,l),d=b(`minimumFractionDigits`,o,Math.min(l,u));if(d>u)throw new e(`minimumFractionDigits must be less than or equal to maximumFractionDigits`);let f=ee(t.amount,l,u),p=f<0n,m=p?-f:f,h=s(u),g=m/h,_=x(u===0?``:(m%h).toString().padStart(u,`0`),d),v=S(r,t.currency,c,p);return{frac:_,intStr:C(r).format(g),negative:p,template:v}}function v(e,t={}){return y(e,t).map(e=>e.value).join(``)}function y(e,t={}){let{frac:n,intStr:r,template:i}=_(e,t),a=n.length>0,o=[],s=!1;for(let e of i)if(e.type!==`group`){if(e.type===`integer`){s||=(o.push({type:`integer`,value:r}),!0);continue}if(e.type===`decimal`){a&&o.push({type:`decimal`,value:e.value});continue}if(e.type===`fraction`){a&&o.push({type:`fraction`,value:n});continue}e.type===`currency`?o.push({type:`currency`,value:e.value}):e.type===`minusSign`?o.push({type:`minusSign`,value:e.value}):o.push({type:`literal`,value:e.value})}return o}function b(t,n,r){if(n==null)return r;if(!Number.isInteger(n)||n<0)throw new e(`${t} must be a non-negative integer`);return n}function ee(e,t,n){if(t===n)return e;if(n>t)return e*s(n-t);let r=e<0n,i=r?-e:e,a=s(t-n),o=p(i/a,i%a,a,`half-away-from-zero`);return r?-o:o}function x(e,t){if(e.length===0)return``;let n=e.length;for(;n>t&&e[n-1]===`0`;)n--;return e.slice(0,n)}function S(e,t,n,r){let i=[e,t,n,r?`neg`:`pos`].join(`\0`),a=h.get(i);if(a!==void 0)return a;let o=new Intl.NumberFormat(e,{currency:t,currencyDisplay:n,maximumFractionDigits:1,minimumFractionDigits:1,style:`currency`}).formatToParts(r?-1.1:1.1);return h.set(i,o),o}function C(e){let t=g.get(e);if(t!==void 0)return t;let n=new Intl.NumberFormat(e,{maximumFractionDigits:0,minimumFractionDigits:0,useGrouping:!0});return g.set(e,n),n}var w=!globalThis.__COINS_PROD__;function T(e){w&&console.warn(`[@vielzeug/coins] ${e}`)}function E(e,t){let n=o(t);if(typeof e==`bigint`)return{amount:e,currency:n};if(typeof e==`number`){let t=a(n),r=String(e),i=l.test(r)?d(r):r,o=i.indexOf(`.`),s=o===-1?0:i.length-o-1;return s>t&&T(`money(): number input "${e}" has more decimal places (${s}) than ${n} supports (${t}). Use a decimal string to avoid IEEE-754 precision loss.`),{amount:O(r,t),currency:n}}return{amount:O(String(e),a(n)),currency:n}}function D(e,n){if(e.currency!==n.currency)throw new t(e.currency,n.currency)}function O(e,t){let{denominator:n,negative:r,numerator:i}=f(e),a=i*s(t),o=p(a/n,a%n,n,`half-away-from-zero`);return r?-o:o}function k(e,t){return{amount:t,currency:e.currency}}function A(e){if(typeof e!=`object`||!e)return!1;let t=e;return Object.hasOwn(t,`amount`)&&typeof t.amount==`bigint`&&Object.hasOwn(t,`currency`)&&typeof t.currency==`string`}function j(e,t){return D(e,t),{amount:e.amount+t.amount,currency:e.currency}}function M(e,t){return D(e,t),{amount:e.amount-t.amount,currency:e.currency}}function N(e,t,n=`half-away-from-zero`){let{denominator:r,negative:i,numerator:a}=f(String(t)),o=e.amount<0n!==i,s=(e.amount<0n?-e.amount:e.amount)*a,c=p(s/r,s%r,r,n,o);return{amount:o?-c:c,currency:e.currency}}function P(t,n,r=`half-away-from-zero`){let{denominator:i,negative:a,numerator:o}=f(String(n));if(o===0n)throw new e(`Division by zero`);let s=t.amount<0n!==a,c=(t.amount<0n?-t.amount:t.amount)*i,l=p(c/o,c%o,o,r,s);return{amount:s?-l:l,currency:t.currency}}function F(t,n){if(n.length===0)throw new e(`allocate requires at least one ratio`);let r=n.map(e=>f(String(e)));if(r.some(e=>e.negative))throw new e(`All ratios must be non-negative`);if(!r.some(e=>e.numerator>0n))throw new e(`At least one ratio must be positive`);let i=t.amount<0n,a=i?-t.amount:t.amount,o=r.reduce((e,t)=>t.denominator>e?t.denominator:e,1n),s=r.map(e=>e.numerator*(o/e.denominator)),c=s.reduce((e,t)=>e+t,0n),l=[],u=[];for(let e of s){let t=a*e;l.push(t/c),u.push(t%c)}let d=n.map((e,t)=>t).sort((e,t)=>{let n=u[t]-u[e];return n>0n?1:n<0n?-1:0}),p=[...l],m=a-l.reduce((e,t)=>e+t,0n);for(let e=0;m>0n;e++,m--)p[d[e]]+=1n;return p.map(e=>({amount:i?-e:e,currency:t.currency}))}function I(n){if(n.length===0)throw new e(`sum requires at least one Money value`);let r=n[0].currency;for(let e=1;e<n.length;e++)if(n[e].currency!==r)throw new t(r,n[e].currency);return{amount:n.reduce((e,t)=>e+t.amount,0n),currency:r}}function L(t){if(t.length===0)throw new e(`min requires at least one Money value`);let n=t[0];for(let e=1;e<t.length;e++)n=U(n,t[e])<=0?n:t[e];return n}function R(t){if(t.length===0)throw new e(`max requires at least one Money value`);let n=t[0];for(let e=1;e<t.length;e++)n=U(n,t[e])>=0?n:t[e];return n}function z(t,n){if(!Number.isInteger(n)||n<=0)throw new e(`splitEvenly requires a positive integer number of parts`);return F(t,Array.from({length:n},()=>1))}function B(n,r,i){if(n.currency!==r.currency||n.currency!==i.currency)throw new t(n.currency,n.currency===r.currency?i.currency:r.currency);if(U(r,i)>0)throw new e(`clamp: lower (${$(r)} ${r.currency}) must be <= upper (${$(i)} ${i.currency})`);return R([r,L([n,i])])}function V(e){return{amount:e.amount<0n?-e.amount:e.amount,currency:e.currency}}function H(e){return{amount:-e.amount,currency:e.currency}}function U(e,t){return D(e,t),e.amount<t.amount?-1:+(e.amount>t.amount)}function W(e,t){return e.currency===t.currency&&e.amount===t.amount}function G(e,t){return U(e,t)>0}function K(e,t){return U(e,t)>=0}function q(e,t){return U(e,t)<0}function J(e,t){return U(e,t)<=0}function Y(e){return e.amount===0n}function X(e){return e.amount>0n}function Z(e){return e.amount<0n}function Q(e){return e.amount>=0n}function te(e){return e.amount<=0n}function ne(e){return{amount:String(e.amount),currency:e.currency}}function re(t){let n=o(t.currency);if(typeof t.amount!=`string`)throw new e(`Invalid money amount in JSON: ${String(t.amount)} (expected an integer string, e.g. '123456')`);let r;try{r=BigInt(t.amount)}catch{throw new e(`Invalid money amount in JSON: "${t.amount}" (expected an integer string, e.g. '123456')`)}return{amount:r,currency:n}}function $(e){let t=a(e.currency);if(t===0)return String(e.amount);let n=e.amount<0n,r=n?-e.amount:e.amount,i=s(t),o=r/i,c=(r%i).toString().padStart(t,`0`);return`${n?`-`:``}${o}.${c}`}function ie(e){let t=a(e.currency);return Number(e.amount)/Number(s(t))}function ae(t,n,r=`half-away-from-zero`){if(!Number.isInteger(n)||n<0)throw new e(`roundTo: places must be a non-negative integer, got ${n}`);let i=a(t.currency);if(n>i)throw new e(`roundTo: places (${n}) exceeds the decimal places for ${t.currency} (${i})`);if(n===i)return t;let o=s(i-n),c=t.amount<0n,l=c?-t.amount:t.amount,u=p(l/o,l%o,o,r,c);return{amount:c?-u:u,currency:t.currency}}exports.CoinsError=e,exports.CurrencyMismatchError=t,exports.InvalidCurrencyError=n,exports.abs=V,exports.add=j,exports.allocate=F,exports.clamp=B,exports.compare=U,exports.divide=P,exports.exchange=m,exports.format=v,exports.formatParts=y,exports.fromJSON=re,exports.getCurrencyDecimals=a,exports.greaterThan=G,exports.greaterThanOrEqual=K,exports.isEqual=W,exports.isMoney=A,exports.isNegative=Z,exports.isNonNegative=Q,exports.isNonPositive=te,exports.isPositive=X,exports.isZero=Y,exports.lessThan=q,exports.lessThanOrEqual=J,exports.max=R,exports.min=L,exports.money=E,exports.multiply=N,exports.negate=H,exports.roundTo=ae,exports.splitEvenly=z,exports.subtract=M,exports.sum=I,exports.toDecimal=$,exports.toJSON=ne,exports.toNumber=ie,exports.validateCurrencyCode=o,exports.withAmount=k;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=class e extends Error{code;static is(t){return t instanceof e}constructor(e,t,n){super(t,n),this.code=e,this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},t=class extends e{expected;received;constructor(e,t){super(`CURRENCY_MISMATCH`,`Currency mismatch: ${e} and ${t}`),this.expected=e,this.received=t}},n=class extends e{value;constructor(e){super(`INVALID_CURRENCY`,`Unsupported currency: "${String(e)}"`),this.value=e}},r=/^(-?)(\d+)(?:\.(\d+))?$/,i=1e3,a=100;function o(t){if(t.length>i)throw new e(`INVALID_DECIMAL`,`Decimal input is too long`);let n=r.exec(t);if(!n)throw new e(`INVALID_DECIMAL`,`Invalid decimal: "${t}"`);let o=(n[3]??``).replace(/0+$/,``);if(o.length>a)throw new e(`INVALID_DECIMAL`,`Decimal precision cannot exceed ${a} places`);let s=10n**BigInt(o.length),c=BigInt(n[2])*s+BigInt(o||`0`),u=n[1]===`-`&&c!==0n?-c:c,d=l(u<0n?-u:u,s);return Object.freeze({denominator:s/d,numerator:u/d})}function s(t,n,r){if(n<=0n)throw new e(`INVALID_DECIMAL`,`Decimal denominator must be positive`);let i=t<0n,a=i?-t:t,o=a/n,s=a%n;if(s===0n)return i?-o:o;let c=(()=>{switch(r){case`awayFromZero`:return!0;case`ceil`:return!i;case`floor`:return i;case`halfAwayFromZero`:return s*2n>=n;case`halfEven`:{let e=s*2n;return e>n||e===n&&o%2n!=0n}case`towardZero`:return!1}})()?o+1n:o;return i?-c:c}function c(e,t){let n=e<0n,r=n?-e:e,i=10n**BigInt(t),a=r/i;if(t===0)return`${n?`-`:``}${a}`;let o=(r%i).toString().padStart(t,`0`);return`${n?`-`:``}${a}.${o}`}function l(e,t){let n=e,r=t;for(;r!==0n;)[n,r]=[r,n%r];return n===0n?1n:n}function u(e,t){return e/l(e,t)*t}var d=new Map,f=new Map;function p(e,t){let n=w(e,t);return d.set(e,n),n}var m=p(`USD`,2),h=p(`EUR`,2),g=p(`GBP`,2),_=p(`JPY`,0),ee=p(`KRW`,0),v=p(`BHD`,3),y=p(`KWD`,3);function b({code:e,minorUnit:t}){T(e,t);let n=d.get(e);if(n){if(n.minorUnit!==t)throw E(e,n.minorUnit);return n}let r=f.get(e);if(r){if(r.minorUnit!==t)throw E(e,r.minorUnit);return r}let i=w(e,t);return f.set(e,i),i}function x(e){let t=d.get(e)??f.get(e);if(!t)throw new n(e);return t}function S(e){if(typeof e!=`object`||!e)return!1;let t=e;return typeof t.code==`string`&&(d.get(t.code)===e||f.get(t.code)===e)}function C(e){let t=d.get(e);if(!t)throw new n(e);return t}function w(e,t){return T(e,t),Object.freeze({code:e,minorUnit:t})}function T(t,n){if(!/^[A-Z]{3}$/.test(t))throw new e(`INVALID_CURRENCY`,`Currency code must be three uppercase letters: "${t}"`);if(!Number.isInteger(n)||n<0||n>6)throw new e(`INVALID_CURRENCY`,`Currency "${t}" must have 0–6 minor-unit digits`)}function E(t,n){return new e(`INVALID_CURRENCY`,`Currency "${t}" already has ${n} minor-unit digits`)}var D=`halfAwayFromZero`;function O(t,n,r){if(z(n),typeof t==`bigint`){if(!r||!(`unit`in r)||r.unit!==`minor`)throw new e(`INVALID_MONEY`,`Bigint amounts require { unit: 'minor' }`);return I(t,n)}if(r&&`unit`in r)throw new e(`INVALID_MONEY`,`Decimal strings do not accept a unit option`);let i=o(t),a=i.numerator*10n**BigInt(n.minorUnit);if(a%i.denominator!==0n&&!r)throw new e(`INVALID_MONEY`,`Amount "${t}" exceeds ${n.code} precision; provide rounding`);return I(s(a,i.denominator,r?.rounding??D),n)}function k(t){if(!B(t))throw new e(`INVALID_MONEY`,`Money must be a plain data object`);let n=Object.getOwnPropertyDescriptors(t),r=n.amount,i=n.currency;if(!V(r)||!V(i))throw new e(`INVALID_MONEY`,`Money properties must be plain data values`);if(typeof r.value!=`bigint`||!S(i.value))throw new e(`INVALID_MONEY`,`Money must contain bigint amount and a registered currency`);return I(r.value,i.value)}function A(e){try{return k(e),!0}catch{return!1}}function te(e,t){return R(e,t),I(e.amount+t.amount,e.currency)}function ne(e,t){return R(e,t),I(e.amount-t.amount,e.currency)}function re(e,t,n={}){L(e);let r=o(t);return I(s(e.amount*r.numerator,r.denominator,n.rounding??D),e.currency)}function ie(t,n,r={}){L(t);let i=o(n);if(i.numerator===0n)throw new e(`DIVISION_BY_ZERO`,`Cannot divide money by zero`);return I(s(t.amount*i.denominator,i.numerator<0n?-i.numerator:i.numerator,r.rounding??D)*(i.numerator<0n?-1n:1n),t.currency)}function j(e,t){return R(e,t),e.amount===t.amount?0:e.amount<t.amount?-1:1}function ae(e){return L(e),I(e.amount<0n?-e.amount:e.amount,e.currency)}function M(e){return L(e),I(-e.amount,e.currency)}function N(t,n){L(t);let{fractionDigits:r,rounding:i=D}=n;if(!Number.isInteger(r)||r<0||r>t.currency.minorUnit)throw new e(`INVALID_ROUNDING`,`fractionDigits must be an integer from 0 to ${t.currency.minorUnit}`);let a=10n**BigInt(t.currency.minorUnit-r);return I(s(t.amount,a,i)*a,t.currency)}function P(e){return L(e),c(e.amount,e.currency.minorUnit)}function F(e,t){return z(t),I(e,t)}function I(e,t){return Object.freeze({amount:e,currency:t})}function L(t){if(!Object.isFrozen(t)||!S(t.currency))throw new e(`INVALID_MONEY`,`Money must be a canonical Coins value`)}function R(e,n){if(L(e),L(n),e.currency!==n.currency)throw new t(e.currency.code,n.currency.code)}function z(t){if(!S(t))throw new e(`INVALID_CURRENCY`,`Money requires a registered currency`)}function B(e){if(typeof e!=`object`||!e)return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function V(e){return e!==void 0&&`value`in e&&e.get===void 0&&e.set===void 0}function H(n,r){let i=0n;for(let a of n){if(!A(a))throw new e(`INVALID_MONEY`,`sum() requires canonical money values`);if(a.currency!==r.currency)throw new t(r.currency.code,a.currency.code);i+=a.amount}return F(i,r.currency)}function U(t,n){if(!A(t))throw new e(`INVALID_MONEY`,`allocate() requires canonical money`);if(typeof n==`number`)return G(t,n);if(n.length===0)throw new e(`INVALID_ALLOCATION`,`Allocation weights cannot be empty`);let r=n.map(o);if(r.some(e=>e.numerator<0n))throw new e(`INVALID_ALLOCATION`,`Allocation weights cannot be negative`);let i=r.reduce((e,t)=>u(e,t.denominator),1n),a=r.map(e=>e.numerator*(i/e.denominator)),s=a.reduce((e,t)=>e+t,0n);if(s===0n)throw new e(`INVALID_ALLOCATION`,`Allocation weights must include a positive value`);let c=t.amount<0n?-1n:1n,l=t.amount<0n?-t.amount:t.amount,d=a.map(e=>l*e/s),f=l-d.reduce((e,t)=>e+t,0n),p=a.map((e,t)=>({index:t,remainder:l*e%s})).sort((e,t)=>e.remainder===t.remainder?e.index-t.index:e.remainder>t.remainder?-1:1);for(let e of p){if(f===0n)break;d[e.index]+=1n,f-=1n}return d.map(e=>F(e*c,t.currency))}function W(t,n){if(j(n.min,n.max)===1)throw new e(`INVALID_ALLOCATION`,`Clamp minimum cannot exceed maximum`);return j(t,n.min)===-1?n.min:j(t,n.max)===1?n.max:t}function G(t,n){if(!Number.isInteger(n)||n<1)throw new e(`INVALID_ALLOCATION`,`Allocation count must be a positive integer`);let r=BigInt(n),i=t.amount<0n?-1n:1n,a=t.amount<0n?-t.amount:t.amount,o=a/r,s=a%r;return Array.from({length:n},(e,n)=>F((o+(BigInt(n)<s?1n:0n))*i,t.currency))}function K({from:t,to:n,value:r}){if(!S(t)||!S(n))throw new e(`INVALID_CURRENCY`,`Exchange rate requires registered currencies`);let i=o(r);if(i.numerator<0n)throw new e(`INVALID_DECIMAL`,`Exchange rates cannot be negative`);return Object.freeze({from:t,to:n,value:i})}function q(n,r,i={}){if(!A(n)||!oe(r))throw new e(`INVALID_MONEY`,`Exchange requires canonical money and rate values`);if(n.currency!==r.from)throw new t(n.currency.code,r.from.code);return F(s(n.amount*r.value.numerator*10n**BigInt(r.to.minorUnit),r.value.denominator*10n**BigInt(r.from.minorUnit),i.rounding??`halfAwayFromZero`),r.to)}function oe(e){if(typeof e!=`object`||!e||!Object.isFrozen(e))return!1;let t=e;return S(t.from)&&S(t.to)&&typeof t.value==`object`&&t.value!==null&&Object.isFrozen(t.value)&&typeof t.value.numerator==`bigint`&&t.value.numerator>=0n&&typeof t.value.denominator==`bigint`&&t.value.denominator>0n}var J=20,Y=new Map,X=new Map;function se(e,t={}){return Z(e,t).map(e=>e.value).join(``)}function Z(t,n={}){if(!A(t))throw new e(`INVALID_MONEY`,`format() requires canonical money`);let{locale:r=`en-US`,maximumFractionDigits:i=t.currency.minorUnit,minimumFractionDigits:a=t.currency.minorUnit,style:o=`symbol`}=n;ce(a,i);let c=10n**BigInt(i),l=i>=t.currency.minorUnit?t.amount*10n**BigInt(i-t.currency.minorUnit):s(t.amount,10n**BigInt(t.currency.minorUnit-i),`halfAwayFromZero`),u=l<0n,d=u?-l:l,f=d/c,p=(i===0?``:(d%c).toString().padStart(i,`0`)).replace(RegExp(`0{0,${Math.max(0,i-a)}}$`),``),m=le(r,t.currency.code,o,u),h=[],g=!1;for(let e of m)e.type!==`group`&&(e.type===`integer`?g||=(h.push({type:`integer`,value:ue(r).format(f)}),!0):e.type===`decimal`?p&&h.push({type:`decimal`,value:e.value}):e.type===`fraction`?p&&h.push({type:`fraction`,value:p}):e.type===`currency`||e.type===`minusSign`||e.type===`plusSign`?h.push({type:e.type,value:e.value}):h.push({type:`literal`,value:e.value}));return h}function ce(t,n){if(!Number.isInteger(n)||!Number.isInteger(t)||t<0||n<t||n>J)throw new e(`INVALID_ROUNDING`,`Fraction digits must be integers satisfying 0 ≤ minimum ≤ maximum ≤ ${J}`)}function le(t,n,r,i){let a=`${t}\0${n}\0${r}\0${i}`,o=X.get(a);if(o)return o;try{let e=new Intl.NumberFormat(t,{currency:n,currencyDisplay:r,maximumFractionDigits:1,minimumFractionDigits:1,style:`currency`}).formatToParts(i?-1.1:1.1);return X.set(a,e),e}catch(r){throw new e(`INVALID_CURRENCY`,`Cannot format currency "${n}" for locale "${t}"`,{cause:r})}}function ue(t){let n=Y.get(t);if(n)return n;try{let e=new Intl.NumberFormat(t,{maximumFractionDigits:0,useGrouping:!0});return Y.set(t,e),e}catch(n){throw new e(`INVALID_ROUNDING`,`Cannot create number formatter for locale "${t}"`,{cause:n})}}var Q=/^(?:0|-[1-9]\d*|[1-9]\d*)$/,$=[`amount`,`currency`,`unit`];function de(e){return{amount:e.amount.toString(),currency:e.currency.code,unit:`minor`}}function fe(t,n={}){try{let e=pe(t),r=n.currency??C;return O(BigInt(e.amount),r(e.currency),{unit:`minor`})}catch(t){throw new e(`INVALID_MONEY`,`Invalid Money JSON`,{cause:t})}}function pe(e){if(typeof e!=`object`||!e||Object.getPrototypeOf(e)!==Object.prototype)throw TypeError(`Money JSON must be a plain object`);let t=Object.getOwnPropertyDescriptors(e),n=Reflect.ownKeys(e);if(n.length!==$.length||!$.every(e=>n.includes(e)))throw TypeError(`Money JSON must contain only amount, currency, and unit`);for(let e of $){let n=t[e];if(!n||!(`value`in n)||n.get||n.set)throw TypeError(`Money JSON property "${e}" must be a data value`)}let r=t.amount.value,i=t.currency.value,a=t.unit.value;if(typeof r!=`string`||!Q.test(r))throw TypeError(`Money JSON amount must be a canonical integer string`);if(typeof i!=`string`||a!==`minor`)throw TypeError(`Money JSON currency/unit are invalid`);return{amount:r,currency:i,unit:a}}exports.BHD=v,exports.CoinsError=e,exports.CurrencyMismatchError=t,exports.EUR=h,exports.GBP=g,exports.InvalidCurrencyError=n,exports.JPY=_,exports.KRW=ee,exports.KWD=y,exports.USD=m,exports.abs=ae,exports.add=te,exports.allocate=U,exports.clamp=W,exports.compare=j,exports.currency=x,exports.decimal=o,exports.defineCurrency=b,exports.divide=ie,exports.exchange=q,exports.exchangeRate=K,exports.format=se,exports.formatParts=Z,exports.isCurrency=S,exports.isMoney=A,exports.money=O,exports.multiply=re,exports.negate=M,exports.parseMoney=k,exports.parseMoneyJSON=fe,exports.round=N,exports.subtract=ne,exports.sum=H,exports.toDecimal=P,exports.toJSON=de;
2
2
  //# sourceMappingURL=coins.cjs.map