@ducatjs/money 0.0.0 → 0.2.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 +1034 -3
- 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 +520 -0
- package/data/checkout.json +230 -0
- package/data/cldr-cash.json +101 -0
- package/data/historic.json +78 -0
- package/data/iso4217-list-one.xml +1956 -0
- package/data/iso4217-list-three.xml +1185 -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 +140 -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/boc.d.ts +31 -0
- package/dist/boc.d.ts.map +1 -0
- package/dist/boc.js +114 -0
- package/dist/boc.js.map +1 -0
- package/dist/boe.d.ts +33 -0
- package/dist/boe.d.ts.map +1 -0
- package/dist/boe.js +114 -0
- package/dist/boe.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 +64 -0
- package/dist/cash.d.ts.map +1 -0
- package/dist/cash.js +134 -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 +52 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +65 -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/drizzle/mysql.d.ts +105 -0
- package/dist/drizzle/mysql.d.ts.map +1 -0
- package/dist/drizzle/mysql.js +60 -0
- package/dist/drizzle/mysql.js.map +1 -0
- package/dist/drizzle/pg.d.ts +103 -0
- package/dist/drizzle/pg.d.ts.map +1 -0
- package/dist/drizzle/pg.js +60 -0
- package/dist/drizzle/pg.js.map +1 -0
- package/dist/drizzle/sqlite.d.ts +102 -0
- package/dist/drizzle/sqlite.d.ts.map +1 -0
- package/dist/drizzle/sqlite.js +59 -0
- package/dist/drizzle/sqlite.js.map +1 -0
- package/dist/ecb.d.ts +52 -0
- package/dist/ecb.d.ts.map +1 -0
- package/dist/ecb.js +285 -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/fed.d.ts +35 -0
- package/dist/fed.d.ts.map +1 -0
- package/dist/fed.js +134 -0
- package/dist/fed.js.map +1 -0
- package/dist/fetch.d.ts +103 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +222 -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 +34 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +37 -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 +14 -0
- package/dist/generated/cash.d.ts.map +1 -0
- package/dist/generated/cash.js +465 -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/historic.d.ts +337 -0
- package/dist/generated/historic.d.ts.map +1 -0
- package/dist/generated/historic.js +66 -0
- package/dist/generated/historic.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/historic.d.ts +40 -0
- package/dist/historic.d.ts.map +1 -0
- package/dist/historic.js +43 -0
- package/dist/historic.js.map +1 -0
- package/dist/history.d.ts +96 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +213 -0
- package/dist/history.js.map +1 -0
- package/dist/hmrc.d.ts +28 -0
- package/dist/hmrc.d.ts.map +1 -0
- package/dist/hmrc.js +108 -0
- package/dist/hmrc.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/kysely.d.ts +120 -0
- package/dist/kysely.d.ts.map +1 -0
- package/dist/kysely.js +309 -0
- package/dist/kysely.js.map +1 -0
- package/dist/money.d.ts +302 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +703 -0
- package/dist/money.js.map +1 -0
- package/dist/parse.d.ts +55 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +419 -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/pg.d.ts +100 -0
- package/dist/pg.d.ts.map +1 -0
- package/dist/pg.js +343 -0
- package/dist/pg.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 +29 -0
- package/dist/plugins/all.d.ts.map +1 -0
- package/dist/plugins/all.js +30 -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 +19 -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 +20 -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/policy.d.ts +34 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +90 -0
- package/dist/policy.js.map +1 -0
- package/dist/prisma.d.ts +77 -0
- package/dist/prisma.d.ts.map +1 -0
- package/dist/prisma.js +76 -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 +103 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +280 -0
- package/dist/provider.js.map +1 -0
- package/dist/rate-text.d.ts +38 -0
- package/dist/rate-text.d.ts.map +1 -0
- package/dist/rate-text.js +143 -0
- package/dist/rate-text.js.map +1 -0
- package/dist/rate.d.ts +175 -0
- package/dist/rate.d.ts.map +1 -0
- package/dist/rate.js +371 -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/schema.d.ts +43 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +77 -0
- package/dist/schema.js.map +1 -0
- package/dist/sql.d.ts +54 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +131 -0
- package/dist/sql.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 +33 -0
- package/dist/tax.d.ts.map +1 -0
- package/dist/tax.js +55 -0
- package/dist/tax.js.map +1 -0
- package/dist/testing/arbitraries.d.ts +43 -0
- package/dist/testing/arbitraries.d.ts.map +1 -0
- package/dist/testing/arbitraries.js +98 -0
- package/dist/testing/arbitraries.js.map +1 -0
- package/dist/testing/index.d.ts +67 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +159 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/jest.d.ts +2 -0
- package/dist/testing/jest.d.ts.map +1 -0
- package/dist/testing/jest.js +15 -0
- package/dist/testing/jest.js.map +1 -0
- package/dist/testing/vitest.d.ts +7 -0
- package/dist/testing/vitest.d.ts.map +1 -0
- package/dist/testing/vitest.js +12 -0
- package/dist/testing/vitest.js.map +1 -0
- package/dist/valibot.d.ts +33 -0
- package/dist/valibot.d.ts.map +1 -0
- package/dist/valibot.js +61 -0
- package/dist/valibot.js.map +1 -0
- package/dist/zod.d.ts +36 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +58 -0
- package/dist/zod.js.map +1 -0
- package/package.json +280 -5
- package/src/adyen-fx.ts +175 -0
- package/src/adyen.ts +46 -0
- package/src/allocate.ts +393 -0
- package/src/bag.ts +170 -0
- package/src/boc.ts +136 -0
- package/src/boe.ts +131 -0
- package/src/braintree.ts +71 -0
- package/src/cash.ts +183 -0
- package/src/checkout.ts +77 -0
- package/src/convert.ts +177 -0
- package/src/currency.ts +61 -0
- package/src/decimal.ts +283 -0
- package/src/drizzle/mysql.ts +78 -0
- package/src/drizzle/pg.ts +78 -0
- package/src/drizzle/sqlite.ts +70 -0
- package/src/ecb.ts +328 -0
- package/src/exact.ts +182 -0
- package/src/fed.ts +146 -0
- package/src/fetch.ts +306 -0
- package/src/fixed.ts +558 -0
- package/src/fn.ts +94 -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 +478 -0
- package/src/generated/checkout.ts +151 -0
- package/src/generated/historic.ts +67 -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/historic.ts +77 -0
- package/src/history.ts +333 -0
- package/src/hmrc.ts +125 -0
- package/src/index.ts +41 -0
- package/src/klarna.ts +54 -0
- package/src/kysely.ts +588 -0
- package/src/money.ts +979 -0
- package/src/parse.ts +511 -0
- package/src/paypal.ts +93 -0
- package/src/pg.ts +410 -0
- package/src/plugins/adyen.ts +25 -0
- package/src/plugins/all.ts +57 -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 +49 -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 +40 -0
- package/src/policy.ts +124 -0
- package/src/prisma.ts +199 -0
- package/src/processor.ts +181 -0
- package/src/provider.ts +389 -0
- package/src/rate-text.ts +151 -0
- package/src/rate.ts +546 -0
- package/src/rounding-error.ts +8 -0
- package/src/rounding.ts +122 -0
- package/src/schema.ts +119 -0
- package/src/sql.ts +178 -0
- package/src/stripe-fx.ts +49 -0
- package/src/stripe.ts +69 -0
- package/src/tax.ts +102 -0
- package/src/testing/arbitraries.ts +134 -0
- package/src/testing/index.ts +247 -0
- package/src/testing/jest.ts +18 -0
- package/src/testing/vitest.ts +18 -0
- package/src/valibot.ts +87 -0
- package/src/zod.ts +78 -0
package/dist/provider.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { CurrencyMismatchError } from './money.js';
|
|
2
|
+
import { exchangeRate, hasEnded, hasStarted, 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
|
+
* A query with an `asOf` gets nothing from a rate that doesn't apply then: one dated after it, one
|
|
62
|
+
* published (`availableAt`) after it, or one whose `validUntil` has passed. Days are UTC days, and
|
|
63
|
+
* a date query covers the whole day: without an `availableAt`, a query at any time on the rate's
|
|
64
|
+
* own day gets the rate, even before the source published it (the ECB publishes at about 15:00
|
|
65
|
+
* UTC). Give `availableAt` to rule that out.
|
|
66
|
+
*
|
|
67
|
+
* const ecb = rateTable({ base: 'EUR', source: 'ECB', asOf: '2026-11-06', rates: { USD: '1.1165' } });
|
|
68
|
+
*/
|
|
69
|
+
export function rateTable(spec) {
|
|
70
|
+
let rates;
|
|
71
|
+
const header = {};
|
|
72
|
+
if (isRateList(spec)) {
|
|
73
|
+
rates = [...spec];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
rates = [];
|
|
77
|
+
const missing = [];
|
|
78
|
+
for (const [currency, entry] of Object.entries(spec.rates)) {
|
|
79
|
+
if (entry === null) {
|
|
80
|
+
missing.push(currency);
|
|
81
|
+
}
|
|
82
|
+
else if (isRate(entry)) {
|
|
83
|
+
// A ready-made Rate is looked up under its own pair, so it must be the one its key says.
|
|
84
|
+
if (entry.from !== spec.base || entry.to !== currency) {
|
|
85
|
+
throw new CurrencyMismatchError(`${spec.base}→${currency}`, `${entry.from}→${entry.to}`);
|
|
86
|
+
}
|
|
87
|
+
rates.push(entry);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
rates.push(exchangeRate(spec.base, currency, entry, spec));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
header.base = spec.base;
|
|
94
|
+
if (spec.asOf !== undefined) {
|
|
95
|
+
header.asOf = toIsoString(spec.asOf);
|
|
96
|
+
}
|
|
97
|
+
if (spec.availableAt !== undefined) {
|
|
98
|
+
header.availableAt = toIsoString(spec.availableAt);
|
|
99
|
+
}
|
|
100
|
+
if (spec.validUntil !== undefined) {
|
|
101
|
+
header.validUntil = toIsoString(spec.validUntil);
|
|
102
|
+
}
|
|
103
|
+
if (missing.length > 0) {
|
|
104
|
+
header.missing = missing;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const byPair = new Map();
|
|
108
|
+
for (const rate of rates) {
|
|
109
|
+
byPair.set(`${rate.from}/${rate.to}`, rate);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...header,
|
|
113
|
+
rates,
|
|
114
|
+
rate(from, to, query) {
|
|
115
|
+
const rate = byPair.get(`${from}/${to}`);
|
|
116
|
+
if (rate === undefined) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
// The table's own dates count too: a ready-made Rate entry doesn't carry them.
|
|
120
|
+
const asOf = query?.asOf;
|
|
121
|
+
if (asOf !== undefined && (!appliesAt(rate, asOf) || !appliesAt(header, asOf))) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
// The key matched, so the rate's currencies are exactly `from` and `to`.
|
|
125
|
+
return rate;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Whether a rate (or a table) applies at `at`: dated, published and still valid then. */
|
|
130
|
+
function appliesAt(rate, at) {
|
|
131
|
+
if (rate.asOf !== undefined && dayOf(at) < dayOf(rate.asOf)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (rate.availableAt !== undefined && !hasStarted(rate.availableAt, at)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return rate.validUntil === undefined || !hasEnded(rate.validUntil, at);
|
|
138
|
+
}
|
|
139
|
+
function isRateList(spec) {
|
|
140
|
+
return Array.isArray(spec) && spec.every(isRate);
|
|
141
|
+
}
|
|
142
|
+
/** Asks each provider in turn; the first with an answer wins. */
|
|
143
|
+
export function chain(...providers) {
|
|
144
|
+
return {
|
|
145
|
+
rate(from, to, query) {
|
|
146
|
+
for (const provider of providers) {
|
|
147
|
+
const rate = provider.rate(from, to, query);
|
|
148
|
+
if (rate !== undefined) {
|
|
149
|
+
return rate;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Answers to→from lookups with the exact inverse of the provider's from→to rate. */
|
|
157
|
+
export function reversed(provider) {
|
|
158
|
+
return {
|
|
159
|
+
rate(from, to, query) {
|
|
160
|
+
return provider.rate(to, from, query)?.inverse();
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Triangulates through `base`: from→base, then base→to, composed exactly (converting rounds once).
|
|
166
|
+
* Each leg is looked up directly or as an exact inverse, so an ECB table (EUR→X only) answers X→Y.
|
|
167
|
+
*/
|
|
168
|
+
export function via(base, provider) {
|
|
169
|
+
function leg(from, to, query) {
|
|
170
|
+
return provider.rate(from, to, query) ?? provider.rate(to, from, query)?.inverse();
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
rate(from, to, query) {
|
|
174
|
+
if (from === to) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
if (from === base || to === base) {
|
|
178
|
+
return leg(from, to, query);
|
|
179
|
+
}
|
|
180
|
+
const toBase = leg(from, base, query);
|
|
181
|
+
if (toBase === undefined) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const fromBase = leg(base, to, query);
|
|
185
|
+
if (fromBase === undefined) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
return toBase.andThen(fromBase);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Dated tables from a loader you supply: `load('2026-11-06')` resolves to that day's table, or to
|
|
194
|
+
* undefined when there is none. Days are UTC days. Loaded tables are cached by day. Misses and
|
|
195
|
+
* failures are not cached, so a table that isn't published yet is tried again next time. A loader
|
|
196
|
+
* that throws fails the lookup rather than falling back to an older day, so an outage doesn't
|
|
197
|
+
* silently become a stale rate. The rates carry the day they're from. A table with an
|
|
198
|
+
* `availableAt` after an instant asked for is passed over, so a lookup at 09:00 on a publication
|
|
199
|
+
* day gets the day before.
|
|
200
|
+
*
|
|
201
|
+
* const history = snapshots(async (day) => parseEcbXml(await readFile(`ecb/${day}.xml`, 'utf8'))[0]);
|
|
202
|
+
* const fx = await history.at('2026-11-08'); // a Sunday: Friday 2026-11-06's table
|
|
203
|
+
*/
|
|
204
|
+
export function snapshots(load, options) {
|
|
205
|
+
const lookback = options?.lookback ?? 4;
|
|
206
|
+
const max = options?.max ?? 32;
|
|
207
|
+
if (!Number.isSafeInteger(lookback) || lookback < 0) {
|
|
208
|
+
throw new RangeError(`lookback must be a non-negative integer, got ${lookback}`);
|
|
209
|
+
}
|
|
210
|
+
if (!Number.isSafeInteger(max) || max < 1) {
|
|
211
|
+
throw new RangeError(`max must be a positive integer, got ${max}`);
|
|
212
|
+
}
|
|
213
|
+
// Map iteration order is insertion order, so re-inserting on every use makes it an LRU.
|
|
214
|
+
const cache = new Map();
|
|
215
|
+
// Only drop the entry this load created: a newer load for the same day may have replaced it.
|
|
216
|
+
function forget(day, loading) {
|
|
217
|
+
if (cache.get(day) === loading) {
|
|
218
|
+
cache.delete(day);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function loadDay(day) {
|
|
222
|
+
let pending = cache.get(day);
|
|
223
|
+
if (pending === undefined) {
|
|
224
|
+
const loading = load(day).then((table) => {
|
|
225
|
+
if (table === undefined) {
|
|
226
|
+
forget(day, loading);
|
|
227
|
+
}
|
|
228
|
+
return table;
|
|
229
|
+
});
|
|
230
|
+
loading.catch(() => forget(day, loading));
|
|
231
|
+
pending = loading;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
cache.delete(day);
|
|
235
|
+
}
|
|
236
|
+
cache.set(day, pending);
|
|
237
|
+
if (cache.size > max) {
|
|
238
|
+
const leastRecentlyUsed = cache.keys().next().value;
|
|
239
|
+
cache.delete(leastRecentlyUsed);
|
|
240
|
+
}
|
|
241
|
+
return pending;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
async at(asOf) {
|
|
245
|
+
const firstDay = dayOf(asOf);
|
|
246
|
+
const firstDayMs = Date.parse(`${firstDay}T00:00:00Z`);
|
|
247
|
+
if (Number.isNaN(firstDayMs)) {
|
|
248
|
+
throw new RangeError(`Invalid asOf ${String(asOf)}`);
|
|
249
|
+
}
|
|
250
|
+
for (let i = 0; i <= lookback; i++) {
|
|
251
|
+
const day = new Date(firstDayMs - i * DAY_MS).toISOString().slice(0, 10);
|
|
252
|
+
// Sequential on purpose: stop at the first day that has a table, without loading older
|
|
253
|
+
// days. oxlint-disable-next-line no-await-in-loop
|
|
254
|
+
const table = await loadDay(day);
|
|
255
|
+
// A table not yet published at the instant asked for is passed over like a missing one.
|
|
256
|
+
const availableAt = table?.availableAt;
|
|
257
|
+
if (table !== undefined && (availableAt === undefined || hasStarted(availableAt, asOf))) {
|
|
258
|
+
return table;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
throw new RateUnavailableError('*', '*', firstDay);
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Snapshots over tables you already hold (e.g. an ECB 90-day history), keyed by their asOf day.
|
|
267
|
+
*
|
|
268
|
+
* const history = snapshotsFrom(parseEcbXml(xml90d));
|
|
269
|
+
* const fx = await history.at('2026-11-08');
|
|
270
|
+
*/
|
|
271
|
+
export function snapshotsFrom(tables, options) {
|
|
272
|
+
const byDay = new Map();
|
|
273
|
+
for (const table of tables) {
|
|
274
|
+
if (table.asOf !== undefined) {
|
|
275
|
+
byDay.set(dayOf(table.asOf), table);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return snapshots(async (day) => byDay.get(day), options);
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* A published rate cell: the decimal text, or null when the source marks the value as missing
|
|
4
|
+
* (holidays, `ND`, `N/A`). Anything else throws SyntaxError: a cell that can't be read must not
|
|
5
|
+
* look like "not quoted".
|
|
6
|
+
*/
|
|
7
|
+
export declare function readRateCell(cell: string, where: string): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
* The published digits of a rate from a feed that strips trailing zeros, given the number of
|
|
11
|
+
* decimals the publisher states: '1.175' with 4 decimals is '1.1750'. Text with more decimals than
|
|
12
|
+
* stated is kept as it is, since dropping digits would change the rate.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withDecimals(text: string, decimals: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
* The records of a CSV file, RFC 4180 style: a cell may be quoted ("a, b", with "" for a quote),
|
|
18
|
+
* and a quote anywhere else, or one left open, is a SyntaxError, so a stray quote can't swallow
|
|
19
|
+
* the lines after it. Blank lines are skipped.
|
|
20
|
+
*/
|
|
21
|
+
export declare function csvRows(text: string): string[][];
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
* Whether `text` is a real calendar day in ISO form. '2026-02-30' is not, although Date.parse rolls
|
|
25
|
+
* it over to 2 March.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isCalendarDay(text: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* @internal
|
|
30
|
+
* Records a currency's rate (or null, missing) for a day. The same currency twice on one day is
|
|
31
|
+
* either a repeat, which is fine, or a contradiction, which throws: a parser never picks one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function addDayRate(days: Map<string, Record<string, string | null>>, day: string, currency: string, rate: string | null): void;
|
|
34
|
+
/** @internal Dated tables, newest first (ISO dates sort as strings). */
|
|
35
|
+
export declare function newestFirst<T extends {
|
|
36
|
+
readonly asOf?: string;
|
|
37
|
+
}>(tables: T[]): T[];
|
|
38
|
+
//# sourceMappingURL=rate-text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-text.d.ts","sourceRoot":"","sources":["../src/rate-text.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASvE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYnE;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAuDhD;AAID;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,EAChD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GAAG,IAAI,GAClB,IAAI,CAWN;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAElF"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Reading rate cells from published files, shared by the source parsers so that every source
|
|
2
|
+
// handles missing values and stripped digits the same way.
|
|
3
|
+
// What sources write for "no value this day": blank (ECB CSV, BoE), ND (Fed H.10), N/A (ECB CSV),
|
|
4
|
+
// NA (IMF), and dashes and NaN in other files. Compared upper-cased.
|
|
5
|
+
const MISSING_MARKERS = new Set(['', 'ND', 'N/A', 'NA', 'N.A.', '-', '–', '—', 'NAN', 'NULL']);
|
|
6
|
+
const PLAIN_DECIMAL = /^\d+(?:\.\d+)?$/;
|
|
7
|
+
/**
|
|
8
|
+
* @internal
|
|
9
|
+
* A published rate cell: the decimal text, or null when the source marks the value as missing
|
|
10
|
+
* (holidays, `ND`, `N/A`). Anything else throws SyntaxError: a cell that can't be read must not
|
|
11
|
+
* look like "not quoted".
|
|
12
|
+
*/
|
|
13
|
+
export function readRateCell(cell, where) {
|
|
14
|
+
const text = cell.trim();
|
|
15
|
+
if (MISSING_MARKERS.has(text.toUpperCase())) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (!PLAIN_DECIMAL.test(text)) {
|
|
19
|
+
throw new SyntaxError(`Not a rate: ${JSON.stringify(cell)} (${where})`);
|
|
20
|
+
}
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @internal
|
|
25
|
+
* The published digits of a rate from a feed that strips trailing zeros, given the number of
|
|
26
|
+
* decimals the publisher states: '1.175' with 4 decimals is '1.1750'. Text with more decimals than
|
|
27
|
+
* stated is kept as it is, since dropping digits would change the rate.
|
|
28
|
+
*/
|
|
29
|
+
export function withDecimals(text, decimals) {
|
|
30
|
+
if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 30) {
|
|
31
|
+
throw new RangeError(`decimals must be an integer from 0 to 30, got ${decimals}`);
|
|
32
|
+
}
|
|
33
|
+
const point = text.indexOf('.');
|
|
34
|
+
const present = point === -1 ? 0 : text.length - point - 1;
|
|
35
|
+
if (present >= decimals) {
|
|
36
|
+
return text;
|
|
37
|
+
}
|
|
38
|
+
const integerPart = point === -1 ? text : text.slice(0, point);
|
|
39
|
+
const fraction = point === -1 ? '' : text.slice(point + 1);
|
|
40
|
+
return `${integerPart}.${fraction.padEnd(decimals, '0')}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @internal
|
|
44
|
+
* The records of a CSV file, RFC 4180 style: a cell may be quoted ("a, b", with "" for a quote),
|
|
45
|
+
* and a quote anywhere else, or one left open, is a SyntaxError, so a stray quote can't swallow
|
|
46
|
+
* the lines after it. Blank lines are skipped.
|
|
47
|
+
*/
|
|
48
|
+
export function csvRows(text) {
|
|
49
|
+
const rows = [];
|
|
50
|
+
let row = [];
|
|
51
|
+
let cell = '';
|
|
52
|
+
// 'start' of a cell, inside an unquoted one, inside quotes, or just after the closing quote
|
|
53
|
+
let state = 'start';
|
|
54
|
+
const endRow = () => {
|
|
55
|
+
row.push(cell);
|
|
56
|
+
if (row.length > 1 || row[0].trim() !== '') {
|
|
57
|
+
rows.push(row);
|
|
58
|
+
}
|
|
59
|
+
row = [];
|
|
60
|
+
cell = '';
|
|
61
|
+
state = 'start';
|
|
62
|
+
};
|
|
63
|
+
for (let i = 0; i < text.length; i++) {
|
|
64
|
+
const char = text[i];
|
|
65
|
+
if (state === 'quoted') {
|
|
66
|
+
if (char !== '"') {
|
|
67
|
+
cell += char;
|
|
68
|
+
}
|
|
69
|
+
else if (text[i + 1] === '"') {
|
|
70
|
+
cell += '"';
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
state = 'closed';
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === ',') {
|
|
79
|
+
row.push(cell);
|
|
80
|
+
cell = '';
|
|
81
|
+
state = 'start';
|
|
82
|
+
}
|
|
83
|
+
else if (char === '\n') {
|
|
84
|
+
endRow();
|
|
85
|
+
}
|
|
86
|
+
else if (char === '\r' && text[i + 1] === '\n') {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
else if (char === '"' && state === 'start') {
|
|
90
|
+
state = 'quoted';
|
|
91
|
+
}
|
|
92
|
+
else if (char === '"' || state === 'closed') {
|
|
93
|
+
throw new SyntaxError(`Malformed CSV: unexpected ${JSON.stringify(char)} on line ${rows.length + 1}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
cell += char;
|
|
97
|
+
state = 'plain';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (state === 'quoted') {
|
|
101
|
+
throw new SyntaxError('Malformed CSV: a quoted cell is never closed');
|
|
102
|
+
}
|
|
103
|
+
if (cell !== '' || row.length > 0) {
|
|
104
|
+
endRow();
|
|
105
|
+
}
|
|
106
|
+
return rows;
|
|
107
|
+
}
|
|
108
|
+
const ISO_DAY = /^(\d{4})-(\d\d)-(\d\d)$/;
|
|
109
|
+
/**
|
|
110
|
+
* @internal
|
|
111
|
+
* Whether `text` is a real calendar day in ISO form. '2026-02-30' is not, although Date.parse rolls
|
|
112
|
+
* it over to 2 March.
|
|
113
|
+
*/
|
|
114
|
+
export function isCalendarDay(text) {
|
|
115
|
+
const match = ISO_DAY.exec(text);
|
|
116
|
+
if (match === null) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
120
|
+
return date.toISOString().slice(0, 10) === text;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* @internal
|
|
124
|
+
* Records a currency's rate (or null, missing) for a day. The same currency twice on one day is
|
|
125
|
+
* either a repeat, which is fine, or a contradiction, which throws: a parser never picks one.
|
|
126
|
+
*/
|
|
127
|
+
export function addDayRate(days, day, currency, rate) {
|
|
128
|
+
let rates = days.get(day);
|
|
129
|
+
if (rates === undefined) {
|
|
130
|
+
rates = {};
|
|
131
|
+
days.set(day, rates);
|
|
132
|
+
}
|
|
133
|
+
const earlier = rates[currency];
|
|
134
|
+
if (earlier !== undefined && earlier !== rate) {
|
|
135
|
+
throw new SyntaxError(`Two different values for ${currency} on ${day}: ${earlier} and ${rate}`);
|
|
136
|
+
}
|
|
137
|
+
rates[currency] = rate;
|
|
138
|
+
}
|
|
139
|
+
/** @internal Dated tables, newest first (ISO dates sort as strings). */
|
|
140
|
+
export function newestFirst(tables) {
|
|
141
|
+
return tables.sort((table, other) => other.asOf.localeCompare(table.asOf));
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=rate-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-text.js","sourceRoot":"","sources":["../src/rate-text.ts"],"names":[],"mappings":"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"}
|
package/dist/rate.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { type Factor, type Ratio } from './decimal.js';
|
|
2
|
+
import { type CurrencyInput, type Money, type RoundingInput, type RoundingPolicy } 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
|
+
* How a source published a rate, kept with the Rate for audit and display. The published direction
|
|
8
|
+
* is `from` → `to`, or `to` → `from` when `inverted`.
|
|
9
|
+
*/
|
|
10
|
+
export interface RateQuote {
|
|
11
|
+
/** The number exactly as published, trailing zeros included ('136.80'). */
|
|
12
|
+
readonly rate: string;
|
|
13
|
+
/**
|
|
14
|
+
* How many units of the published direction's first currency the number is for: 100 for "NOK
|
|
15
|
+
* per 100 JPY". Omitted for 1.
|
|
16
|
+
*/
|
|
17
|
+
readonly per?: number;
|
|
18
|
+
/** The source quoted `to` → `from`; this Rate is its exact inverse. */
|
|
19
|
+
readonly inverted?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Where a rate came from. `asOf`, `availableAt` and `validUntil` are ISO-8601 dates ('2026-11-06')
|
|
23
|
+
* or instants.
|
|
24
|
+
*/
|
|
25
|
+
export interface RateInfo {
|
|
26
|
+
readonly source?: string;
|
|
27
|
+
/** The date or time the rate applies to. */
|
|
28
|
+
readonly asOf?: string | Date;
|
|
29
|
+
/**
|
|
30
|
+
* When the rate was published. A lookup for an earlier time doesn't get it, so a historical
|
|
31
|
+
* lookup sees only what was known then. A date means from the start of that UTC day.
|
|
32
|
+
*/
|
|
33
|
+
readonly availableAt?: string | Date;
|
|
34
|
+
/** The last date or time the rate applies to. A date is valid all day (UTC). */
|
|
35
|
+
readonly validUntil?: string | Date;
|
|
36
|
+
readonly type?: RateType;
|
|
37
|
+
/** The rate as the source published it. */
|
|
38
|
+
readonly quote?: RateQuote;
|
|
39
|
+
}
|
|
40
|
+
/** Options for converting an amount. */
|
|
41
|
+
export interface ConvertOptions {
|
|
42
|
+
/** Result scale (default: the target currency's exponent). */
|
|
43
|
+
readonly scale?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Throw RateExpiredError when the rate has a `validUntil` that has passed (by the system clock).
|
|
46
|
+
* Off by default: ducat reads no clock unless asked to.
|
|
47
|
+
*/
|
|
48
|
+
readonly strictValidity?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Thrown by a strict-validity conversion with a rate past its `validUntil`. */
|
|
51
|
+
export declare class RateExpiredError extends RangeError {
|
|
52
|
+
readonly validUntil: string;
|
|
53
|
+
constructor(from: string, to: string, validUntil: string);
|
|
54
|
+
}
|
|
55
|
+
/** The JSON form of a Rate: the exact decimal string, or "num/den" when it doesn't terminate. */
|
|
56
|
+
export interface RateJSON {
|
|
57
|
+
from: string;
|
|
58
|
+
to: string;
|
|
59
|
+
rate: string;
|
|
60
|
+
source?: string;
|
|
61
|
+
asOf?: string;
|
|
62
|
+
availableAt?: string;
|
|
63
|
+
validUntil?: string;
|
|
64
|
+
type?: string;
|
|
65
|
+
/** Present only when it says more than `rate` does: a `per` or an inverted quote. */
|
|
66
|
+
quote?: RateQuote;
|
|
67
|
+
derivedFrom?: RateJSON[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* @internal
|
|
71
|
+
* A date stays as given ('2026-11-06'); an instant becomes UTC ISO ('2026-11-06T15:00:00.000Z'), so
|
|
72
|
+
* that times with different offsets compare correctly as strings.
|
|
73
|
+
*/
|
|
74
|
+
export declare function toIsoString(time: string | Date): string;
|
|
75
|
+
/**
|
|
76
|
+
* @internal
|
|
77
|
+
* Whether `time` has come by `at`. A date `at` means the whole of that UTC day, so anything on or
|
|
78
|
+
* before that day counts; a date `time` means the start of its day.
|
|
79
|
+
*/
|
|
80
|
+
export declare function hasStarted(time: string, at: string | Date): boolean;
|
|
81
|
+
/** @internal Whether `validUntil` (a date: valid all day, UTC) has passed at `at`. */
|
|
82
|
+
export declare function hasEnded(validUntil: string, at: string | Date): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* minor × rate × 10^scaleShift, rounded once with `rounding`. This is the kernel of every
|
|
85
|
+
* conversion: `scaleShift` is the result scale minus the input scale. Pure, and `rate.den > 0` by
|
|
86
|
+
* construction.
|
|
87
|
+
*/
|
|
88
|
+
export declare function multiplyByRatioAtScale(minor: number | bigint, rate: Ratio, scaleShift: number, rounding: RoundingMode): number | bigint;
|
|
89
|
+
/**
|
|
90
|
+
* An exact exchange rate: one unit of `from` is worth `ratio` units of `to`. Build it with
|
|
91
|
+
* `exchangeRate()`. Derived rates (`inverse`, `andThen`, `withMargin`, `round`) keep their inputs
|
|
92
|
+
* in `derivedFrom`, the audit chain. Their `asOf` is the oldest input's, so staleness can't hide,
|
|
93
|
+
* and their `validUntil` is the earliest.
|
|
94
|
+
*/
|
|
95
|
+
export declare class Rate<From extends string = string, To extends string = string> {
|
|
96
|
+
readonly from: From;
|
|
97
|
+
readonly to: To;
|
|
98
|
+
readonly ratio: Ratio;
|
|
99
|
+
readonly source?: string;
|
|
100
|
+
readonly asOf?: string;
|
|
101
|
+
readonly availableAt?: string;
|
|
102
|
+
readonly validUntil?: string;
|
|
103
|
+
readonly type?: RateType;
|
|
104
|
+
readonly quote?: RateQuote;
|
|
105
|
+
readonly derivedFrom?: readonly Rate[];
|
|
106
|
+
/** @internal use exchangeRate() */
|
|
107
|
+
constructor(from: From, to: To, rate: Ratio, info?: RateInfo, derivedFrom?: readonly Rate[]);
|
|
108
|
+
/** The exact reciprocal, `to` → `from`. A bid becomes an ask and vice versa. */
|
|
109
|
+
inverse(): Rate<To, From>;
|
|
110
|
+
/**
|
|
111
|
+
* The exact composite `from` → `next.to`. Nothing is rounded: converting with it rounds once.
|
|
112
|
+
* (Named `andThen`, not `then`, so that a Rate is never mistaken for a promise.)
|
|
113
|
+
*/
|
|
114
|
+
andThen<Next extends string>(next: Rate<To, Next>): Rate<From, Next>;
|
|
115
|
+
/** This rate × (1 + margin), exactly: `withMargin('-0.02')` gives 2% less `to` per `from`. */
|
|
116
|
+
withMargin(margin: Factor): Rate<From, To>;
|
|
117
|
+
/**
|
|
118
|
+
* A new Rate rounded to `places` decimals, for businesses that quote rounded (e.g. inverse)
|
|
119
|
+
* rates.
|
|
120
|
+
*/
|
|
121
|
+
round(places: number, rounding?: RoundingMode): Rate<From, To>;
|
|
122
|
+
/** The rate with exactly `places` decimals, for display. The Rate itself keeps every digit. */
|
|
123
|
+
toDecimal(places: number, rounding?: RoundingMode): string;
|
|
124
|
+
/**
|
|
125
|
+
* The exact rate: '1.1165', or '2000/2233' when its decimal expansion doesn't terminate. A rate
|
|
126
|
+
* published one unit for one unit keeps the published digits ('136.80').
|
|
127
|
+
*/
|
|
128
|
+
toString(): string;
|
|
129
|
+
/** The rate, its metadata and its audit chain as JSON. `rateFromJSON` reverses it. */
|
|
130
|
+
toJSON(): RateJSON;
|
|
131
|
+
/**
|
|
132
|
+
* Whether `validUntil` has passed at `now` (default: the system clock). A date is valid all day
|
|
133
|
+
* (UTC).
|
|
134
|
+
*/
|
|
135
|
+
isExpired(now?: string | Date): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Whether the rate had been published by `at`: always, without an `availableAt`. A date `at`
|
|
138
|
+
* means by the end of that UTC day.
|
|
139
|
+
*/
|
|
140
|
+
isAvailable(at: string | Date): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Converts `money`, rounding once: with a mode, to the target currency's exponent (or
|
|
143
|
+
* `options.scale`); with a rounding policy, to the policy's scale and increment.
|
|
144
|
+
*/
|
|
145
|
+
convert(money: Money<From>, rounding?: RoundingInput, options?: ConvertOptions): Money<To>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* @internal
|
|
149
|
+
* money × rate in `to`, rounded once by a policy. The amount is relabelled `to` at its own scale,
|
|
150
|
+
* so the policy rounds the product exactly as it would a multiplication in `to`: to its scale
|
|
151
|
+
* (default `to`'s exponent) and increment, checking its currency against `to`.
|
|
152
|
+
*/
|
|
153
|
+
export declare function convertWithPolicy<To extends string>(money: Money, to: To, rate: Ratio, policy: RoundingPolicy, options?: ConvertOptions): Money<To>;
|
|
154
|
+
/**
|
|
155
|
+
* An exact rate pinned to a currency pair, so converting the wrong currency is a type error. Give
|
|
156
|
+
* the rate as a decimal string ('1.1165', every digit kept), a "num/den" string, a bigint or a
|
|
157
|
+
* Ratio. Numbers are read at their shortest decimal form (1.234 is exactly 1234/1000).
|
|
158
|
+
*
|
|
159
|
+
* const eurUsd = exchangeRate('EUR', 'USD', '1.1165', { source: 'ECB', asOf: '2026-11-06' });
|
|
160
|
+
* eurUsd.convert(money('100', 'EUR'), 'halfEven'); // 111.65 USD
|
|
161
|
+
* eurUsd.inverse().ratio // 2000/2233, exactly
|
|
162
|
+
*/
|
|
163
|
+
export declare function exchangeRate<From extends CurrencyInput, To extends CurrencyInput>(from: From, to: To, rate: Factor, info?: RateInfo): Rate<From, To>;
|
|
164
|
+
/** Rebuilds a Rate, with its audit chain, from `rate.toJSON()`. */
|
|
165
|
+
export declare function rateFromJSON(json: RateJSON): Rate;
|
|
166
|
+
/** Whether `value` is a Rate. */
|
|
167
|
+
export declare function isRate(value: unknown): value is Rate;
|
|
168
|
+
/**
|
|
169
|
+
* The exact rate a processor actually applied: settled ÷ presented. Use it to audit a quoted rate,
|
|
170
|
+
* which after the processor's own rounding often doesn't reproduce the settled amount.
|
|
171
|
+
*
|
|
172
|
+
* impliedRate(money('10.00', 'EUR'), money('12.34', 'USD')) // EUR→USD 1.234
|
|
173
|
+
*/
|
|
174
|
+
export declare function impliedRate<From extends string, To extends string>(presented: Money<From>, settled: Money<To>, info?: RateInfo): Rate<From, To>;
|
|
175
|
+
//# sourceMappingURL=rate.d.ts.map
|