@ducatjs/money 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +51 -0
  3. package/README.md +617 -3
  4. package/assets/CREDITS.md +12 -0
  5. package/data/adyen-fx.json +19 -0
  6. package/data/adyen.json +171 -0
  7. package/data/braintree.json +223 -0
  8. package/data/cash-rounding.json +399 -0
  9. package/data/checkout.json +230 -0
  10. package/data/iso4217-list-one.xml +1956 -0
  11. package/data/klarna.json +95 -0
  12. package/data/paypal.json +65 -0
  13. package/data/stripe-fx.json +17 -0
  14. package/data/stripe.json +285 -0
  15. package/dist/adyen-fx.d.ts +25 -0
  16. package/dist/adyen-fx.d.ts.map +1 -0
  17. package/dist/adyen-fx.js +122 -0
  18. package/dist/adyen-fx.js.map +1 -0
  19. package/dist/adyen.d.ts +25 -0
  20. package/dist/adyen.d.ts.map +1 -0
  21. package/dist/adyen.js +34 -0
  22. package/dist/adyen.js.map +1 -0
  23. package/dist/allocate.d.ts +72 -0
  24. package/dist/allocate.d.ts.map +1 -0
  25. package/dist/allocate.js +314 -0
  26. package/dist/allocate.js.map +1 -0
  27. package/dist/bag.d.ts +58 -0
  28. package/dist/bag.d.ts.map +1 -0
  29. package/dist/bag.js +122 -0
  30. package/dist/bag.js.map +1 -0
  31. package/dist/braintree.d.ts +34 -0
  32. package/dist/braintree.d.ts.map +1 -0
  33. package/dist/braintree.js +45 -0
  34. package/dist/braintree.js.map +1 -0
  35. package/dist/cash.d.ts +35 -0
  36. package/dist/cash.d.ts.map +1 -0
  37. package/dist/cash.js +86 -0
  38. package/dist/cash.js.map +1 -0
  39. package/dist/checkout.d.ts +42 -0
  40. package/dist/checkout.d.ts.map +1 -0
  41. package/dist/checkout.js +52 -0
  42. package/dist/checkout.js.map +1 -0
  43. package/dist/convert.d.ts +51 -0
  44. package/dist/convert.d.ts.map +1 -0
  45. package/dist/convert.js +55 -0
  46. package/dist/convert.js.map +1 -0
  47. package/dist/currency.d.ts +22 -0
  48. package/dist/currency.d.ts.map +1 -0
  49. package/dist/currency.js +38 -0
  50. package/dist/currency.js.map +1 -0
  51. package/dist/decimal.d.ts +52 -0
  52. package/dist/decimal.d.ts.map +1 -0
  53. package/dist/decimal.js +251 -0
  54. package/dist/decimal.js.map +1 -0
  55. package/dist/ecb.d.ts +17 -0
  56. package/dist/ecb.d.ts.map +1 -0
  57. package/dist/ecb.js +147 -0
  58. package/dist/ecb.js.map +1 -0
  59. package/dist/exact.d.ts +62 -0
  60. package/dist/exact.d.ts.map +1 -0
  61. package/dist/exact.js +147 -0
  62. package/dist/exact.js.map +1 -0
  63. package/dist/fetch.d.ts +58 -0
  64. package/dist/fetch.d.ts.map +1 -0
  65. package/dist/fetch.js +79 -0
  66. package/dist/fetch.js.map +1 -0
  67. package/dist/fixed.d.ts +121 -0
  68. package/dist/fixed.d.ts.map +1 -0
  69. package/dist/fixed.js +420 -0
  70. package/dist/fixed.js.map +1 -0
  71. package/dist/fn.d.ts +27 -0
  72. package/dist/fn.d.ts.map +1 -0
  73. package/dist/fn.js +30 -0
  74. package/dist/fn.js.map +1 -0
  75. package/dist/format.d.ts +13 -0
  76. package/dist/format.d.ts.map +1 -0
  77. package/dist/format.js +90 -0
  78. package/dist/format.js.map +1 -0
  79. package/dist/generated/adyen.d.ts +3 -0
  80. package/dist/generated/adyen.d.ts.map +1 -0
  81. package/dist/generated/adyen.js +144 -0
  82. package/dist/generated/adyen.js.map +1 -0
  83. package/dist/generated/braintree.d.ts +3 -0
  84. package/dist/generated/braintree.d.ts.map +1 -0
  85. package/dist/generated/braintree.js +140 -0
  86. package/dist/generated/braintree.js.map +1 -0
  87. package/dist/generated/cash.d.ts +12 -0
  88. package/dist/generated/cash.d.ts.map +1 -0
  89. package/dist/generated/cash.js +364 -0
  90. package/dist/generated/cash.js.map +1 -0
  91. package/dist/generated/checkout.d.ts +3 -0
  92. package/dist/generated/checkout.d.ts.map +1 -0
  93. package/dist/generated/checkout.js +151 -0
  94. package/dist/generated/checkout.js.map +1 -0
  95. package/dist/generated/iso4217.d.ts +174 -0
  96. package/dist/generated/iso4217.d.ts.map +1 -0
  97. package/dist/generated/iso4217.js +175 -0
  98. package/dist/generated/iso4217.js.map +1 -0
  99. package/dist/generated/kernel.d.ts +17 -0
  100. package/dist/generated/kernel.d.ts.map +1 -0
  101. package/dist/generated/kernel.js +208 -0
  102. package/dist/generated/kernel.js.map +1 -0
  103. package/dist/generated/klarna.d.ts +3 -0
  104. package/dist/generated/klarna.d.ts.map +1 -0
  105. package/dist/generated/klarna.js +21 -0
  106. package/dist/generated/klarna.js.map +1 -0
  107. package/dist/generated/paypal.d.ts +3 -0
  108. package/dist/generated/paypal.d.ts.map +1 -0
  109. package/dist/generated/paypal.js +31 -0
  110. package/dist/generated/paypal.js.map +1 -0
  111. package/dist/generated/stripe.d.ts +8 -0
  112. package/dist/generated/stripe.d.ts.map +1 -0
  113. package/dist/generated/stripe.js +150 -0
  114. package/dist/generated/stripe.js.map +1 -0
  115. package/dist/index.d.ts +11 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +11 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/klarna.d.ts +31 -0
  120. package/dist/klarna.d.ts.map +1 -0
  121. package/dist/klarna.js +38 -0
  122. package/dist/klarna.js.map +1 -0
  123. package/dist/money.d.ts +218 -0
  124. package/dist/money.d.ts.map +1 -0
  125. package/dist/money.js +577 -0
  126. package/dist/money.js.map +1 -0
  127. package/dist/parse.d.ts +11 -0
  128. package/dist/parse.d.ts.map +1 -0
  129. package/dist/parse.js +132 -0
  130. package/dist/parse.js.map +1 -0
  131. package/dist/paypal.d.ts +41 -0
  132. package/dist/paypal.d.ts.map +1 -0
  133. package/dist/paypal.js +58 -0
  134. package/dist/paypal.js.map +1 -0
  135. package/dist/plugins/adyen.d.ts +15 -0
  136. package/dist/plugins/adyen.d.ts.map +1 -0
  137. package/dist/plugins/adyen.js +10 -0
  138. package/dist/plugins/adyen.js.map +1 -0
  139. package/dist/plugins/all.d.ts +22 -0
  140. package/dist/plugins/all.d.ts.map +1 -0
  141. package/dist/plugins/all.js +23 -0
  142. package/dist/plugins/all.js.map +1 -0
  143. package/dist/plugins/allocate.d.ts +20 -0
  144. package/dist/plugins/allocate.d.ts.map +1 -0
  145. package/dist/plugins/allocate.js +26 -0
  146. package/dist/plugins/allocate.js.map +1 -0
  147. package/dist/plugins/braintree.d.ts +12 -0
  148. package/dist/plugins/braintree.d.ts.map +1 -0
  149. package/dist/plugins/braintree.js +12 -0
  150. package/dist/plugins/braintree.js.map +1 -0
  151. package/dist/plugins/cash.d.ts +19 -0
  152. package/dist/plugins/cash.d.ts.map +1 -0
  153. package/dist/plugins/cash.js +16 -0
  154. package/dist/plugins/cash.js.map +1 -0
  155. package/dist/plugins/checkout.d.ts +21 -0
  156. package/dist/plugins/checkout.d.ts.map +1 -0
  157. package/dist/plugins/checkout.js +16 -0
  158. package/dist/plugins/checkout.js.map +1 -0
  159. package/dist/plugins/convert.d.ts +20 -0
  160. package/dist/plugins/convert.d.ts.map +1 -0
  161. package/dist/plugins/convert.js +13 -0
  162. package/dist/plugins/convert.js.map +1 -0
  163. package/dist/plugins/exact.d.ts +15 -0
  164. package/dist/plugins/exact.d.ts.map +1 -0
  165. package/dist/plugins/exact.js +13 -0
  166. package/dist/plugins/exact.js.map +1 -0
  167. package/dist/plugins/fixed.d.ts +17 -0
  168. package/dist/plugins/fixed.d.ts.map +1 -0
  169. package/dist/plugins/fixed.js +13 -0
  170. package/dist/plugins/fixed.js.map +1 -0
  171. package/dist/plugins/format.d.ts +9 -0
  172. package/dist/plugins/format.d.ts.map +1 -0
  173. package/dist/plugins/format.js +16 -0
  174. package/dist/plugins/format.js.map +1 -0
  175. package/dist/plugins/klarna.d.ts +10 -0
  176. package/dist/plugins/klarna.d.ts.map +1 -0
  177. package/dist/plugins/klarna.js +10 -0
  178. package/dist/plugins/klarna.js.map +1 -0
  179. package/dist/plugins/paypal.d.ts +14 -0
  180. package/dist/plugins/paypal.d.ts.map +1 -0
  181. package/dist/plugins/paypal.js +16 -0
  182. package/dist/plugins/paypal.js.map +1 -0
  183. package/dist/plugins/prisma.d.ts +8 -0
  184. package/dist/plugins/prisma.d.ts.map +1 -0
  185. package/dist/plugins/prisma.js +13 -0
  186. package/dist/plugins/prisma.js.map +1 -0
  187. package/dist/plugins/stripe.d.ts +14 -0
  188. package/dist/plugins/stripe.d.ts.map +1 -0
  189. package/dist/plugins/stripe.js +16 -0
  190. package/dist/plugins/stripe.js.map +1 -0
  191. package/dist/plugins/tax.d.ts +21 -0
  192. package/dist/plugins/tax.d.ts.map +1 -0
  193. package/dist/plugins/tax.js +18 -0
  194. package/dist/plugins/tax.js.map +1 -0
  195. package/dist/prisma.d.ts +66 -0
  196. package/dist/prisma.d.ts.map +1 -0
  197. package/dist/prisma.js +66 -0
  198. package/dist/prisma.js.map +1 -0
  199. package/dist/processor.d.ts +28 -0
  200. package/dist/processor.d.ts.map +1 -0
  201. package/dist/processor.js +118 -0
  202. package/dist/processor.js.map +1 -0
  203. package/dist/provider.d.ts +89 -0
  204. package/dist/provider.d.ts.map +1 -0
  205. package/dist/provider.js +240 -0
  206. package/dist/provider.js.map +1 -0
  207. package/dist/rate.d.ts +119 -0
  208. package/dist/rate.d.ts.map +1 -0
  209. package/dist/rate.js +271 -0
  210. package/dist/rate.js.map +1 -0
  211. package/dist/rounding-error.d.ts +9 -0
  212. package/dist/rounding-error.d.ts.map +1 -0
  213. package/dist/rounding-error.js +9 -0
  214. package/dist/rounding-error.js.map +1 -0
  215. package/dist/rounding.d.ts +50 -0
  216. package/dist/rounding.d.ts.map +1 -0
  217. package/dist/rounding.js +87 -0
  218. package/dist/rounding.js.map +1 -0
  219. package/dist/stripe-fx.d.ts +22 -0
  220. package/dist/stripe-fx.d.ts.map +1 -0
  221. package/dist/stripe-fx.js +30 -0
  222. package/dist/stripe-fx.js.map +1 -0
  223. package/dist/stripe.d.ts +36 -0
  224. package/dist/stripe.d.ts.map +1 -0
  225. package/dist/stripe.js +39 -0
  226. package/dist/stripe.js.map +1 -0
  227. package/dist/tax.d.ts +29 -0
  228. package/dist/tax.d.ts.map +1 -0
  229. package/dist/tax.js +39 -0
  230. package/dist/tax.js.map +1 -0
  231. package/package.json +169 -5
  232. package/src/adyen-fx.ts +154 -0
  233. package/src/adyen.ts +46 -0
  234. package/src/allocate.ts +393 -0
  235. package/src/bag.ts +170 -0
  236. package/src/braintree.ts +71 -0
  237. package/src/cash.ts +115 -0
  238. package/src/checkout.ts +77 -0
  239. package/src/convert.ts +159 -0
  240. package/src/currency.ts +61 -0
  241. package/src/decimal.ts +283 -0
  242. package/src/ecb.ts +162 -0
  243. package/src/exact.ts +182 -0
  244. package/src/fetch.ts +127 -0
  245. package/src/fixed.ts +558 -0
  246. package/src/fn.ts +80 -0
  247. package/src/format.ts +106 -0
  248. package/src/generated/adyen.ts +144 -0
  249. package/src/generated/braintree.ts +140 -0
  250. package/src/generated/cash.ts +375 -0
  251. package/src/generated/checkout.ts +151 -0
  252. package/src/generated/iso4217.ts +179 -0
  253. package/src/generated/kernel.ts +230 -0
  254. package/src/generated/klarna.ts +21 -0
  255. package/src/generated/paypal.ts +31 -0
  256. package/src/generated/stripe.ts +150 -0
  257. package/src/index.ts +37 -0
  258. package/src/klarna.ts +54 -0
  259. package/src/money.ts +781 -0
  260. package/src/parse.ts +144 -0
  261. package/src/paypal.ts +93 -0
  262. package/src/plugins/adyen.ts +25 -0
  263. package/src/plugins/all.ts +47 -0
  264. package/src/plugins/allocate.ts +53 -0
  265. package/src/plugins/braintree.ts +23 -0
  266. package/src/plugins/cash.ts +33 -0
  267. package/src/plugins/checkout.ts +32 -0
  268. package/src/plugins/convert.ts +50 -0
  269. package/src/plugins/exact.ts +23 -0
  270. package/src/plugins/fixed.ts +28 -0
  271. package/src/plugins/format.ts +28 -0
  272. package/src/plugins/klarna.ts +21 -0
  273. package/src/plugins/paypal.ts +29 -0
  274. package/src/plugins/prisma.ts +24 -0
  275. package/src/plugins/stripe.ts +29 -0
  276. package/src/plugins/tax.ts +41 -0
  277. package/src/prisma.ts +118 -0
  278. package/src/processor.ts +181 -0
  279. package/src/provider.ts +324 -0
  280. package/src/rate.ts +377 -0
  281. package/src/rounding-error.ts +8 -0
  282. package/src/rounding.ts +122 -0
  283. package/src/stripe-fx.ts +49 -0
  284. package/src/stripe.ts +69 -0
  285. package/src/tax.ts +71 -0
@@ -0,0 +1,89 @@
1
+ import type { Factor } from './decimal.js';
2
+ import { type Rate, type RateInfo } from './rate.js';
3
+ /** When a rate is wanted for. */
4
+ export interface RateQuery {
5
+ /** An ISO-8601 date or instant, or a Date. */
6
+ readonly asOf?: string | Date;
7
+ }
8
+ /** Looks up a rate synchronously. `undefined` when it has none for the pair (or for `asOf`). */
9
+ export interface RateProvider {
10
+ rate<From extends string, To extends string>(from: From, to: To, query?: RateQuery): Rate<From, To> | undefined;
11
+ }
12
+ /** A fixed set of rates, as built by rateTable() and returned by the ECB parsers and fetchers. */
13
+ export interface RateTable extends RateProvider {
14
+ readonly rates: readonly Rate[];
15
+ /** The quoted currency, for a one-base table. */
16
+ readonly base?: string;
17
+ /** The table's date, when it has one. */
18
+ readonly asOf?: string;
19
+ }
20
+ /** Thrown when no provider has a rate for the pair. */
21
+ export declare class RateUnavailableError extends Error {
22
+ readonly from: string;
23
+ readonly to: string;
24
+ readonly asOf: string | undefined;
25
+ constructor(from: string, to: string, asOf?: string | Date);
26
+ }
27
+ /** The rate for the pair, or RateUnavailableError: a lookup that must succeed. */
28
+ export declare function rateOf<From extends string, To extends string>(provider: RateProvider, from: From, to: To, query?: RateQuery): Rate<From, To>;
29
+ /** 1:1 for a currency to itself; nothing otherwise. */
30
+ export declare function identity(): RateProvider;
31
+ /** A one-base table: `rates.USD` is the number of USD per 1 `base`. */
32
+ export interface RateTableSpec extends RateInfo {
33
+ readonly base: string;
34
+ readonly rates: Readonly<Record<string, Factor>>;
35
+ }
36
+ /**
37
+ * A fixed table of rates: one base currency against many (ECB style), or a list of Rates. Only the
38
+ * pairs given are answered; wrap it in `reversed()` or `via()` for the others.
39
+ *
40
+ * Lookups are by whole UTC day: a query for a day before a rate's asOf gets nothing, and a query at
41
+ * any time on the rate's own day gets the rate, even before its publication time (the ECB publishes
42
+ * at about 15:00 UTC). If that matters, query with the previous day until the table is published.
43
+ *
44
+ * const ecb = rateTable({ base: 'EUR', source: 'ECB', asOf: '2026-11-06', rates: { USD: '1.1165' } });
45
+ */
46
+ export declare function rateTable(spec: RateTableSpec | readonly Rate[]): RateTable;
47
+ /** Asks each provider in turn; the first with an answer wins. */
48
+ export declare function chain(...providers: readonly RateProvider[]): RateProvider;
49
+ /** Answers to→from lookups with the exact inverse of the provider's from→to rate. */
50
+ export declare function reversed(provider: RateProvider): RateProvider;
51
+ /**
52
+ * Triangulates through `base`: from→base, then base→to, composed exactly (converting rounds once).
53
+ * Each leg is looked up directly or as an exact inverse, so an ECB table (EUR→X only) answers X→Y.
54
+ */
55
+ export declare function via(base: string, provider: RateProvider): RateProvider;
56
+ /** Options for snapshots(). */
57
+ export interface SnapshotOptions {
58
+ /**
59
+ * How many earlier days to try when a day has no table (weekends, holidays). Default 4, which
60
+ * covers Good Friday to Easter Monday.
61
+ */
62
+ readonly lookback?: number;
63
+ /** How many loaded days to keep; the least recently used are dropped. Default 32. */
64
+ readonly max?: number;
65
+ }
66
+ /** Dated tables, loaded on demand. */
67
+ export interface Snapshots {
68
+ /** The latest table at or before `asOf` (a date, an instant or a Date), within the lookback. */
69
+ at(asOf: string | Date): Promise<RateProvider>;
70
+ }
71
+ /**
72
+ * Dated tables from a loader you supply: `load('2026-11-06')` resolves to that day's table, or to
73
+ * undefined when there is none. Days are UTC days. Loaded tables are cached by day. Misses and
74
+ * failures are not cached, so a table that isn't published yet is tried again next time. A loader
75
+ * that throws fails the lookup rather than falling back to an older day, so an outage doesn't
76
+ * silently become a stale rate. The rates carry the day they're from.
77
+ *
78
+ * const history = snapshots(async (day) => parseEcbXml(await readFile(`ecb/${day}.xml`, 'utf8'))[0]);
79
+ * const fx = await history.at('2026-11-08'); // a Sunday: Friday 2026-11-06's table
80
+ */
81
+ export declare function snapshots(load: (day: string) => Promise<RateProvider | undefined>, options?: SnapshotOptions): Snapshots;
82
+ /**
83
+ * Snapshots over tables you already hold (e.g. an ECB 90-day history), keyed by their asOf day.
84
+ *
85
+ * const history = snapshotsFrom(parseEcbXml(xml90d));
86
+ * const fx = await history.at('2026-11-08');
87
+ */
88
+ export declare function snapshotsFrom(tables: readonly RateTable[], options?: SnapshotOptions): Snapshots;
89
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,240 @@
1
+ import { CurrencyMismatchError } from './money.js';
2
+ import { exchangeRate, isRate, toIsoString } from './rate.js';
3
+ const DAY_MS = 86_400_000;
4
+ /** Thrown when no provider has a rate for the pair. */
5
+ export class RateUnavailableError extends Error {
6
+ from;
7
+ to;
8
+ asOf;
9
+ constructor(from, to, asOf) {
10
+ let when;
11
+ if (asOf !== undefined) {
12
+ when = toIsoString(asOf);
13
+ }
14
+ let message = `No exchange rate ${from}→${to}`;
15
+ if (when !== undefined) {
16
+ message += ` as of ${when}`;
17
+ }
18
+ super(message);
19
+ this.name = 'RateUnavailableError';
20
+ this.from = from;
21
+ this.to = to;
22
+ this.asOf = when;
23
+ }
24
+ }
25
+ /** The UTC date ('2026-11-06') of a date, an instant (any offset) or a Date. */
26
+ function dayOf(time) {
27
+ return toIsoString(time).slice(0, 10);
28
+ }
29
+ /** The rate for the pair, or RateUnavailableError: a lookup that must succeed. */
30
+ export function rateOf(provider, from, to, query) {
31
+ const rate = provider.rate(from, to, query);
32
+ if (rate === undefined) {
33
+ throw new RateUnavailableError(from, to, query?.asOf);
34
+ }
35
+ // A provider is user code: a wrong pair here would produce Money labelled with the wrong
36
+ // currency.
37
+ if (rate.from !== from) {
38
+ throw new CurrencyMismatchError(from, rate.from);
39
+ }
40
+ if (rate.to !== to) {
41
+ throw new CurrencyMismatchError(to, rate.to);
42
+ }
43
+ return rate;
44
+ }
45
+ /** 1:1 for a currency to itself; nothing otherwise. */
46
+ export function identity() {
47
+ return {
48
+ rate(from, to) {
49
+ // Compared as strings: From and To are independent type parameters.
50
+ if (from !== to) {
51
+ return undefined;
52
+ }
53
+ return exchangeRate(from, to, 1n);
54
+ },
55
+ };
56
+ }
57
+ /**
58
+ * A fixed table of rates: one base currency against many (ECB style), or a list of Rates. Only the
59
+ * pairs given are answered; wrap it in `reversed()` or `via()` for the others.
60
+ *
61
+ * Lookups are by whole UTC day: a query for a day before a rate's asOf gets nothing, and a query at
62
+ * any time on the rate's own day gets the rate, even before its publication time (the ECB publishes
63
+ * at about 15:00 UTC). If that matters, query with the previous day until the table is published.
64
+ *
65
+ * const ecb = rateTable({ base: 'EUR', source: 'ECB', asOf: '2026-11-06', rates: { USD: '1.1165' } });
66
+ */
67
+ export function rateTable(spec) {
68
+ let rates;
69
+ const header = {};
70
+ if (isRateList(spec)) {
71
+ rates = [...spec];
72
+ }
73
+ else {
74
+ rates = Object.entries(spec.rates).map(([to, rate]) => exchangeRate(spec.base, to, rate, spec));
75
+ header.base = spec.base;
76
+ if (spec.asOf !== undefined) {
77
+ header.asOf = toIsoString(spec.asOf);
78
+ }
79
+ }
80
+ const byPair = new Map();
81
+ for (const rate of rates) {
82
+ byPair.set(`${rate.from}/${rate.to}`, rate);
83
+ }
84
+ return {
85
+ ...header,
86
+ rates,
87
+ rate(from, to, query) {
88
+ const rate = byPair.get(`${from}/${to}`);
89
+ if (rate === undefined) {
90
+ return undefined;
91
+ }
92
+ const askedBeforeRateExisted = query?.asOf !== undefined &&
93
+ rate.asOf !== undefined &&
94
+ dayOf(query.asOf) < dayOf(rate.asOf);
95
+ if (askedBeforeRateExisted) {
96
+ return undefined;
97
+ }
98
+ // The key matched, so the rate's currencies are exactly `from` and `to`.
99
+ return rate;
100
+ },
101
+ };
102
+ }
103
+ function isRateList(spec) {
104
+ return Array.isArray(spec) && spec.every(isRate);
105
+ }
106
+ /** Asks each provider in turn; the first with an answer wins. */
107
+ export function chain(...providers) {
108
+ return {
109
+ rate(from, to, query) {
110
+ for (const provider of providers) {
111
+ const rate = provider.rate(from, to, query);
112
+ if (rate !== undefined) {
113
+ return rate;
114
+ }
115
+ }
116
+ return undefined;
117
+ },
118
+ };
119
+ }
120
+ /** Answers to→from lookups with the exact inverse of the provider's from→to rate. */
121
+ export function reversed(provider) {
122
+ return {
123
+ rate(from, to, query) {
124
+ return provider.rate(to, from, query)?.inverse();
125
+ },
126
+ };
127
+ }
128
+ /**
129
+ * Triangulates through `base`: from→base, then base→to, composed exactly (converting rounds once).
130
+ * Each leg is looked up directly or as an exact inverse, so an ECB table (EUR→X only) answers X→Y.
131
+ */
132
+ export function via(base, provider) {
133
+ function leg(from, to, query) {
134
+ return provider.rate(from, to, query) ?? provider.rate(to, from, query)?.inverse();
135
+ }
136
+ return {
137
+ rate(from, to, query) {
138
+ if (from === to) {
139
+ return undefined;
140
+ }
141
+ if (from === base || to === base) {
142
+ return leg(from, to, query);
143
+ }
144
+ const toBase = leg(from, base, query);
145
+ if (toBase === undefined) {
146
+ return undefined;
147
+ }
148
+ const fromBase = leg(base, to, query);
149
+ if (fromBase === undefined) {
150
+ return undefined;
151
+ }
152
+ return toBase.andThen(fromBase);
153
+ },
154
+ };
155
+ }
156
+ /**
157
+ * Dated tables from a loader you supply: `load('2026-11-06')` resolves to that day's table, or to
158
+ * undefined when there is none. Days are UTC days. Loaded tables are cached by day. Misses and
159
+ * failures are not cached, so a table that isn't published yet is tried again next time. A loader
160
+ * that throws fails the lookup rather than falling back to an older day, so an outage doesn't
161
+ * silently become a stale rate. The rates carry the day they're from.
162
+ *
163
+ * const history = snapshots(async (day) => parseEcbXml(await readFile(`ecb/${day}.xml`, 'utf8'))[0]);
164
+ * const fx = await history.at('2026-11-08'); // a Sunday: Friday 2026-11-06's table
165
+ */
166
+ export function snapshots(load, options) {
167
+ const lookback = options?.lookback ?? 4;
168
+ const max = options?.max ?? 32;
169
+ if (!Number.isSafeInteger(lookback) || lookback < 0) {
170
+ throw new RangeError(`lookback must be a non-negative integer, got ${lookback}`);
171
+ }
172
+ if (!Number.isSafeInteger(max) || max < 1) {
173
+ throw new RangeError(`max must be a positive integer, got ${max}`);
174
+ }
175
+ // Map iteration order is insertion order, so re-inserting on every use makes it an LRU.
176
+ const cache = new Map();
177
+ // Only drop the entry this load created: a newer load for the same day may have replaced it.
178
+ function forget(day, loading) {
179
+ if (cache.get(day) === loading) {
180
+ cache.delete(day);
181
+ }
182
+ }
183
+ function loadDay(day) {
184
+ let pending = cache.get(day);
185
+ if (pending === undefined) {
186
+ const loading = load(day).then((table) => {
187
+ if (table === undefined) {
188
+ forget(day, loading);
189
+ }
190
+ return table;
191
+ });
192
+ loading.catch(() => forget(day, loading));
193
+ pending = loading;
194
+ }
195
+ else {
196
+ cache.delete(day);
197
+ }
198
+ cache.set(day, pending);
199
+ if (cache.size > max) {
200
+ const leastRecentlyUsed = cache.keys().next().value;
201
+ cache.delete(leastRecentlyUsed);
202
+ }
203
+ return pending;
204
+ }
205
+ return {
206
+ async at(asOf) {
207
+ const firstDay = dayOf(asOf);
208
+ const firstDayMs = Date.parse(`${firstDay}T00:00:00Z`);
209
+ if (Number.isNaN(firstDayMs)) {
210
+ throw new RangeError(`Invalid asOf ${String(asOf)}`);
211
+ }
212
+ for (let i = 0; i <= lookback; i++) {
213
+ const day = new Date(firstDayMs - i * DAY_MS).toISOString().slice(0, 10);
214
+ // Sequential on purpose: stop at the first day that has a table, without loading older
215
+ // days. oxlint-disable-next-line no-await-in-loop
216
+ const table = await loadDay(day);
217
+ if (table !== undefined) {
218
+ return table;
219
+ }
220
+ }
221
+ throw new RateUnavailableError('*', '*', firstDay);
222
+ },
223
+ };
224
+ }
225
+ /**
226
+ * Snapshots over tables you already hold (e.g. an ECB 90-day history), keyed by their asOf day.
227
+ *
228
+ * const history = snapshotsFrom(parseEcbXml(xml90d));
229
+ * const fx = await history.at('2026-11-08');
230
+ */
231
+ export function snapshotsFrom(tables, options) {
232
+ const byDay = new Map();
233
+ for (const table of tables) {
234
+ if (table.asOf !== undefined) {
235
+ byDay.set(dayOf(table.asOf), table);
236
+ }
237
+ }
238
+ return snapshots(async (day) => byDay.get(day), options);
239
+ }
240
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
package/dist/rate.d.ts ADDED
@@ -0,0 +1,119 @@
1
+ import { type Factor, type Ratio } from './decimal.js';
2
+ import { type CurrencyInput, type Money } from './money.js';
3
+ import { type RoundingMode } from './rounding.js';
4
+ /** How a rate was quoted. `inverse()` swaps 'bid' and 'ask'. */
5
+ export type RateType = 'mid' | 'bid' | 'ask' | (string & {});
6
+ /**
7
+ * Where a rate came from. `asOf` and `validUntil` are ISO-8601 dates ('2026-11-06') or instants.
8
+ */
9
+ export interface RateInfo {
10
+ readonly source?: string;
11
+ readonly asOf?: string | Date;
12
+ readonly validUntil?: string | Date;
13
+ readonly type?: RateType;
14
+ }
15
+ /** Options for converting an amount. */
16
+ export interface ConvertOptions {
17
+ /** Result scale (default: the target currency's exponent). */
18
+ readonly scale?: number;
19
+ /**
20
+ * Throw RateExpiredError when the rate has a `validUntil` that has passed (by the system clock).
21
+ * Off by default: ducat reads no clock unless asked to.
22
+ */
23
+ readonly strictValidity?: boolean;
24
+ }
25
+ /** Thrown by a strict-validity conversion with a rate past its `validUntil`. */
26
+ export declare class RateExpiredError extends RangeError {
27
+ readonly validUntil: string;
28
+ constructor(from: string, to: string, validUntil: string);
29
+ }
30
+ /** The JSON form of a Rate: the exact decimal string, or "num/den" when it doesn't terminate. */
31
+ export interface RateJSON {
32
+ from: string;
33
+ to: string;
34
+ rate: string;
35
+ source?: string;
36
+ asOf?: string;
37
+ validUntil?: string;
38
+ type?: string;
39
+ derivedFrom?: RateJSON[];
40
+ }
41
+ /**
42
+ * @internal
43
+ * A date stays as given ('2026-11-06'); an instant becomes UTC ISO ('2026-11-06T15:00:00.000Z'), so
44
+ * that times with different offsets compare correctly as strings.
45
+ */
46
+ export declare function toIsoString(time: string | Date): string;
47
+ /**
48
+ * minor × rate × 10^scaleShift, rounded once with `rounding`. This is the kernel of every
49
+ * conversion: `scaleShift` is the result scale minus the input scale. Pure, and `rate.den > 0` by
50
+ * construction.
51
+ */
52
+ export declare function multiplyByRatioAtScale(minor: number | bigint, rate: Ratio, scaleShift: number, rounding: RoundingMode): number | bigint;
53
+ /**
54
+ * An exact exchange rate: one unit of `from` is worth `ratio` units of `to`. Build it with
55
+ * `exchangeRate()`. Derived rates (`inverse`, `andThen`, `withMargin`, `round`) keep their inputs
56
+ * in `derivedFrom`, the audit chain. Their `asOf` is the oldest input's, so staleness can't hide,
57
+ * and their `validUntil` is the earliest.
58
+ */
59
+ export declare class Rate<From extends string = string, To extends string = string> {
60
+ readonly from: From;
61
+ readonly to: To;
62
+ readonly ratio: Ratio;
63
+ readonly source?: string;
64
+ readonly asOf?: string;
65
+ readonly validUntil?: string;
66
+ readonly type?: RateType;
67
+ readonly derivedFrom?: readonly Rate[];
68
+ /** @internal use exchangeRate() */
69
+ constructor(from: From, to: To, rate: Ratio, info?: RateInfo, derivedFrom?: readonly Rate[]);
70
+ /** The exact reciprocal, `to` → `from`. A bid becomes an ask and vice versa. */
71
+ inverse(): Rate<To, From>;
72
+ /**
73
+ * The exact composite `from` → `next.to`. Nothing is rounded: converting with it rounds once.
74
+ * (Named `andThen`, not `then`, so that a Rate is never mistaken for a promise.)
75
+ */
76
+ andThen<Next extends string>(next: Rate<To, Next>): Rate<From, Next>;
77
+ /** This rate × (1 + margin), exactly: `withMargin('-0.02')` gives 2% less `to` per `from`. */
78
+ withMargin(margin: Factor): Rate<From, To>;
79
+ /**
80
+ * A new Rate rounded to `places` decimals, for businesses that quote rounded (e.g. inverse)
81
+ * rates.
82
+ */
83
+ round(places: number, rounding?: RoundingMode): Rate<From, To>;
84
+ /** The rate with exactly `places` decimals, for display. The Rate itself keeps every digit. */
85
+ toDecimal(places: number, rounding?: RoundingMode): string;
86
+ /** The exact rate: '1.1165', or '2000/2233' when its decimal expansion doesn't terminate. */
87
+ toString(): string;
88
+ /** The rate, its metadata and its audit chain as JSON. `rateFromJSON` reverses it. */
89
+ toJSON(): RateJSON;
90
+ /**
91
+ * Whether `validUntil` has passed at `now` (default: the system clock). A date is valid all day
92
+ * (UTC).
93
+ */
94
+ isExpired(now?: string | Date): boolean;
95
+ /** Converts `money`, rounding once to the target currency's exponent (or `options.scale`). */
96
+ convert(money: Money<From>, rounding?: RoundingMode, options?: ConvertOptions): Money<To>;
97
+ }
98
+ /**
99
+ * An exact rate pinned to a currency pair, so converting the wrong currency is a type error. Give
100
+ * the rate as a decimal string ('1.1165', every digit kept), a "num/den" string, a bigint or a
101
+ * Ratio. Numbers are read at their shortest decimal form (1.234 is exactly 1234/1000).
102
+ *
103
+ * const eurUsd = exchangeRate('EUR', 'USD', '1.1165', { source: 'ECB', asOf: '2026-11-06' });
104
+ * eurUsd.convert(money('100', 'EUR'), 'halfEven'); // 111.65 USD
105
+ * eurUsd.inverse().ratio // 2000/2233, exactly
106
+ */
107
+ export declare function exchangeRate<From extends CurrencyInput, To extends CurrencyInput>(from: From, to: To, rate: Factor, info?: RateInfo): Rate<From, To>;
108
+ /** Rebuilds a Rate, with its audit chain, from `rate.toJSON()`. */
109
+ export declare function rateFromJSON(json: RateJSON): Rate;
110
+ /** Whether `value` is a Rate. */
111
+ export declare function isRate(value: unknown): value is Rate;
112
+ /**
113
+ * The exact rate a processor actually applied: settled ÷ presented. Use it to audit a quoted rate,
114
+ * which after the processor's own rounding often doesn't reproduce the settled amount.
115
+ *
116
+ * impliedRate(money('10.00', 'EUR'), money('12.34', 'USD')) // EUR→USD 1.234
117
+ */
118
+ export declare function impliedRate<From extends string, To extends string>(presented: Money<From>, settled: Money<To>, info?: RateInfo): Rate<From, To>;
119
+ //# sourceMappingURL=rate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate.d.ts","sourceRoot":"","sources":["../src/rate.ts"],"names":[],"mappings":"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"}