iso-price 1.0.3

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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/dist/contract/index.d.ts +25 -0
  3. package/dist/contract/index.js +60 -0
  4. package/dist/contract/index.js.map +1 -0
  5. package/dist/domain.objects/IsoCurrency.d.ts +63 -0
  6. package/dist/domain.objects/IsoCurrency.js +70 -0
  7. package/dist/domain.objects/IsoCurrency.js.map +1 -0
  8. package/dist/domain.objects/IsoPrice.d.ts +16 -0
  9. package/dist/domain.objects/IsoPrice.js +3 -0
  10. package/dist/domain.objects/IsoPrice.js.map +1 -0
  11. package/dist/domain.objects/IsoPriceExponent.d.ts +28 -0
  12. package/dist/domain.objects/IsoPriceExponent.js +33 -0
  13. package/dist/domain.objects/IsoPriceExponent.js.map +1 -0
  14. package/dist/domain.objects/IsoPriceHuman.d.ts +19 -0
  15. package/dist/domain.objects/IsoPriceHuman.js +3 -0
  16. package/dist/domain.objects/IsoPriceHuman.js.map +1 -0
  17. package/dist/domain.objects/IsoPriceRoundMode.d.ts +23 -0
  18. package/dist/domain.objects/IsoPriceRoundMode.js +28 -0
  19. package/dist/domain.objects/IsoPriceRoundMode.js.map +1 -0
  20. package/dist/domain.objects/IsoPriceShape.d.ts +30 -0
  21. package/dist/domain.objects/IsoPriceShape.js +3 -0
  22. package/dist/domain.objects/IsoPriceShape.js.map +1 -0
  23. package/dist/domain.objects/IsoPriceWords.d.ts +20 -0
  24. package/dist/domain.objects/IsoPriceWords.js +3 -0
  25. package/dist/domain.objects/IsoPriceWords.js.map +1 -0
  26. package/dist/domain.operations/arithmetic/allocatePrice.d.ts +48 -0
  27. package/dist/domain.operations/arithmetic/allocatePrice.js +167 -0
  28. package/dist/domain.operations/arithmetic/allocatePrice.js.map +1 -0
  29. package/dist/domain.operations/arithmetic/dividePrice.d.ts +40 -0
  30. package/dist/domain.operations/arithmetic/dividePrice.js +127 -0
  31. package/dist/domain.operations/arithmetic/dividePrice.js.map +1 -0
  32. package/dist/domain.operations/arithmetic/multiplyPrice.d.ts +38 -0
  33. package/dist/domain.operations/arithmetic/multiplyPrice.js +89 -0
  34. package/dist/domain.operations/arithmetic/multiplyPrice.js.map +1 -0
  35. package/dist/domain.operations/arithmetic/subPrices.d.ts +28 -0
  36. package/dist/domain.operations/arithmetic/subPrices.js +62 -0
  37. package/dist/domain.operations/arithmetic/subPrices.js.map +1 -0
  38. package/dist/domain.operations/arithmetic/sumPrices.d.ts +44 -0
  39. package/dist/domain.operations/arithmetic/sumPrices.js +88 -0
  40. package/dist/domain.operations/arithmetic/sumPrices.js.map +1 -0
  41. package/dist/domain.operations/cast/asIsoPrice.d.ts +35 -0
  42. package/dist/domain.operations/cast/asIsoPrice.js +117 -0
  43. package/dist/domain.operations/cast/asIsoPrice.js.map +1 -0
  44. package/dist/domain.operations/cast/asIsoPriceHuman.d.ts +25 -0
  45. package/dist/domain.operations/cast/asIsoPriceHuman.js +106 -0
  46. package/dist/domain.operations/cast/asIsoPriceHuman.js.map +1 -0
  47. package/dist/domain.operations/cast/asIsoPriceShape.d.ts +25 -0
  48. package/dist/domain.operations/cast/asIsoPriceShape.js +164 -0
  49. package/dist/domain.operations/cast/asIsoPriceShape.js.map +1 -0
  50. package/dist/domain.operations/cast/asIsoPriceWords.d.ts +25 -0
  51. package/dist/domain.operations/cast/asIsoPriceWords.js +103 -0
  52. package/dist/domain.operations/cast/asIsoPriceWords.js.map +1 -0
  53. package/dist/domain.operations/guard/isIsoPrice.d.ts +18 -0
  54. package/dist/domain.operations/guard/isIsoPrice.js +29 -0
  55. package/dist/domain.operations/guard/isIsoPrice.js.map +1 -0
  56. package/dist/domain.operations/guard/isIsoPriceHuman.d.ts +24 -0
  57. package/dist/domain.operations/guard/isIsoPriceHuman.js +76 -0
  58. package/dist/domain.operations/guard/isIsoPriceHuman.js.map +1 -0
  59. package/dist/domain.operations/guard/isIsoPriceShape.d.ts +29 -0
  60. package/dist/domain.operations/guard/isIsoPriceShape.js +50 -0
  61. package/dist/domain.operations/guard/isIsoPriceShape.js.map +1 -0
  62. package/dist/domain.operations/guard/isIsoPriceWords.d.ts +24 -0
  63. package/dist/domain.operations/guard/isIsoPriceWords.js +48 -0
  64. package/dist/domain.operations/guard/isIsoPriceWords.js.map +1 -0
  65. package/dist/domain.operations/precision/getIsoPriceExponentByCurrency.d.ts +15 -0
  66. package/dist/domain.operations/precision/getIsoPriceExponentByCurrency.js +49 -0
  67. package/dist/domain.operations/precision/getIsoPriceExponentByCurrency.js.map +1 -0
  68. package/dist/domain.operations/precision/roundPrice.d.ts +25 -0
  69. package/dist/domain.operations/precision/roundPrice.js +24 -0
  70. package/dist/domain.operations/precision/roundPrice.js.map +1 -0
  71. package/dist/domain.operations/precision/setPricePrecision.d.ts +29 -0
  72. package/dist/domain.operations/precision/setPricePrecision.js +119 -0
  73. package/dist/domain.operations/precision/setPricePrecision.js.map +1 -0
  74. package/dist/domain.operations/statistics/calcPriceAvg.d.ts +21 -0
  75. package/dist/domain.operations/statistics/calcPriceAvg.js +92 -0
  76. package/dist/domain.operations/statistics/calcPriceAvg.js.map +1 -0
  77. package/dist/domain.operations/statistics/calcPriceStdev.d.ts +23 -0
  78. package/dist/domain.operations/statistics/calcPriceStdev.js +137 -0
  79. package/dist/domain.operations/statistics/calcPriceStdev.js.map +1 -0
  80. package/dist/index.d.ts +1 -0
  81. package/dist/index.js +18 -0
  82. package/dist/index.js.map +1 -0
  83. package/license.md +21 -0
  84. package/package.json +102 -0
  85. package/readme.md +373 -0
package/readme.md ADDED
@@ -0,0 +1,373 @@
1
+ # iso-price
2
+
3
+ ![test](https://github.com/ehmpathy/iso-price/workflows/test/badge.svg)
4
+ ![publish](https://github.com/ehmpathy/iso-price/workflows/publish/badge.svg)
5
+
6
+ a pit of success for prices. iso 4217 currencies. si metric precision. ecmascript numeric separators.
7
+
8
+ ## why
9
+
10
+ money math is deceptively hard:
11
+
12
+ ```ts
13
+ 0.1 + 0.2 === 0.30000000000000004 // float math
14
+ ```
15
+
16
+ most solutions add complexity: decimal libraries, bigint wrappers, money objects. iso-price takes the opposite approach — make the correct choice simple.
17
+
18
+ ```ts
19
+ import { sumPrices } from 'iso-price';
20
+
21
+ sumPrices('USD 0.10', 'USD 0.20'); // => 'USD 0.30'
22
+ ```
23
+
24
+ that's it. words in, words out. the precision, currency validation, and bigint arithmetic happen automatically.
25
+
26
+ ## design
27
+
28
+ **any input, safe output.**
29
+
30
+ throw any format at it — `'$50.37'`, `'USD 50.37'`, `{ amount: 5037, currency: 'USD' }`, `{ amount: 5037n, currency: 'USD' }` — and get back `IsoPriceWords`: a string that is serializable, lossless, readable, composable, and standards-conformant.
31
+
32
+ when you need structured access for schema persistence or performance optimization:
33
+
34
+ ```ts
35
+ asIsoPriceShape('USD 50.37'); // => { amount: 5037n, currency: 'USD' }
36
+ ```
37
+
38
+ when you need a ux-friendly format for display:
39
+
40
+ ```ts
41
+ asIsoPriceHuman('USD 50.37'); // => '$50.37'
42
+ ```
43
+
44
+ by default, you get the best of all worlds.
45
+
46
+ **thin contract, deep behavior.**
47
+
48
+ the surface is simple: prices are strictly structured strings formatted like `'USD 50.37'`. under the hood: sub-cent precision, currency exponents, lossless bigint arithmetic, automatic precision normalization.
49
+
50
+ **explicit, not magic.**
51
+
52
+ precision is visible in the string. `'USD 50.370_000'` shows micro-dollar precision. `'USD 0.000_000_250'` shows nano-dollar precision. no hidden state, no surprises.
53
+
54
+ **standards-based.**
55
+
56
+ - **iso 4217** — currency codes and exponents (USD=2, JPY=0, BHD=3)
57
+ - **si metric prefixes** — centi, milli, micro, nano, pico
58
+ - **ecmascript numeric separators** — underscores for readability
59
+
60
+ ## install
61
+
62
+ ```sh
63
+ npm install iso-price
64
+ ```
65
+
66
+ ## usage
67
+
68
+ ### the basics
69
+
70
+ ```ts
71
+ import { asIsoPrice, sumPrices, multiplyPrice } from 'iso-price';
72
+
73
+ // parse any format
74
+ const price = asIsoPrice('$50.37'); // => 'USD 50.37'
75
+ const price = asIsoPrice('EUR 100.00'); // => 'EUR 100.00'
76
+
77
+ // arithmetic just works, regardless of input format
78
+ sumPrices('$10', 'USD 20.00'); // => 'USD 30.00'
79
+ multiplyPrice({ of: '$100', by: 1.08 }); // => 'USD 108.00'
80
+ ```
81
+
82
+ ### sub-cent precision
83
+
84
+ llm token costs, serverless invocations, crypto — sometimes you need more than cents:
85
+
86
+ ```ts
87
+ import { dividePrice, sumPrices } from 'iso-price';
88
+
89
+ // $0.25 per million tokens
90
+ const perToken = dividePrice({ of: '$0.25', by: 1_000_000 });
91
+ // => 'USD 0.000_000_250'
92
+
93
+ // track micro-costs, sum to invoice
94
+ const costs = ['USD 0.011_845_500', 'USD 47.370_001_970'];
95
+ sumPrices(costs); // => 'USD 47.381_847_470'
96
+
97
+ // cross-precision arithmetic auto-resolves to most granular
98
+ sumPrices('USD 50.00', 'USD 0.000_005'); // => 'USD 50.000_005'
99
+ ```
100
+
101
+ precision scales automatically. no configuration needed.
102
+
103
+ ### three formats
104
+
105
+ | format | example | use |
106
+ | --------------- | ------------------------------------ | ------------------------ |
107
+ | `IsoPriceWords` | `'USD 50.37'` | storage, logs, api, json |
108
+ | `IsoPriceShape` | `{ amount: 5037n, currency: 'USD' }` | computation |
109
+ | `IsoPriceHuman` | `'$50.37'` | display |
110
+
111
+ all operations accept any format. all return `IsoPriceWords` by default.
112
+
113
+ ```ts
114
+ import { asIsoPriceShape, asIsoPriceHuman } from 'iso-price';
115
+
116
+ // need structured access for stripe or persistance?
117
+ asIsoPriceShape('USD 50.37'); // => { amount: 5037n, currency: 'USD' }
118
+
119
+ // need display format?
120
+ asIsoPriceHuman('USD 50.37'); // => '$50.37'
121
+ ```
122
+
123
+ ### allocation without loss
124
+
125
+ money splits create remainders. iso-price handles them:
126
+
127
+ ```ts
128
+ import { allocatePrice } from 'iso-price';
129
+
130
+ allocatePrice({ of: 'USD 10.00', into: { parts: 3 }, remainder: 'first' });
131
+ // => ['USD 3.34', 'USD 3.33', 'USD 3.33']
132
+ // sum: exactly $10.00
133
+ ```
134
+
135
+ ## api
136
+
137
+ ### cast
138
+
139
+ - `asIsoPrice(input)` — normalize to words
140
+ - `asIsoPriceWords(input)` — convert to words
141
+ - `asIsoPriceShape(input)` — convert to shape
142
+ - `asIsoPriceHuman(input)` — convert to display
143
+
144
+ ### arithmetic
145
+
146
+ - `sumPrices(...prices)` / `priceSum` / `addPrices` / `priceAdd`
147
+ - `subPrices(a, b)` / `priceSub`
148
+ - `multiplyPrice({ of, by })` / `priceMultiply`
149
+ - `dividePrice({ of, by })` / `priceDivide`
150
+ - `allocatePrice({ of, into, remainder })` / `priceAllocate`
151
+
152
+ ### precision
153
+
154
+ - `setPricePrecision({ of, to }, options?)`
155
+ - `roundPrice({ of }, options?)`
156
+ - `getIsoPriceExponentByCurrency(currency)`
157
+
158
+ ### statistics
159
+
160
+ - `calcPriceAvg(prices)`
161
+ - `calcPriceStdev(prices)`
162
+
163
+ ### guards
164
+
165
+ - `isIsoPrice(input)` / `.assure(input)`
166
+ - `isIsoPriceWords(input)` / `.assure(input)`
167
+ - `isIsoPriceShape(input)` / `.assure(input)`
168
+ - `isIsoPriceHuman(input)` / `.assure(input)`
169
+
170
+ ### types
171
+
172
+ - `IsoPrice<TCurrency>` — union of all formats
173
+ - `IsoPriceWords<TCurrency>` — branded string
174
+ - `IsoPriceShape<TCurrency>` — bigint object
175
+ - `IsoPriceHuman` — display string
176
+ - `IsoPriceExponent` — precision enum
177
+ - `IsoPriceRoundMode` — round mode enum
178
+ - `IsoCurrency` — top 25 currencies enum
179
+
180
+ ## currency exponents
181
+
182
+ iso 4217 defines the standard precision (exponent) for each currency. iso-price applies these automatically:
183
+
184
+ ```ts
185
+ asIsoPrice('$50.37'); // => 'USD 50.37' (2 decimals — cents)
186
+ asIsoPrice('¥1000'); // => 'JPY 1000' (0 decimals — no minor unit)
187
+ asIsoPrice('BHD 1.234'); // => 'BHD 1.234' (3 decimals — fils)
188
+ ```
189
+
190
+ | currency | exponent | minor unit | example |
191
+ | ------------- | -------- | ---------- | ------------- |
192
+ | USD, EUR, GBP | 2 | cents | `'USD 50.37'` |
193
+ | JPY, KRW, VND | 0 | none | `'JPY 1000'` |
194
+ | BHD, KWD, OMR | 3 | fils/baisa | `'BHD 1.234'` |
195
+
196
+ when you need more precision than the standard (llm tokens, serverless), iso-price extends with si metric prefixes:
197
+
198
+ ## exponents
199
+
200
+ si metric prefixes for explicit precision:
201
+
202
+ | exponent | factor | iso 4217 | examples |
203
+ | -------------- | ------ | ---------- | ---------- |
204
+ | `whole.x10^0` | 10⁰ | 0 decimals | jpy, krw |
205
+ | `centi.x10^-2` | 10⁻² | 2 decimals | usd, eur |
206
+ | `milli.x10^-3` | 10⁻³ | 3 decimals | bhd, kwd |
207
+ | `micro.x10^-6` | 10⁻⁶ | — | llm tokens |
208
+ | `nano.x10^-9` | 10⁻⁹ | — | serverless |
209
+ | `pico.x10^-12` | 10⁻¹² | — | extreme |
210
+
211
+ ## real-world examples
212
+
213
+ ### e-commerce invoice
214
+
215
+ standard cents precision — the common case:
216
+
217
+ ```ts
218
+ const lineItems = ['USD 29.99', 'USD 14.50', 'USD 5.99'];
219
+ const subtotal = sumPrices(lineItems); // => 'USD 50.48'
220
+ const withTax = multiplyPrice({ of: subtotal, by: 1.08 }); // => 'USD 54.52'
221
+ ```
222
+
223
+ ### llm api cost aggregation
224
+
225
+ nano-dollar precision for per-token costs:
226
+
227
+ ```ts
228
+ // claude haiku: $0.25 per million input tokens
229
+ const costPerToken = dividePrice({ of: 'USD 0.25', by: 1_000_000 });
230
+ // => 'USD 0.000_000_250'
231
+
232
+ // accumulate usage across requests
233
+ const costsAccumulated = [
234
+ 'USD 0.000_047_250', // 189 tokens
235
+ 'USD 0.000_125_000', // 500 tokens
236
+ 'USD 0.002_847_500', // 11,390 tokens
237
+ ];
238
+
239
+ // sum micro-costs into invoiceable amount
240
+ const costTotal = sumPrices(costsAccumulated); // => 'USD 0.003_019_750'
241
+
242
+ // combine with standard e-commerce charges
243
+ const platformFee = 'USD 9.99';
244
+ const invoice = sumPrices(platformFee, costTotal);
245
+ // => 'USD 9.993_019_750'
246
+ ```
247
+
248
+ ### bill allocation
249
+
250
+ allocate without remainder loss:
251
+
252
+ ```ts
253
+ // split a $100 dinner bill 3 ways
254
+ allocatePrice({ of: 'USD 100.00', into: { parts: 3 }, remainder: 'first' });
255
+ // => ['USD 33.34', 'USD 33.33', 'USD 33.33']
256
+ // sum: exactly $100.00
257
+
258
+ // split by ratio (60/40)
259
+ allocatePrice({ of: 'USD 100.00', into: { ratios: [6, 4] }, remainder: 'first' });
260
+ // => ['USD 60.00', 'USD 40.00']
261
+ ```
262
+
263
+ ## currency symbols
264
+
265
+ currency symbols are lossy — `$` could be USD, CAD, AUD, or 20+ other currencies. iso-price defaults to the most common:
266
+
267
+ | symbol | default | also used by |
268
+ | ------ | ------- | --------------------------------- |
269
+ | `$` | USD | CAD, AUD, NZD, MXN, SGD, HKD, ... |
270
+ | `€` | EUR | (unique) |
271
+ | `£` | GBP | EGP, LBP, SYP, ... |
272
+ | `¥` | JPY | CNY |
273
+
274
+ override when needed:
275
+
276
+ ```ts
277
+ asIsoPrice('$50.37'); // => 'USD 50.37'
278
+ asIsoPrice('$50.37', { currency: 'CAD' }); // => 'CAD 50.37'
279
+ asIsoPrice('$50.37', { currency: 'AUD' }); // => 'AUD 50.37'
280
+ ```
281
+
282
+ for unambiguous storage and transmission, always use `IsoPriceWords` format (`'USD 50.37'`). use `IsoPriceHuman` (`'$50.37'`) only for display.
283
+
284
+ ## serialization
285
+
286
+ `IsoPriceWords` is a string — it serializes trivially:
287
+
288
+ ```ts
289
+ const price = sumPrices('USD 10.00', 'USD 20.00'); // => 'USD 30.00'
290
+
291
+ JSON.stringify({ total: price });
292
+ // => '{"total":"USD 30.00"}'
293
+ ```
294
+
295
+ `IsoPriceShape` uses bigint, which cannot be serialized directly:
296
+
297
+ ```ts
298
+ JSON.stringify({ amount: 5037n });
299
+ // => TypeError: Do not know how to serialize a BigInt
300
+ ```
301
+
302
+ this is intentional — it pushes you toward `IsoPriceWords` for persistence, which is portable, human-readable, and lossless.
303
+
304
+ ## persistence
305
+
306
+ | database | recommendation | storage |
307
+ | --------------------------- | ----------------------- | ------------------------------ |
308
+ | dynamodb, mongodb, s3, json | `IsoPriceWords` | `"USD 50.37"` |
309
+ | postgres, sqlite | `iso_price` extension | native arithmetic + comparison |
310
+ | mysql, mariadb | see persistence catalog | many considerations |
311
+
312
+ **nosql / json** — store as `IsoPriceWords` string. these databases don't support numeric price comparisons anyway, so use the portable, human-readable format:
313
+
314
+ ```ts
315
+ await dynamodb.put({ total: 'USD 50.37' });
316
+ const price = asIsoPrice(item.total); // => 'USD 50.37'
317
+ ```
318
+
319
+ **postgres / sqlite** — use the `iso_price` extension for native operators:
320
+
321
+ ```sql
322
+ SELECT * FROM items WHERE price > 'USD 10.00' ORDER BY price;
323
+ SELECT sum(price) FROM line_items; -- auto-normalizes exponents
324
+ ```
325
+
326
+ **other sql** (mysql, mariadb, etc) — these databases don't support extensions, so price comparisons across currencies and precisions require careful consideration. see the [persistence catalog](./.agent/repo=.this/role=dbadmin/briefs/per001.persistence._.[catalog].md) for patterns.
327
+
328
+ ## round modes
329
+
330
+ when precision decreases, a round mode is required:
331
+
332
+ ```ts
333
+ import { setPricePrecision, roundPrice } from 'iso-price';
334
+
335
+ // default: half-up (standard round behavior)
336
+ setPricePrecision({ of: 'USD 5.555', to: 'centi.x10^-2' });
337
+ // => 'USD 5.56'
338
+
339
+ // explicit round modes
340
+ setPricePrecision({ of: 'USD 5.555', to: 'centi.x10^-2' }, { round: 'floor' });
341
+ // => 'USD 5.55'
342
+
343
+ setPricePrecision({ of: 'USD 5.555', to: 'centi.x10^-2' }, { round: 'ceil' });
344
+ // => 'USD 5.56'
345
+
346
+ // round to currency's standard precision
347
+ roundPrice({ of: 'USD 5.555_555' }); // => 'USD 5.56'
348
+ ```
349
+
350
+ | mode | behavior | 5.555 → cents |
351
+ | ----------- | ------------------------------------- | ------------- |
352
+ | `half-up` | round half toward +∞ (default) | 5.56 |
353
+ | `half-down` | round half toward 0 | 5.55 |
354
+ | `half-even` | round half to nearest even (banker's) | 5.56 |
355
+ | `floor` | toward −∞ | 5.55 |
356
+ | `ceil` | toward +∞ | 5.56 |
357
+ | `trunc` | toward 0 | 5.55 |
358
+
359
+ ## supported currencies
360
+
361
+ `IsoCurrency` includes the top 25 currencies by forex volume plus the 3-decimal currencies:
362
+
363
+ ```ts
364
+ enum IsoCurrency {
365
+ // top by volume
366
+ USD, EUR, JPY, GBP, CNY, AUD, CAD, CHF, HKD, NZD,
367
+ SEK, KRW, SGD, NOK, MXN, INR, ZAR, BRL, DKK, PLN, THB,
368
+ // 3-decimal (fils/baisa)
369
+ BHD, KWD, OMR, TND,
370
+ }
371
+ ```
372
+
373
+ custom currencies (BTC, ETH, or any 3-letter code) are supported — they default to 2-decimal precision unless explicitly specified.