@ducatjs/money 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/NOTICE +51 -0
- package/README.md +617 -2
- package/assets/CREDITS.md +12 -0
- package/data/adyen-fx.json +19 -0
- package/data/adyen.json +171 -0
- package/data/braintree.json +223 -0
- package/data/cash-rounding.json +399 -0
- package/data/checkout.json +230 -0
- package/data/iso4217-list-one.xml +1956 -0
- package/data/klarna.json +95 -0
- package/data/paypal.json +65 -0
- package/data/stripe-fx.json +17 -0
- package/data/stripe.json +285 -0
- package/dist/adyen-fx.d.ts +25 -0
- package/dist/adyen-fx.d.ts.map +1 -0
- package/dist/adyen-fx.js +122 -0
- package/dist/adyen-fx.js.map +1 -0
- package/dist/adyen.d.ts +25 -0
- package/dist/adyen.d.ts.map +1 -0
- package/dist/adyen.js +34 -0
- package/dist/adyen.js.map +1 -0
- package/dist/allocate.d.ts +72 -0
- package/dist/allocate.d.ts.map +1 -0
- package/dist/allocate.js +314 -0
- package/dist/allocate.js.map +1 -0
- package/dist/bag.d.ts +58 -0
- package/dist/bag.d.ts.map +1 -0
- package/dist/bag.js +122 -0
- package/dist/bag.js.map +1 -0
- package/dist/braintree.d.ts +34 -0
- package/dist/braintree.d.ts.map +1 -0
- package/dist/braintree.js +45 -0
- package/dist/braintree.js.map +1 -0
- package/dist/cash.d.ts +35 -0
- package/dist/cash.d.ts.map +1 -0
- package/dist/cash.js +86 -0
- package/dist/cash.js.map +1 -0
- package/dist/checkout.d.ts +42 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +52 -0
- package/dist/checkout.js.map +1 -0
- package/dist/convert.d.ts +51 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +55 -0
- package/dist/convert.js.map +1 -0
- package/dist/currency.d.ts +22 -0
- package/dist/currency.d.ts.map +1 -0
- package/dist/currency.js +38 -0
- package/dist/currency.js.map +1 -0
- package/dist/decimal.d.ts +52 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +251 -0
- package/dist/decimal.js.map +1 -0
- package/dist/ecb.d.ts +17 -0
- package/dist/ecb.d.ts.map +1 -0
- package/dist/ecb.js +147 -0
- package/dist/ecb.js.map +1 -0
- package/dist/exact.d.ts +62 -0
- package/dist/exact.d.ts.map +1 -0
- package/dist/exact.js +147 -0
- package/dist/exact.js.map +1 -0
- package/dist/fetch.d.ts +58 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +79 -0
- package/dist/fetch.js.map +1 -0
- package/dist/fixed.d.ts +121 -0
- package/dist/fixed.d.ts.map +1 -0
- package/dist/fixed.js +420 -0
- package/dist/fixed.js.map +1 -0
- package/dist/fn.d.ts +27 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +30 -0
- package/dist/fn.js.map +1 -0
- package/dist/format.d.ts +13 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +90 -0
- package/dist/format.js.map +1 -0
- package/dist/generated/adyen.d.ts +3 -0
- package/dist/generated/adyen.d.ts.map +1 -0
- package/dist/generated/adyen.js +144 -0
- package/dist/generated/adyen.js.map +1 -0
- package/dist/generated/braintree.d.ts +3 -0
- package/dist/generated/braintree.d.ts.map +1 -0
- package/dist/generated/braintree.js +140 -0
- package/dist/generated/braintree.js.map +1 -0
- package/dist/generated/cash.d.ts +12 -0
- package/dist/generated/cash.d.ts.map +1 -0
- package/dist/generated/cash.js +364 -0
- package/dist/generated/cash.js.map +1 -0
- package/dist/generated/checkout.d.ts +3 -0
- package/dist/generated/checkout.d.ts.map +1 -0
- package/dist/generated/checkout.js +151 -0
- package/dist/generated/checkout.js.map +1 -0
- package/dist/generated/iso4217.d.ts +174 -0
- package/dist/generated/iso4217.d.ts.map +1 -0
- package/dist/generated/iso4217.js +175 -0
- package/dist/generated/iso4217.js.map +1 -0
- package/dist/generated/kernel.d.ts +17 -0
- package/dist/generated/kernel.d.ts.map +1 -0
- package/dist/generated/kernel.js +208 -0
- package/dist/generated/kernel.js.map +1 -0
- package/dist/generated/klarna.d.ts +3 -0
- package/dist/generated/klarna.d.ts.map +1 -0
- package/dist/generated/klarna.js +21 -0
- package/dist/generated/klarna.js.map +1 -0
- package/dist/generated/paypal.d.ts +3 -0
- package/dist/generated/paypal.d.ts.map +1 -0
- package/dist/generated/paypal.js +31 -0
- package/dist/generated/paypal.js.map +1 -0
- package/dist/generated/stripe.d.ts +8 -0
- package/dist/generated/stripe.d.ts.map +1 -0
- package/dist/generated/stripe.js +150 -0
- package/dist/generated/stripe.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/klarna.d.ts +31 -0
- package/dist/klarna.d.ts.map +1 -0
- package/dist/klarna.js +38 -0
- package/dist/klarna.js.map +1 -0
- package/dist/money.d.ts +218 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +577 -0
- package/dist/money.js.map +1 -0
- package/dist/parse.d.ts +11 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +132 -0
- package/dist/parse.js.map +1 -0
- package/dist/paypal.d.ts +41 -0
- package/dist/paypal.d.ts.map +1 -0
- package/dist/paypal.js +58 -0
- package/dist/paypal.js.map +1 -0
- package/dist/plugins/adyen.d.ts +15 -0
- package/dist/plugins/adyen.d.ts.map +1 -0
- package/dist/plugins/adyen.js +10 -0
- package/dist/plugins/adyen.js.map +1 -0
- package/dist/plugins/all.d.ts +22 -0
- package/dist/plugins/all.d.ts.map +1 -0
- package/dist/plugins/all.js +23 -0
- package/dist/plugins/all.js.map +1 -0
- package/dist/plugins/allocate.d.ts +20 -0
- package/dist/plugins/allocate.d.ts.map +1 -0
- package/dist/plugins/allocate.js +26 -0
- package/dist/plugins/allocate.js.map +1 -0
- package/dist/plugins/braintree.d.ts +12 -0
- package/dist/plugins/braintree.d.ts.map +1 -0
- package/dist/plugins/braintree.js +12 -0
- package/dist/plugins/braintree.js.map +1 -0
- package/dist/plugins/cash.d.ts +19 -0
- package/dist/plugins/cash.d.ts.map +1 -0
- package/dist/plugins/cash.js +16 -0
- package/dist/plugins/cash.js.map +1 -0
- package/dist/plugins/checkout.d.ts +21 -0
- package/dist/plugins/checkout.d.ts.map +1 -0
- package/dist/plugins/checkout.js +16 -0
- package/dist/plugins/checkout.js.map +1 -0
- package/dist/plugins/convert.d.ts +20 -0
- package/dist/plugins/convert.d.ts.map +1 -0
- package/dist/plugins/convert.js +13 -0
- package/dist/plugins/convert.js.map +1 -0
- package/dist/plugins/exact.d.ts +15 -0
- package/dist/plugins/exact.d.ts.map +1 -0
- package/dist/plugins/exact.js +13 -0
- package/dist/plugins/exact.js.map +1 -0
- package/dist/plugins/fixed.d.ts +17 -0
- package/dist/plugins/fixed.d.ts.map +1 -0
- package/dist/plugins/fixed.js +13 -0
- package/dist/plugins/fixed.js.map +1 -0
- package/dist/plugins/format.d.ts +9 -0
- package/dist/plugins/format.d.ts.map +1 -0
- package/dist/plugins/format.js +16 -0
- package/dist/plugins/format.js.map +1 -0
- package/dist/plugins/klarna.d.ts +10 -0
- package/dist/plugins/klarna.d.ts.map +1 -0
- package/dist/plugins/klarna.js +10 -0
- package/dist/plugins/klarna.js.map +1 -0
- package/dist/plugins/paypal.d.ts +14 -0
- package/dist/plugins/paypal.d.ts.map +1 -0
- package/dist/plugins/paypal.js +16 -0
- package/dist/plugins/paypal.js.map +1 -0
- package/dist/plugins/prisma.d.ts +8 -0
- package/dist/plugins/prisma.d.ts.map +1 -0
- package/dist/plugins/prisma.js +13 -0
- package/dist/plugins/prisma.js.map +1 -0
- package/dist/plugins/stripe.d.ts +14 -0
- package/dist/plugins/stripe.d.ts.map +1 -0
- package/dist/plugins/stripe.js +16 -0
- package/dist/plugins/stripe.js.map +1 -0
- package/dist/plugins/tax.d.ts +21 -0
- package/dist/plugins/tax.d.ts.map +1 -0
- package/dist/plugins/tax.js +18 -0
- package/dist/plugins/tax.js.map +1 -0
- package/dist/prisma.d.ts +66 -0
- package/dist/prisma.d.ts.map +1 -0
- package/dist/prisma.js +66 -0
- package/dist/prisma.js.map +1 -0
- package/dist/processor.d.ts +28 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +118 -0
- package/dist/processor.js.map +1 -0
- package/dist/provider.d.ts +89 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +240 -0
- package/dist/provider.js.map +1 -0
- package/dist/rate.d.ts +119 -0
- package/dist/rate.d.ts.map +1 -0
- package/dist/rate.js +271 -0
- package/dist/rate.js.map +1 -0
- package/dist/rounding-error.d.ts +9 -0
- package/dist/rounding-error.d.ts.map +1 -0
- package/dist/rounding-error.js +9 -0
- package/dist/rounding-error.js.map +1 -0
- package/dist/rounding.d.ts +50 -0
- package/dist/rounding.d.ts.map +1 -0
- package/dist/rounding.js +87 -0
- package/dist/rounding.js.map +1 -0
- package/dist/stripe-fx.d.ts +22 -0
- package/dist/stripe-fx.d.ts.map +1 -0
- package/dist/stripe-fx.js +30 -0
- package/dist/stripe-fx.js.map +1 -0
- package/dist/stripe.d.ts +36 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +39 -0
- package/dist/stripe.js.map +1 -0
- package/dist/tax.d.ts +29 -0
- package/dist/tax.d.ts.map +1 -0
- package/dist/tax.js +39 -0
- package/dist/tax.js.map +1 -0
- package/package.json +171 -4
- package/src/adyen-fx.ts +154 -0
- package/src/adyen.ts +46 -0
- package/src/allocate.ts +393 -0
- package/src/bag.ts +170 -0
- package/src/braintree.ts +71 -0
- package/src/cash.ts +115 -0
- package/src/checkout.ts +77 -0
- package/src/convert.ts +159 -0
- package/src/currency.ts +61 -0
- package/src/decimal.ts +283 -0
- package/src/ecb.ts +162 -0
- package/src/exact.ts +182 -0
- package/src/fetch.ts +127 -0
- package/src/fixed.ts +558 -0
- package/src/fn.ts +80 -0
- package/src/format.ts +106 -0
- package/src/generated/adyen.ts +144 -0
- package/src/generated/braintree.ts +140 -0
- package/src/generated/cash.ts +375 -0
- package/src/generated/checkout.ts +151 -0
- package/src/generated/iso4217.ts +179 -0
- package/src/generated/kernel.ts +230 -0
- package/src/generated/klarna.ts +21 -0
- package/src/generated/paypal.ts +31 -0
- package/src/generated/stripe.ts +150 -0
- package/src/index.ts +37 -0
- package/src/klarna.ts +54 -0
- package/src/money.ts +781 -0
- package/src/parse.ts +144 -0
- package/src/paypal.ts +93 -0
- package/src/plugins/adyen.ts +25 -0
- package/src/plugins/all.ts +47 -0
- package/src/plugins/allocate.ts +53 -0
- package/src/plugins/braintree.ts +23 -0
- package/src/plugins/cash.ts +33 -0
- package/src/plugins/checkout.ts +32 -0
- package/src/plugins/convert.ts +50 -0
- package/src/plugins/exact.ts +23 -0
- package/src/plugins/fixed.ts +28 -0
- package/src/plugins/format.ts +28 -0
- package/src/plugins/klarna.ts +21 -0
- package/src/plugins/paypal.ts +29 -0
- package/src/plugins/prisma.ts +24 -0
- package/src/plugins/stripe.ts +29 -0
- package/src/plugins/tax.ts +41 -0
- package/src/prisma.ts +118 -0
- package/src/processor.ts +181 -0
- package/src/provider.ts +324 -0
- package/src/rate.ts +377 -0
- package/src/rounding-error.ts +8 -0
- package/src/rounding.ts +122 -0
- package/src/stripe-fx.ts +49 -0
- package/src/stripe.ts +69 -0
- 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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"}
|
package/dist/provider.js
ADDED
|
@@ -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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"}
|