@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/src/currency.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ISO_EXPONENTS, type IsoCurrencyCode } from './generated/iso4217.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Augment this interface to add non-ISO currencies to the `CurrencyCode` type:
|
|
5
|
+
*
|
|
6
|
+
* declare module '@ducatjs/money' { interface CustomCurrencies { BTC: true; USDC: true } }
|
|
7
|
+
* defineCurrency('BTC', 8);
|
|
8
|
+
*/
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
10
|
+
export interface CustomCurrencies {}
|
|
11
|
+
|
|
12
|
+
/** A known currency code: ISO 4217, or one added to `CustomCurrencies`. */
|
|
13
|
+
export type CurrencyCode = IsoCurrencyCode | (keyof CustomCurrencies & string);
|
|
14
|
+
|
|
15
|
+
const custom = new Map<string, number>();
|
|
16
|
+
const iso = ISO_EXPONENTS as Readonly<Record<string, number>>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Registers a currency that is not in ISO 4217 (crypto, loyalty points, withdrawn codes you still
|
|
20
|
+
* hold balances in). ISO codes cannot be redefined: use an explicit `scale` for extra precision.
|
|
21
|
+
*/
|
|
22
|
+
export function defineCurrency(code: string, exponent: number): void {
|
|
23
|
+
if (!Number.isInteger(exponent) || exponent < 0 || exponent > 30) {
|
|
24
|
+
throw new RangeError(`Invalid exponent ${exponent}`);
|
|
25
|
+
}
|
|
26
|
+
if (typeof iso[code] === 'number') {
|
|
27
|
+
throw new TypeError(`${code} is an ISO 4217 currency and cannot be redefined`);
|
|
28
|
+
}
|
|
29
|
+
const prev = custom.get(code);
|
|
30
|
+
if (prev !== undefined && prev !== exponent) {
|
|
31
|
+
throw new TypeError(`${code} is already defined with exponent ${prev}`);
|
|
32
|
+
}
|
|
33
|
+
custom.set(code, exponent);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The minor-unit exponent of a currency (EUR 2, JPY 0, KWD 3). Throws for unknown codes. */
|
|
37
|
+
export function currencyExponent(code: string): number {
|
|
38
|
+
const isoExponent = iso[code];
|
|
39
|
+
if (typeof isoExponent === 'number') {
|
|
40
|
+
return isoExponent;
|
|
41
|
+
}
|
|
42
|
+
const customExponent = custom.get(code);
|
|
43
|
+
if (customExponent !== undefined) {
|
|
44
|
+
return customExponent;
|
|
45
|
+
}
|
|
46
|
+
throw new RangeError(
|
|
47
|
+
`Unknown currency "${code}" (not ISO 4217 and not registered with defineCurrency)`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Whether the code is an ISO 4217 currency or one registered with `defineCurrency`. */
|
|
52
|
+
export function isCurrency(code: string): code is CurrencyCode {
|
|
53
|
+
return typeof iso[code] === 'number' || custom.has(code);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
ISO_EXPONENTS,
|
|
58
|
+
ISO_4217_PUBLISHED,
|
|
59
|
+
ISO_NON_MONETARY,
|
|
60
|
+
type IsoCurrencyCode,
|
|
61
|
+
} from './generated/iso4217.js';
|
package/src/decimal.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Exact decimal-string handling. No floating point is used on amounts: numbers given as factors are
|
|
2
|
+
// converted through their shortest round-trip decimal representation (String(x)), never multiplied.
|
|
3
|
+
|
|
4
|
+
/** The largest minor-unit count held as a number (2^53 - 1); larger values are bigints. */
|
|
5
|
+
const MAX_SAFE_MINOR = Number.MAX_SAFE_INTEGER;
|
|
6
|
+
const MAXN = BigInt(MAX_SAFE_MINOR);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An exact rational factor num/den (den > 0). `n`/`d` hold the same values as numbers when both are
|
|
10
|
+
* safe.
|
|
11
|
+
*/
|
|
12
|
+
export interface Ratio {
|
|
13
|
+
readonly num: bigint;
|
|
14
|
+
readonly den: bigint;
|
|
15
|
+
/** num as a number, or NaN when it is not a safe integer */
|
|
16
|
+
readonly n: number;
|
|
17
|
+
/** den as a number, or NaN when it is not a safe integer */
|
|
18
|
+
readonly d: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Anything accepted where a multiplier, rate or percentage is expected. */
|
|
22
|
+
export type Factor = number | bigint | string | Ratio;
|
|
23
|
+
|
|
24
|
+
const POW10N: bigint[] = [];
|
|
25
|
+
/** 10^exponent as a bigint, cached. */
|
|
26
|
+
export function pow10n(exponent: number): bigint {
|
|
27
|
+
return (POW10N[exponent] ??= 10n ** BigInt(exponent));
|
|
28
|
+
}
|
|
29
|
+
/** 10^e as a number for 0 <= e <= 15 (all exact and safe). */
|
|
30
|
+
export const POW10 = [
|
|
31
|
+
1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15,
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A bigint as a number when it is a safe integer, else unchanged: the representation Money keeps.
|
|
36
|
+
*/
|
|
37
|
+
export const normalizeMinor = (value: bigint): number | bigint =>
|
|
38
|
+
value <= MAXN && value >= -MAXN ? Number(value) : value;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parses a plain decimal string ("-12.345", "+7", "0.50") into digits/10^scale. Returns the integer
|
|
42
|
+
* of all digits (number when safe, else bigint) and the count of fraction digits. Hand-rolled (no
|
|
43
|
+
* regex, no BigInt for short inputs): this is the hot path of `money("12.34", ...)`.
|
|
44
|
+
*/
|
|
45
|
+
export function parsePlainDecimal(
|
|
46
|
+
input: string,
|
|
47
|
+
): { digits: number | bigint; scale: number } | null {
|
|
48
|
+
const len = input.length;
|
|
49
|
+
let i = 0;
|
|
50
|
+
let neg = false;
|
|
51
|
+
let code = input.charCodeAt(0);
|
|
52
|
+
if (code === 45 /* - */ || code === 43 /* + */) {
|
|
53
|
+
neg = code === 45;
|
|
54
|
+
i = 1;
|
|
55
|
+
}
|
|
56
|
+
let acc = 0;
|
|
57
|
+
let digitsSeen = 0;
|
|
58
|
+
let scale = -1;
|
|
59
|
+
let big = false;
|
|
60
|
+
const start = i;
|
|
61
|
+
for (; i < len; i++) {
|
|
62
|
+
code = input.charCodeAt(i);
|
|
63
|
+
if (code >= 48 && code <= 57) {
|
|
64
|
+
digitsSeen++;
|
|
65
|
+
if (scale >= 0) {
|
|
66
|
+
scale++;
|
|
67
|
+
}
|
|
68
|
+
if (!big) {
|
|
69
|
+
// 15 digits always fit exactly in a double; beyond that use BigInt
|
|
70
|
+
if (digitsSeen > 15) {
|
|
71
|
+
big = true;
|
|
72
|
+
} else {
|
|
73
|
+
acc = acc * 10 + (code - 48);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else if (code === 46 /* . */ && scale < 0 && digitsSeen > 0) {
|
|
77
|
+
scale = 0;
|
|
78
|
+
} else {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// "", "-" and "12." are not decimals
|
|
83
|
+
if (digitsSeen === 0 || scale === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// parseDecimal reports fractions longer than MAX_DECIMAL_SCALE
|
|
87
|
+
if (scale > MAX_DECIMAL_SCALE) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (scale < 0) {
|
|
91
|
+
scale = 0;
|
|
92
|
+
}
|
|
93
|
+
if (big) {
|
|
94
|
+
const raw = input.slice(start).replace('.', '');
|
|
95
|
+
const value = BigInt(raw);
|
|
96
|
+
return { digits: normalizeMinor(neg ? -value : value), scale };
|
|
97
|
+
}
|
|
98
|
+
return { digits: neg ? 0 - acc : acc, scale };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const EXP_RE = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The largest exponent and number of fraction digits a decimal string may have. Far beyond any real
|
|
105
|
+
* amount or rate, and it keeps input such as "1e999999999" from building enormous powers of ten.
|
|
106
|
+
*/
|
|
107
|
+
export const MAX_DECIMAL_SCALE = 1000;
|
|
108
|
+
|
|
109
|
+
/** Parses a decimal string, allowing exponent notation ("1e-7", as produced by String(1e-7)). */
|
|
110
|
+
export function parseDecimal(input: string): { digits: bigint; scale: number } {
|
|
111
|
+
const match = EXP_RE.exec(input.trim());
|
|
112
|
+
if (!match) {
|
|
113
|
+
throw new SyntaxError(`Invalid decimal string "${input}"`);
|
|
114
|
+
}
|
|
115
|
+
const [, sign, int, frac = '', exp = '0'] = match;
|
|
116
|
+
const exponent = Number(exp);
|
|
117
|
+
let scale = frac.length - exponent;
|
|
118
|
+
if (!(Math.abs(exponent) <= MAX_DECIMAL_SCALE) || scale > MAX_DECIMAL_SCALE) {
|
|
119
|
+
// The input itself is not quoted: it may be huge.
|
|
120
|
+
throw new RangeError(`Decimal string exponent or fraction digits beyond ${MAX_DECIMAL_SCALE}`);
|
|
121
|
+
}
|
|
122
|
+
let digits = BigInt(int! + frac);
|
|
123
|
+
if (scale < 0) {
|
|
124
|
+
digits *= pow10n(-scale);
|
|
125
|
+
scale = 0;
|
|
126
|
+
}
|
|
127
|
+
return { digits: sign === '-' ? -digits : digits, scale };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function gcd(left: bigint, right: bigint): bigint {
|
|
131
|
+
if (left < 0n) {
|
|
132
|
+
left = -left;
|
|
133
|
+
}
|
|
134
|
+
// a temporary, not a destructuring swap: `[a, b] = [b, a % b]` builds an array per step until the
|
|
135
|
+
// optimiser removes it, and this loop runs for every ratio that is built
|
|
136
|
+
while (right) {
|
|
137
|
+
const remainder = left % right;
|
|
138
|
+
left = right;
|
|
139
|
+
right = remainder;
|
|
140
|
+
}
|
|
141
|
+
return left;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The value as a number when it is a safe integer, otherwise NaN (a Ratio's `n` and `d`). */
|
|
145
|
+
function safeNumberOrNaN(value: bigint): number {
|
|
146
|
+
return value <= MAXN && value >= -MAXN ? Number(value) : NaN;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function makeRatio(num: bigint, den: bigint): Ratio {
|
|
150
|
+
if (den === 0n) {
|
|
151
|
+
throw new RangeError('Division by zero');
|
|
152
|
+
}
|
|
153
|
+
if (den < 0n) {
|
|
154
|
+
num = -num;
|
|
155
|
+
den = -den;
|
|
156
|
+
}
|
|
157
|
+
// an integer (den 1) is already in lowest terms
|
|
158
|
+
if (den !== 1n) {
|
|
159
|
+
const divisor = gcd(num, den);
|
|
160
|
+
if (divisor > 1n) {
|
|
161
|
+
num /= divisor;
|
|
162
|
+
den /= divisor;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { num, den, n: safeNumberOrNaN(num), d: safeNumberOrNaN(den) };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cache = new Map<string, Ratio>();
|
|
169
|
+
const INT_RATIOS: Ratio[] = [];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds an exact ratio. `ratio('1.075')`, `ratio(3, 4)`, `ratio(10n)`, `ratio('1/3')`.
|
|
173
|
+
* Numbers are taken at their shortest decimal representation: `ratio(0.1)` is exactly 1/10.
|
|
174
|
+
* Pre-build ratios you use in hot loops; strings are also cached (up to 1024 entries).
|
|
175
|
+
*/
|
|
176
|
+
export function ratio(value: Factor, den?: Factor): Ratio {
|
|
177
|
+
if (den !== undefined) {
|
|
178
|
+
// two bigints (divide, percentages, tax): one reduction instead of three
|
|
179
|
+
if (typeof value === 'bigint' && typeof den === 'bigint') {
|
|
180
|
+
return makeRatio(value, den);
|
|
181
|
+
}
|
|
182
|
+
const numeratorRatio = ratio(value);
|
|
183
|
+
const denominatorRatio = ratio(den);
|
|
184
|
+
return makeRatio(
|
|
185
|
+
numeratorRatio.num * denominatorRatio.den,
|
|
186
|
+
numeratorRatio.den * denominatorRatio.num,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (typeof value === 'object') {
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === 'number') {
|
|
193
|
+
if (Number.isSafeInteger(value)) {
|
|
194
|
+
if (value >= 0 && value < 128) {
|
|
195
|
+
return (INT_RATIOS[value] ??= makeRatio(BigInt(value), 1n));
|
|
196
|
+
}
|
|
197
|
+
return makeRatio(BigInt(value), 1n);
|
|
198
|
+
}
|
|
199
|
+
if (!Number.isFinite(value)) {
|
|
200
|
+
throw new RangeError(`Invalid factor ${value}`);
|
|
201
|
+
}
|
|
202
|
+
value = String(value);
|
|
203
|
+
}
|
|
204
|
+
if (typeof value === 'bigint') {
|
|
205
|
+
return makeRatio(value, 1n);
|
|
206
|
+
}
|
|
207
|
+
let cached = cache.get(value);
|
|
208
|
+
if (cached === undefined) {
|
|
209
|
+
const slash = value.indexOf('/');
|
|
210
|
+
if (slash > 0) {
|
|
211
|
+
cached = ratio(value.slice(0, slash).trim(), value.slice(slash + 1).trim());
|
|
212
|
+
} else {
|
|
213
|
+
const { digits, scale } = parseDecimal(value);
|
|
214
|
+
cached = makeRatio(digits, pow10n(scale));
|
|
215
|
+
}
|
|
216
|
+
if (cache.size >= 1024) {
|
|
217
|
+
cache.clear();
|
|
218
|
+
}
|
|
219
|
+
cache.set(value, cached);
|
|
220
|
+
}
|
|
221
|
+
return cached;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** num/den -> exact decimal string if the expansion terminates, else null. */
|
|
225
|
+
export function ratioToDecimal(value: Ratio): string | null {
|
|
226
|
+
let remainingDenominator = value.den;
|
|
227
|
+
let twos = 0;
|
|
228
|
+
let fives = 0;
|
|
229
|
+
while (remainingDenominator % 2n === 0n) {
|
|
230
|
+
remainingDenominator /= 2n;
|
|
231
|
+
twos++;
|
|
232
|
+
}
|
|
233
|
+
while (remainingDenominator % 5n === 0n) {
|
|
234
|
+
remainingDenominator /= 5n;
|
|
235
|
+
fives++;
|
|
236
|
+
}
|
|
237
|
+
if (remainingDenominator !== 1n) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const scale = Math.max(twos, fives);
|
|
241
|
+
return formatScaled(value.num * (pow10n(scale) / value.den), scale);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Zero padding for the fraction digits, built once: indexing it is faster than padStart.
|
|
245
|
+
const ZPAD = Array.from({ length: 16 }, (_, length) => '0'.repeat(length));
|
|
246
|
+
|
|
247
|
+
/** Formats an integer at `scale` as a plain decimal string ("-0.05"). */
|
|
248
|
+
export function formatScaled(minor: number | bigint, scale: number): string {
|
|
249
|
+
if (typeof minor === 'number' && scale <= 15) {
|
|
250
|
+
if (scale === 0) {
|
|
251
|
+
return String(minor);
|
|
252
|
+
}
|
|
253
|
+
// integer/fraction split with exact integer arithmetic (the float quotient is corrected)
|
|
254
|
+
const magnitude = minor < 0 ? 0 - minor : minor;
|
|
255
|
+
const unit = POW10[scale]!;
|
|
256
|
+
if (magnitude + unit <= MAX_SAFE_MINOR) {
|
|
257
|
+
let whole = Math.floor(magnitude / unit);
|
|
258
|
+
let fraction = magnitude - whole * unit;
|
|
259
|
+
if (fraction < 0) {
|
|
260
|
+
whole -= 1;
|
|
261
|
+
fraction += unit;
|
|
262
|
+
} else if (fraction >= unit) {
|
|
263
|
+
whole += 1;
|
|
264
|
+
fraction -= unit;
|
|
265
|
+
}
|
|
266
|
+
let fractionDigits = String(fraction);
|
|
267
|
+
if (fractionDigits.length < scale) {
|
|
268
|
+
fractionDigits = ZPAD[scale - fractionDigits.length] + fractionDigits;
|
|
269
|
+
}
|
|
270
|
+
return (minor < 0 ? '-' : '') + whole + '.' + fractionDigits;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const neg = minor < 0;
|
|
274
|
+
let digits =
|
|
275
|
+
typeof minor === 'number' ? String(neg ? 0 - minor : minor) : (neg ? -minor : minor).toString();
|
|
276
|
+
if (scale > 0) {
|
|
277
|
+
if (digits.length <= scale) {
|
|
278
|
+
digits = '0'.repeat(scale - digits.length + 1) + digits;
|
|
279
|
+
}
|
|
280
|
+
digits = digits.slice(0, digits.length - scale) + '.' + digits.slice(digits.length - scale);
|
|
281
|
+
}
|
|
282
|
+
return neg ? '-' + digits : digits;
|
|
283
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ducatjs/money/drizzle/mysql`: Drizzle ORM column types for MySQL and MariaDB (mysql2,
|
|
3
|
+
* PlanetScale). Selects return `Money`, inserts and updates accept it, and the currency is part of
|
|
4
|
+
* the column's type.
|
|
5
|
+
*
|
|
6
|
+
* import { mysqlTable } from 'drizzle-orm/mysql-core';
|
|
7
|
+
* import { moneyMinor } from '@ducatjs/money/drizzle/mysql';
|
|
8
|
+
*
|
|
9
|
+
* const orders = mysqlTable('orders', { total: moneyMinor('EUR')('total_minor').notNull() });
|
|
10
|
+
*/
|
|
11
|
+
import { customType } from 'drizzle-orm/mysql-core';
|
|
12
|
+
import type { CurrencyInput, Money, MoneyJSON } from '../money.js';
|
|
13
|
+
import {
|
|
14
|
+
checkDecimalSize,
|
|
15
|
+
decimalFromDriver,
|
|
16
|
+
decimalToDriver,
|
|
17
|
+
DEFAULT_DECIMAL_SIZE,
|
|
18
|
+
jsonFromDriver,
|
|
19
|
+
jsonToDriver,
|
|
20
|
+
minorFromDriver,
|
|
21
|
+
minorToDriver,
|
|
22
|
+
type DecimalColumnSize,
|
|
23
|
+
} from '../sql.js';
|
|
24
|
+
|
|
25
|
+
export { DEFAULT_DECIMAL_SIZE, type DecimalColumnSize } from '../sql.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A BIGINT column of minor units in a fixed currency (`1234` is 12.34 EUR), read back as
|
|
29
|
+
* `Money<C>`. Exact up to the column's limit of ±9.2·10^18 minor units. mysql2 returns BIGINT as a
|
|
30
|
+
* number by default, which is exact only below 2^53; beyond that it throws, so set
|
|
31
|
+
* `supportBigNumbers: true` (with `bigNumberStrings: true`) on the connection for larger amounts.
|
|
32
|
+
* Writing another currency, or a value at a scale other than the currency's, throws.
|
|
33
|
+
*
|
|
34
|
+
* total: moneyMinor('EUR')('total_minor').notNull()
|
|
35
|
+
*/
|
|
36
|
+
export function moneyMinor<const C extends CurrencyInput>(currency: C) {
|
|
37
|
+
return customType<{ data: Money<C>; driverData: string | number | bigint }>({
|
|
38
|
+
dataType: () => 'bigint',
|
|
39
|
+
toDriver: (money) => minorToDriver(money, currency).toString(),
|
|
40
|
+
fromDriver: (value) => minorFromDriver(value, currency),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A DECIMAL(precision, scale) column, DECIMAL(19,4) by default, read back as `Money<C>`. The driver
|
|
46
|
+
* must return DECIMAL as a string (mysql2's default; don't set `decimalNumbers`): a number has been
|
|
47
|
+
* through a float and throws. Writing a value the column could not hold unchanged throws (outside
|
|
48
|
+
* strict mode MySQL would clamp it), and so does a column scale below the currency's exponent.
|
|
49
|
+
*
|
|
50
|
+
* price: moneyDecimal('EUR', { precision: 19, scale: 4 })('price').notNull()
|
|
51
|
+
*/
|
|
52
|
+
export function moneyDecimal<const C extends CurrencyInput>(
|
|
53
|
+
currency: C,
|
|
54
|
+
size: DecimalColumnSize = DEFAULT_DECIMAL_SIZE,
|
|
55
|
+
) {
|
|
56
|
+
checkDecimalSize(currency, size);
|
|
57
|
+
|
|
58
|
+
return customType<{ data: Money<C>; driverData: string }>({
|
|
59
|
+
dataType: () => `decimal(${size.precision}, ${size.scale})`,
|
|
60
|
+
toDriver: (money) => decimalToDriver(money, currency, size),
|
|
61
|
+
fromDriver: (value) => decimalFromDriver(value, currency),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A JSON column holding `{"amount":"12.34","currency":"EUR"}`, with the currency in each value.
|
|
67
|
+
* Pass a currency to fix it: then a value in another currency throws, on write and on read.
|
|
68
|
+
*
|
|
69
|
+
* refund: moneyJson()('refund')
|
|
70
|
+
*/
|
|
71
|
+
export function moneyJson<const C extends CurrencyInput = CurrencyInput>(currency?: C) {
|
|
72
|
+
return customType<{ data: Money<C>; driverData: string | MoneyJSON }>({
|
|
73
|
+
dataType: () => 'json',
|
|
74
|
+
toDriver: (money) => jsonToDriver(money, currency),
|
|
75
|
+
// mysql2 parses JSON columns itself; other drivers may return the text
|
|
76
|
+
fromDriver: (value) => jsonFromDriver(value, currency),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ducatjs/money/drizzle/pg`: Drizzle ORM column types for Postgres (node-postgres, postgres.js,
|
|
3
|
+
* PGlite, Neon). Selects return `Money`, inserts and updates accept it, and the currency is part of
|
|
4
|
+
* the column's type.
|
|
5
|
+
*
|
|
6
|
+
* import { pgTable } from 'drizzle-orm/pg-core';
|
|
7
|
+
* import { moneyMinor } from '@ducatjs/money/drizzle/pg';
|
|
8
|
+
*
|
|
9
|
+
* const orders = pgTable('orders', { total: moneyMinor('EUR')('total_minor').notNull() });
|
|
10
|
+
*/
|
|
11
|
+
import { customType } from 'drizzle-orm/pg-core';
|
|
12
|
+
import type { CurrencyInput, Money, MoneyJSON } from '../money.js';
|
|
13
|
+
import {
|
|
14
|
+
checkDecimalSize,
|
|
15
|
+
decimalFromDriver,
|
|
16
|
+
decimalToDriver,
|
|
17
|
+
DEFAULT_DECIMAL_SIZE,
|
|
18
|
+
jsonFromDriver,
|
|
19
|
+
jsonToDriver,
|
|
20
|
+
minorFromDriver,
|
|
21
|
+
minorToDriver,
|
|
22
|
+
type DecimalColumnSize,
|
|
23
|
+
} from '../sql.js';
|
|
24
|
+
|
|
25
|
+
export { DEFAULT_DECIMAL_SIZE, type DecimalColumnSize } from '../sql.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A BIGINT column of minor units in a fixed currency (`1234` is 12.34 EUR), read back as
|
|
29
|
+
* `Money<C>`. Exact up to the column's limit of ±9.2·10^18 minor units. Writing another currency, or
|
|
30
|
+
* a value at a scale other than the currency's, throws.
|
|
31
|
+
*
|
|
32
|
+
* total: moneyMinor('EUR')('total_minor').notNull()
|
|
33
|
+
*/
|
|
34
|
+
export function moneyMinor<const C extends CurrencyInput>(currency: C) {
|
|
35
|
+
return customType<{ data: Money<C>; driverData: string | number | bigint }>({
|
|
36
|
+
dataType: () => 'bigint',
|
|
37
|
+
// a string, not a bigint: node-postgres cannot bind a bigint parameter
|
|
38
|
+
toDriver: (money) => minorToDriver(money, currency).toString(),
|
|
39
|
+
// node-postgres returns int8 as a string, postgres.js as a string or a bigint
|
|
40
|
+
fromDriver: (value) => minorFromDriver(value, currency),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A NUMERIC(precision, scale) column, NUMERIC(19,4) by default, read back as `Money<C>`. The
|
|
46
|
+
* driver must return NUMERIC as a string (node-postgres and postgres.js do); a number has been
|
|
47
|
+
* through a float and throws, as do `NaN` and `Infinity`. Writing a value the column could not hold
|
|
48
|
+
* unchanged throws, and so does a column scale below the currency's exponent.
|
|
49
|
+
*
|
|
50
|
+
* price: moneyDecimal('EUR', { precision: 19, scale: 4 })('price').notNull()
|
|
51
|
+
*/
|
|
52
|
+
export function moneyDecimal<const C extends CurrencyInput>(
|
|
53
|
+
currency: C,
|
|
54
|
+
size: DecimalColumnSize = DEFAULT_DECIMAL_SIZE,
|
|
55
|
+
) {
|
|
56
|
+
checkDecimalSize(currency, size);
|
|
57
|
+
|
|
58
|
+
return customType<{ data: Money<C>; driverData: string }>({
|
|
59
|
+
dataType: () => `numeric(${size.precision}, ${size.scale})`,
|
|
60
|
+
toDriver: (money) => decimalToDriver(money, currency, size),
|
|
61
|
+
fromDriver: (value) => decimalFromDriver(value, currency),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A JSONB column holding `{"amount":"12.34","currency":"EUR"}`, with the currency in each value.
|
|
67
|
+
* Pass a currency to fix it: then a value in another currency throws, on write and on read.
|
|
68
|
+
*
|
|
69
|
+
* refund: moneyJson()('refund')
|
|
70
|
+
*/
|
|
71
|
+
export function moneyJson<const C extends CurrencyInput = CurrencyInput>(currency?: C) {
|
|
72
|
+
return customType<{ data: Money<C>; driverData: string | MoneyJSON }>({
|
|
73
|
+
dataType: () => 'jsonb',
|
|
74
|
+
toDriver: (money) => jsonToDriver(money, currency),
|
|
75
|
+
// node-postgres parses JSONB itself; other drivers may return the text
|
|
76
|
+
fromDriver: (value) => jsonFromDriver(value, currency),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ducatjs/money/drizzle/sqlite`: Drizzle ORM column types for SQLite (better-sqlite3, libSQL,
|
|
3
|
+
* Bun). Selects return `Money`, inserts and updates accept it, and the currency is part of the
|
|
4
|
+
* column's type.
|
|
5
|
+
*
|
|
6
|
+
* import { sqliteTable } from 'drizzle-orm/sqlite-core';
|
|
7
|
+
* import { moneyMinor } from '@ducatjs/money/drizzle/sqlite';
|
|
8
|
+
*
|
|
9
|
+
* const orders = sqliteTable('orders', { total: moneyMinor('EUR')('total_minor').notNull() });
|
|
10
|
+
*/
|
|
11
|
+
import { customType } from 'drizzle-orm/sqlite-core';
|
|
12
|
+
import { currencyExponent } from '../currency.js';
|
|
13
|
+
import type { CurrencyInput, Money } from '../money.js';
|
|
14
|
+
import {
|
|
15
|
+
decimalFromDriver,
|
|
16
|
+
decimalToDriver,
|
|
17
|
+
jsonFromDriver,
|
|
18
|
+
jsonToDriver,
|
|
19
|
+
minorFromDriver,
|
|
20
|
+
minorToDriver,
|
|
21
|
+
} from '../sql.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An INTEGER column of minor units in a fixed currency (`1234` is 12.34 EUR), read back as
|
|
25
|
+
* `Money<C>`. Exact up to SQLite's limit of ±9.2·10^18 minor units. better-sqlite3 returns integers
|
|
26
|
+
* as numbers by default, which are exact only below 2^53; beyond that reading throws, so call
|
|
27
|
+
* `defaultSafeIntegers(true)` on the database for larger amounts. Writing another currency, or a
|
|
28
|
+
* value at a scale other than the currency's, throws.
|
|
29
|
+
*
|
|
30
|
+
* total: moneyMinor('EUR')('total_minor').notNull()
|
|
31
|
+
*/
|
|
32
|
+
export function moneyMinor<const C extends CurrencyInput>(currency: C) {
|
|
33
|
+
return customType<{ data: Money<C>; driverData: number | bigint }>({
|
|
34
|
+
dataType: () => 'integer',
|
|
35
|
+
toDriver: (money) => minorToDriver(money, currency),
|
|
36
|
+
fromDriver: (value) => minorFromDriver(value, currency),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A TEXT column holding the exact decimal string (`12.34`), read back as `Money<C>`. SQLite's
|
|
42
|
+
* NUMERIC affinity would store some decimals as REAL and lose digits, so they are kept as text.
|
|
43
|
+
* Writing another currency, or a value at a scale other than the currency's, throws.
|
|
44
|
+
*
|
|
45
|
+
* price: moneyDecimal('EUR')('price').notNull()
|
|
46
|
+
*/
|
|
47
|
+
export function moneyDecimal<const C extends CurrencyInput>(currency: C) {
|
|
48
|
+
// fail at the schema definition, not on the first query, for an unknown currency
|
|
49
|
+
currencyExponent(currency);
|
|
50
|
+
|
|
51
|
+
return customType<{ data: Money<C>; driverData: string }>({
|
|
52
|
+
dataType: () => 'text',
|
|
53
|
+
toDriver: (money) => decimalToDriver(money, currency, undefined),
|
|
54
|
+
fromDriver: (value) => decimalFromDriver(value, currency),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A TEXT column holding `{"amount":"12.34","currency":"EUR"}`, with the currency in each value.
|
|
60
|
+
* Pass a currency to fix it: then a value in another currency throws, on write and on read.
|
|
61
|
+
*
|
|
62
|
+
* refund: moneyJson()('refund')
|
|
63
|
+
*/
|
|
64
|
+
export function moneyJson<const C extends CurrencyInput = CurrencyInput>(currency?: C) {
|
|
65
|
+
return customType<{ data: Money<C>; driverData: string }>({
|
|
66
|
+
dataType: () => 'text',
|
|
67
|
+
toDriver: (money) => jsonToDriver(money, currency),
|
|
68
|
+
fromDriver: (value) => jsonFromDriver(value, currency),
|
|
69
|
+
});
|
|
70
|
+
}
|