@ducatjs/money 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +51 -0
- package/README.md +617 -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 +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 +169 -5
- 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
package/src/fixed.ts
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
// Fixed-point decimals without a currency: the engine under Money, exposed on its own. A Fixed is
|
|
2
|
+
// an integer (a JS number while it is a safe integer, a bigint beyond) and a scale, the count of
|
|
3
|
+
// decimal places: `fixed('12.345')` is 12345 at scale 3. Arithmetic is exact, and every operation
|
|
4
|
+
// that has to drop digits rounds once, with an explicit or the default mode.
|
|
5
|
+
import { currencyExponent } from './currency.js';
|
|
6
|
+
import {
|
|
7
|
+
formatScaled,
|
|
8
|
+
parseDecimal,
|
|
9
|
+
parsePlainDecimal,
|
|
10
|
+
pow10n,
|
|
11
|
+
ratio,
|
|
12
|
+
normalizeMinor,
|
|
13
|
+
type Factor,
|
|
14
|
+
type Ratio,
|
|
15
|
+
} from './decimal.js';
|
|
16
|
+
import {
|
|
17
|
+
fromMinor,
|
|
18
|
+
scaleDown,
|
|
19
|
+
scaleUp,
|
|
20
|
+
toScale as moneyToScale,
|
|
21
|
+
type CurrencyInput,
|
|
22
|
+
type Money,
|
|
23
|
+
} from './money.js';
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_ROUNDING,
|
|
26
|
+
divideAndRound,
|
|
27
|
+
divideAndRoundBigint,
|
|
28
|
+
RoundingError,
|
|
29
|
+
type RoundingMode,
|
|
30
|
+
} from './rounding.js';
|
|
31
|
+
|
|
32
|
+
const MAX_SAFE_MINOR = Number.MAX_SAFE_INTEGER;
|
|
33
|
+
|
|
34
|
+
// A double carries 15 significant decimal digits faithfully (DBL_DIG): a number with no more than
|
|
35
|
+
// that is the decimal its author wrote. 0.1 + 0.2 reads back as 0.30000000000000004 (17 digits), so
|
|
36
|
+
// it is refused unless a scale and a rounding mode say what to do with it.
|
|
37
|
+
const MAX_FAITHFUL_DIGITS = 15;
|
|
38
|
+
|
|
39
|
+
// The largest scale accepted. Scales cost memory and time in proportion (10^scale is a bigint with
|
|
40
|
+
// `scale` digits), so an untrusted '1e-999999999' must be refused before any work is done. toJSON
|
|
41
|
+
// never writes an exponent, so real data stays far below this.
|
|
42
|
+
const MAX_SCALE = 1000;
|
|
43
|
+
const EXPONENT = /[eE]([+-]?\d+)\s*$/;
|
|
44
|
+
|
|
45
|
+
// Passed to the constructor by this module only, so `new Fixed(1.5, 2)` from outside can't break
|
|
46
|
+
// the representation invariant. Values come from fixed(), fromScaled() and the methods.
|
|
47
|
+
const INTERNAL: unique symbol = Symbol('Fixed');
|
|
48
|
+
|
|
49
|
+
/** What `fixed()` accepts: a decimal string, a number, a bigint or a Fixed. */
|
|
50
|
+
export type FixedInput = string | number | bigint | Fixed;
|
|
51
|
+
|
|
52
|
+
/** Options for `fixed()`. */
|
|
53
|
+
export interface FixedOptions {
|
|
54
|
+
/** Decimal places to keep (default: as many as the input has; 0 for integers). */
|
|
55
|
+
readonly scale?: number;
|
|
56
|
+
/** How to round input with more decimals than `scale` (default 'unnecessary': throw). */
|
|
57
|
+
readonly rounding?: RoundingMode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Options for multiply and divide. */
|
|
61
|
+
export interface ArithmeticOptions {
|
|
62
|
+
/** The result's scale (default: the scale of the value being multiplied or divided). */
|
|
63
|
+
readonly scale?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The integer-and-scale form of a Fixed: `value / 10^scale`. */
|
|
67
|
+
export interface ScaledInteger {
|
|
68
|
+
readonly value: number | bigint;
|
|
69
|
+
readonly scale: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A number stays; a bigint becomes a number when it is safe (the representation invariant). */
|
|
73
|
+
function normalize(value: number | bigint): number | bigint {
|
|
74
|
+
return typeof value === 'bigint' ? normalizeMinor(value) : value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toBigInt(value: number | bigint): bigint {
|
|
78
|
+
return typeof value === 'bigint' ? value : BigInt(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function checkScale(scale: number): void {
|
|
82
|
+
if (!Number.isSafeInteger(scale) || scale < 0 || scale > MAX_SCALE) {
|
|
83
|
+
throw new RangeError(`Invalid scale ${scale}: use an integer from 0 to ${MAX_SCALE}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A Fixed from a value that already holds the invariant (an integer, safe when a number). */
|
|
88
|
+
function make(value: number | bigint, scale: number): Fixed {
|
|
89
|
+
return new Fixed(INTERNAL, value, scale);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resultScale(
|
|
93
|
+
value: { readonly scale: number },
|
|
94
|
+
options: ArithmeticOptions | undefined,
|
|
95
|
+
): number {
|
|
96
|
+
const scale = options?.scale;
|
|
97
|
+
if (scale === undefined) {
|
|
98
|
+
return value.scale;
|
|
99
|
+
}
|
|
100
|
+
checkScale(scale);
|
|
101
|
+
return scale;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* An exact fixed-point decimal: `value / 10^scale`. Immutable. Create one with `fixed()` or
|
|
106
|
+
* `fromScaled()`. Mixed scales work as for Money: add, subtract and compare align to the larger
|
|
107
|
+
* scale exactly, and multiply and divide keep this value's scale.
|
|
108
|
+
*/
|
|
109
|
+
export class Fixed {
|
|
110
|
+
/** The scaled integer: a number while it is a safe integer, a bigint beyond. */
|
|
111
|
+
readonly value: number | bigint;
|
|
112
|
+
/** Decimal places. */
|
|
113
|
+
readonly scale: number;
|
|
114
|
+
|
|
115
|
+
/** @internal use fixed() or fromScaled() */
|
|
116
|
+
constructor(token: typeof INTERNAL, value: number | bigint, scale: number) {
|
|
117
|
+
if (token !== INTERNAL) {
|
|
118
|
+
throw new TypeError('Use fixed() or fromScaled() to create a Fixed');
|
|
119
|
+
}
|
|
120
|
+
// `+ 0` turns -0 into 0, so no operation can leak a negative zero.
|
|
121
|
+
this.value = typeof value === 'bigint' ? normalizeMinor(value) : value + 0;
|
|
122
|
+
this.scale = scale;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Arithmetic
|
|
126
|
+
|
|
127
|
+
/** this + other, exactly, at the larger of the two scales. */
|
|
128
|
+
add(other: FixedInput): Fixed {
|
|
129
|
+
return addAligned(this, toFixed(other), 1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** this − other, exactly, at the larger of the two scales. */
|
|
133
|
+
subtract(other: FixedInput): Fixed {
|
|
134
|
+
return addAligned(this, toFixed(other), -1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* this × factor, rounded once to this value's scale or `options.scale` (default 'halfExpand').
|
|
139
|
+
* The factor is exact: an integer, a decimal string ('1.075'), a bigint, a `ratio()` or another
|
|
140
|
+
* Fixed. Give `scale` rather than calling toScale afterwards, which would round twice.
|
|
141
|
+
*/
|
|
142
|
+
multiply(
|
|
143
|
+
factor: Factor | Fixed,
|
|
144
|
+
rounding: RoundingMode = DEFAULT_ROUNDING,
|
|
145
|
+
options?: ArithmeticOptions,
|
|
146
|
+
): Fixed {
|
|
147
|
+
const value = this.value;
|
|
148
|
+
const scale = resultScale(this, options);
|
|
149
|
+
|
|
150
|
+
// An integer factor keeps the scale and can't need rounding, so a safe product is the answer.
|
|
151
|
+
if (
|
|
152
|
+
scale === this.scale &&
|
|
153
|
+
typeof factor === 'number' &&
|
|
154
|
+
Number.isSafeInteger(factor) &&
|
|
155
|
+
typeof value === 'number'
|
|
156
|
+
) {
|
|
157
|
+
const product = value * factor;
|
|
158
|
+
if (product <= MAX_SAFE_MINOR && product >= -MAX_SAFE_MINOR) {
|
|
159
|
+
return make(product + 0, this.scale);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return multiplyByRatio(this, factorRatio(factor), rounding, scale);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* this ÷ divisor, rounded once to this value's scale or `options.scale` (default 'halfExpand'):
|
|
168
|
+
* `fixed('9.990').divide('1.2', 'halfEven', { scale: 2 })` is 8.32.
|
|
169
|
+
*/
|
|
170
|
+
divide(
|
|
171
|
+
divisor: Factor | Fixed,
|
|
172
|
+
rounding: RoundingMode = DEFAULT_ROUNDING,
|
|
173
|
+
options?: ArithmeticOptions,
|
|
174
|
+
): Fixed {
|
|
175
|
+
const scale = resultScale(this, options);
|
|
176
|
+
const divisorRatio = factorRatio(divisor);
|
|
177
|
+
if (divisorRatio.num === 0n) {
|
|
178
|
+
throw new RangeError('Division by zero');
|
|
179
|
+
}
|
|
180
|
+
return multiplyByRatio(this, ratio(divisorRatio.den, divisorRatio.num), rounding, scale);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** −this */
|
|
184
|
+
negate(): Fixed {
|
|
185
|
+
const value = this.value;
|
|
186
|
+
if (typeof value === 'number') {
|
|
187
|
+
return make(0 - value, this.scale);
|
|
188
|
+
}
|
|
189
|
+
return make(-value, this.scale);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** |this| */
|
|
193
|
+
abs(): Fixed {
|
|
194
|
+
return this.value < 0 ? this.negate() : this;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Rounds to `scale` decimal places (default 0, an integer): `fixed('2.5').round('halfEven')` is
|
|
199
|
+
* 2. Money's round() defaults to the currency's exponent; a Fixed has none, so the default is 0.
|
|
200
|
+
*/
|
|
201
|
+
round(rounding: RoundingMode, scale = 0): Fixed {
|
|
202
|
+
return this.toScale(scale, rounding);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Changes the scale: increasing it is always exact, decreasing it rounds (default 'halfExpand').
|
|
207
|
+
*/
|
|
208
|
+
toScale(scale: number, rounding: RoundingMode = DEFAULT_ROUNDING): Fixed {
|
|
209
|
+
checkScale(scale);
|
|
210
|
+
if (scale === this.scale) {
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
if (scale > this.scale) {
|
|
214
|
+
return make(scaleUp(this.value, scale - this.scale), scale);
|
|
215
|
+
}
|
|
216
|
+
return make(scaleDown(this.value, this.scale - scale, rounding), scale);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Comparison
|
|
220
|
+
|
|
221
|
+
/** −1, 0 or 1, exactly, across scales and number/bigint. */
|
|
222
|
+
compare(other: FixedInput): -1 | 0 | 1 {
|
|
223
|
+
const operand = toFixed(other);
|
|
224
|
+
let first = this.value;
|
|
225
|
+
let second = operand.value;
|
|
226
|
+
if (this.scale !== operand.scale) {
|
|
227
|
+
const scale = Math.max(this.scale, operand.scale);
|
|
228
|
+
first = scaleUp(first, scale - this.scale);
|
|
229
|
+
second = scaleUp(second, scale - operand.scale);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Comparisons between a number and a bigint are exact in JS.
|
|
233
|
+
if (first < second) {
|
|
234
|
+
return -1;
|
|
235
|
+
}
|
|
236
|
+
if (first > second) {
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The same value, at any scale: 1.0 equals 1.00. */
|
|
243
|
+
equals(other: FixedInput): boolean {
|
|
244
|
+
const operand = toFixed(other);
|
|
245
|
+
if (this.scale === operand.scale) {
|
|
246
|
+
// == compares a number and a bigint exactly.
|
|
247
|
+
return this.value == operand.value;
|
|
248
|
+
}
|
|
249
|
+
return this.compare(operand) === 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** this < other */
|
|
253
|
+
lessThan(other: FixedInput): boolean {
|
|
254
|
+
return this.compare(other) < 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** this ≤ other */
|
|
258
|
+
lessThanOrEqual(other: FixedInput): boolean {
|
|
259
|
+
return this.compare(other) <= 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** this > other */
|
|
263
|
+
greaterThan(other: FixedInput): boolean {
|
|
264
|
+
return this.compare(other) > 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** this ≥ other */
|
|
268
|
+
greaterThanOrEqual(other: FixedInput): boolean {
|
|
269
|
+
return this.compare(other) >= 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** this = 0 */
|
|
273
|
+
isZero(): boolean {
|
|
274
|
+
return this.value == 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** this > 0 */
|
|
278
|
+
isPositive(): boolean {
|
|
279
|
+
return this.value > 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** this < 0 */
|
|
283
|
+
isNegative(): boolean {
|
|
284
|
+
return this.value < 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** −1, 0 or 1 */
|
|
288
|
+
sign(): -1 | 0 | 1 {
|
|
289
|
+
if (this.value > 0) {
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
if (this.value < 0) {
|
|
293
|
+
return -1;
|
|
294
|
+
}
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Conversion
|
|
299
|
+
|
|
300
|
+
/** The plain decimal string, with every decimal place of the scale: '12.340'. */
|
|
301
|
+
toString(): string {
|
|
302
|
+
return formatScaled(this.value, this.scale);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** The decimal string, as toString(), so JSON round-trips exactly through `fixed()`. */
|
|
306
|
+
toJSON(): string {
|
|
307
|
+
return this.toString();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** `{ value, scale }`, the exact integer form: value / 10^scale. */
|
|
311
|
+
toScaled(): ScaledInteger {
|
|
312
|
+
return { value: this.value, scale: this.scale };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Money in `currency`, at its exponent. Throws RoundingError when that would drop digits, unless
|
|
317
|
+
* `rounding` is given: `fixed('1.005').toMoney('EUR', 'halfEven')` is 1.00 EUR.
|
|
318
|
+
*/
|
|
319
|
+
toMoney<C extends CurrencyInput>(currency: C, rounding: RoundingMode = 'unnecessary'): Money<C> {
|
|
320
|
+
const exact = fromMinor(this.value, currency, this.scale);
|
|
321
|
+
const target = currencyExponent(currency);
|
|
322
|
+
if (target === this.scale) {
|
|
323
|
+
return exact;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return moneyToScale(exact, target, rounding);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error instanceof RoundingError) {
|
|
329
|
+
throw new RoundingError(
|
|
330
|
+
`${this.toString()} has more than ${target} decimal places for ${currency}; pass a rounding mode`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Blocks `a + b`, `a > b` and `+a`: arithmetic and comparison go through the methods. */
|
|
338
|
+
[Symbol.toPrimitive](hint: string): string {
|
|
339
|
+
if (hint === 'string') {
|
|
340
|
+
return this.toString();
|
|
341
|
+
}
|
|
342
|
+
throw new TypeError(
|
|
343
|
+
'Fixed cannot be converted to a number; use add(), compare(), toString() etc.',
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** value × rate at `scale`, rounded once: the kernel of multiply and divide. */
|
|
349
|
+
function multiplyByRatio(
|
|
350
|
+
fixedValue: Fixed,
|
|
351
|
+
rate: Ratio,
|
|
352
|
+
rounding: RoundingMode,
|
|
353
|
+
scale: number,
|
|
354
|
+
): Fixed {
|
|
355
|
+
const value = fixedValue.value;
|
|
356
|
+
|
|
357
|
+
// Fast path: both factors are safe integers, so a product within the safe range is exact, and
|
|
358
|
+
// divideAndRound then rounds the exact quotient.
|
|
359
|
+
if (
|
|
360
|
+
typeof value === 'number' &&
|
|
361
|
+
scale === fixedValue.scale &&
|
|
362
|
+
rate.n === rate.n &&
|
|
363
|
+
rate.d === rate.d
|
|
364
|
+
) {
|
|
365
|
+
const product = value * rate.n;
|
|
366
|
+
if (product <= MAX_SAFE_MINOR && product >= -MAX_SAFE_MINOR) {
|
|
367
|
+
return make(divideAndRound(product, rate.d, rounding), scale);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Slow path: the same single division, in bigint.
|
|
372
|
+
let numerator = toBigInt(value) * rate.num;
|
|
373
|
+
let denominator = rate.den;
|
|
374
|
+
if (scale > fixedValue.scale) {
|
|
375
|
+
numerator *= pow10n(scale - fixedValue.scale);
|
|
376
|
+
} else if (scale < fixedValue.scale) {
|
|
377
|
+
denominator *= pow10n(fixedValue.scale - scale);
|
|
378
|
+
}
|
|
379
|
+
return make(divideAndRoundBigint(numerator, denominator, rounding), scale);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** first + sign × second, exactly, at the larger scale. */
|
|
383
|
+
function addAligned(first: Fixed, second: Fixed, sign: 1 | -1): Fixed {
|
|
384
|
+
const firstValue = first.value;
|
|
385
|
+
const secondValue = second.value;
|
|
386
|
+
|
|
387
|
+
// Fast path: two safe integers at one scale have an exact sum whenever it is itself safe.
|
|
388
|
+
if (
|
|
389
|
+
typeof firstValue === 'number' &&
|
|
390
|
+
typeof secondValue === 'number' &&
|
|
391
|
+
first.scale === second.scale
|
|
392
|
+
) {
|
|
393
|
+
const sum = sign === 1 ? firstValue + secondValue : firstValue - secondValue;
|
|
394
|
+
if (sum <= MAX_SAFE_MINOR && sum >= -MAX_SAFE_MINOR) {
|
|
395
|
+
return make(sum + 0, first.scale);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const scale = Math.max(first.scale, second.scale);
|
|
400
|
+
const aligned = toBigInt(scaleUp(firstValue, scale - first.scale));
|
|
401
|
+
const alignedOther = toBigInt(scaleUp(secondValue, scale - second.scale));
|
|
402
|
+
if (sign === 1) {
|
|
403
|
+
return make(aligned + alignedOther, scale);
|
|
404
|
+
}
|
|
405
|
+
return make(aligned - alignedOther, scale);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** A factor as an exact ratio; a Fixed is value / 10^scale. */
|
|
409
|
+
function factorRatio(factor: Factor | Fixed): Ratio {
|
|
410
|
+
if (factor instanceof Fixed) {
|
|
411
|
+
return ratio(toBigInt(factor.value), pow10n(factor.scale));
|
|
412
|
+
}
|
|
413
|
+
// A Ratio object is trusted as it is, as Money trusts it: build it with ratio(), once, for hot
|
|
414
|
+
// loops.
|
|
415
|
+
return ratio(factor);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function toFixed(input: FixedInput): Fixed {
|
|
419
|
+
if (input instanceof Fixed) {
|
|
420
|
+
return input;
|
|
421
|
+
}
|
|
422
|
+
return fixed(input);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Digits at `fromScale` as a Fixed at `targetScale`, rounding when decimals are dropped. */
|
|
426
|
+
function rescale(
|
|
427
|
+
digits: number | bigint,
|
|
428
|
+
fromScale: number,
|
|
429
|
+
targetScale: number,
|
|
430
|
+
rounding: RoundingMode,
|
|
431
|
+
input: unknown,
|
|
432
|
+
): Fixed {
|
|
433
|
+
if (fromScale === targetScale) {
|
|
434
|
+
return make(digits, targetScale);
|
|
435
|
+
}
|
|
436
|
+
if (fromScale < targetScale) {
|
|
437
|
+
return make(scaleUp(normalize(digits), targetScale - fromScale), targetScale);
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
return make(scaleDown(normalize(digits), fromScale - targetScale, rounding), targetScale);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error instanceof RoundingError) {
|
|
443
|
+
throw new RoundingError(
|
|
444
|
+
`${String(input)} has more than ${targetScale} decimal places; pass { rounding }`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* parseDecimal, refusing exponents that would make a scale (or a power of ten) beyond MAX_SCALE.
|
|
453
|
+
*/
|
|
454
|
+
function parseBoundedDecimal(text: string): { digits: bigint; scale: number } {
|
|
455
|
+
const exponent = EXPONENT.exec(text);
|
|
456
|
+
if (exponent !== null && Math.abs(Number(exponent[1])) > MAX_SCALE) {
|
|
457
|
+
throw new RangeError(`${text}: the exponent is beyond ±${MAX_SCALE}`);
|
|
458
|
+
}
|
|
459
|
+
const parsed = parseDecimal(text);
|
|
460
|
+
if (parsed.scale > MAX_SCALE) {
|
|
461
|
+
throw new RangeError(`${text} has more than ${MAX_SCALE} decimal places`);
|
|
462
|
+
}
|
|
463
|
+
return parsed;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The count of significant digits in a plain decimal string ('0.0012300' has 3). */
|
|
467
|
+
function significantDigits(text: string): number {
|
|
468
|
+
const digits = text.replace(/^[-+]/, '').replace('.', '').replace(/^0+/, '').replace(/0+$/, '');
|
|
469
|
+
return digits.length;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Creates an exact fixed-point decimal.
|
|
474
|
+
*
|
|
475
|
+
* fixed('12.340') // 12.340: a string keeps its scale (3)
|
|
476
|
+
* fixed('1.005', { scale: 2, rounding: 'halfEven' }) // 1.00
|
|
477
|
+
* fixed(12) // 12, scale 0
|
|
478
|
+
* fixed(0.25) // 0.25: read at its shortest decimal form
|
|
479
|
+
* fixed(10n ** 30n)
|
|
480
|
+
*
|
|
481
|
+
* Numbers are taken at their shortest decimal form, and only when that has at most 15 significant
|
|
482
|
+
* digits, which a double always holds faithfully. So `fixed(0.1 + 0.2)` throws rather than becoming
|
|
483
|
+
* 0.30000000000000004; pass the decimal as a string, or give `scale` and `rounding`. Input with
|
|
484
|
+
* more decimals than `scale` throws unless `rounding` is given, and `rounding` needs a `scale`.
|
|
485
|
+
* Scales run from 0 to 1000.
|
|
486
|
+
*/
|
|
487
|
+
export function fixed(value: FixedInput, options?: FixedOptions): Fixed {
|
|
488
|
+
const scale = options?.scale;
|
|
489
|
+
if (scale !== undefined) {
|
|
490
|
+
checkScale(scale);
|
|
491
|
+
} else if (options?.rounding !== undefined) {
|
|
492
|
+
throw new RangeError('fixed(): `rounding` needs a `scale` to round to');
|
|
493
|
+
}
|
|
494
|
+
const rounding = options?.rounding ?? 'unnecessary';
|
|
495
|
+
|
|
496
|
+
if (value instanceof Fixed) {
|
|
497
|
+
return scale === undefined ? value : rescale(value.value, value.scale, scale, rounding, value);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (typeof value === 'string') {
|
|
501
|
+
const parsed = parsePlainDecimal(value) ?? parseBoundedDecimal(value);
|
|
502
|
+
if (parsed.scale > MAX_SCALE) {
|
|
503
|
+
throw new RangeError(`${value.slice(0, 40)}… has more than ${MAX_SCALE} decimal places`);
|
|
504
|
+
}
|
|
505
|
+
return rescale(parsed.digits, parsed.scale, scale ?? parsed.scale, rounding, value);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (typeof value === 'number') {
|
|
509
|
+
if (Number.isSafeInteger(value)) {
|
|
510
|
+
return scale === undefined ? make(value + 0, 0) : make(scaleUp(value + 0, scale), scale);
|
|
511
|
+
}
|
|
512
|
+
if (!Number.isFinite(value)) {
|
|
513
|
+
throw new RangeError(`Invalid number ${value}`);
|
|
514
|
+
}
|
|
515
|
+
const parsed = parseDecimal(String(value));
|
|
516
|
+
// Only an explicit rounding mode says the float's extra digits may be dropped.
|
|
517
|
+
if (
|
|
518
|
+
options?.rounding === undefined &&
|
|
519
|
+
significantDigits(formatScaled(normalize(parsed.digits), parsed.scale)) > MAX_FAITHFUL_DIGITS
|
|
520
|
+
) {
|
|
521
|
+
throw new RangeError(
|
|
522
|
+
`${value} is not an exact decimal; pass a string, or { scale, rounding }`,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return rescale(parsed.digits, parsed.scale, scale ?? parsed.scale, rounding, value);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (typeof value === 'bigint') {
|
|
529
|
+
return scale === undefined ? make(value, 0) : make(scaleUp(normalize(value), scale), scale);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
throw new TypeError(`Invalid fixed-point value ${String(value)}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** The Fixed `value / 10^scale`: `fromScaled(12345, 3)` is 12.345. `value` must be an integer. */
|
|
536
|
+
export function fromScaled(value: number | bigint, scale: number): Fixed {
|
|
537
|
+
checkScale(scale);
|
|
538
|
+
if (typeof value === 'number') {
|
|
539
|
+
if (!Number.isSafeInteger(value)) {
|
|
540
|
+
throw new RangeError(`The scaled value must be a safe integer, got ${value}; pass a bigint`);
|
|
541
|
+
}
|
|
542
|
+
return make(value + 0, scale);
|
|
543
|
+
}
|
|
544
|
+
if (typeof value !== 'bigint') {
|
|
545
|
+
throw new TypeError(`Invalid scaled value ${String(value)}`);
|
|
546
|
+
}
|
|
547
|
+
return make(value, scale);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Whether `value` is a Fixed. */
|
|
551
|
+
export function isFixed(value: unknown): value is Fixed {
|
|
552
|
+
return value instanceof Fixed;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** The exact Fixed of a Money amount, at the money's scale: `money('12.34', 'EUR')` → 12.34. */
|
|
556
|
+
export function moneyToFixed(money: Money): Fixed {
|
|
557
|
+
return make(money.minor, money.scale);
|
|
558
|
+
}
|
package/src/fn.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ducatjs/money/fn`: every operation of ducat and its opt-in modules as a plain function
|
|
3
|
+
* (`add(price, fee)`, `allocate(total, [1, 1])`), for code that prefers that style or must not
|
|
4
|
+
* install methods on Money (importing it changes no prototype).
|
|
5
|
+
*/
|
|
6
|
+
// Everything the root entry exports, plus the core Money methods as plain functions. Each is
|
|
7
|
+
// re-exported straight from its own module, so every name is one re-export away from its definition
|
|
8
|
+
// (under vitest each hop is a getter call, which shows in the CodSpeed benchmarks).
|
|
9
|
+
export {
|
|
10
|
+
money,
|
|
11
|
+
fromMinor,
|
|
12
|
+
zero,
|
|
13
|
+
fromJSON,
|
|
14
|
+
isMoney,
|
|
15
|
+
extendMoney,
|
|
16
|
+
type ExtendMoneyOptions,
|
|
17
|
+
sum,
|
|
18
|
+
ratio,
|
|
19
|
+
add,
|
|
20
|
+
subtract,
|
|
21
|
+
negate,
|
|
22
|
+
abs,
|
|
23
|
+
multiply,
|
|
24
|
+
divide,
|
|
25
|
+
round,
|
|
26
|
+
toScale,
|
|
27
|
+
compare,
|
|
28
|
+
equals,
|
|
29
|
+
lessThan,
|
|
30
|
+
lessThanOrEqual,
|
|
31
|
+
greaterThan,
|
|
32
|
+
greaterThanOrEqual,
|
|
33
|
+
isZero,
|
|
34
|
+
isPositive,
|
|
35
|
+
isNegative,
|
|
36
|
+
sign,
|
|
37
|
+
min,
|
|
38
|
+
max,
|
|
39
|
+
clamp,
|
|
40
|
+
toDecimal,
|
|
41
|
+
toMinor,
|
|
42
|
+
toMinorNumber,
|
|
43
|
+
CurrencyMismatchError,
|
|
44
|
+
type Money,
|
|
45
|
+
type MoneyJSON,
|
|
46
|
+
type MoneyOptions,
|
|
47
|
+
type CurrencyInput,
|
|
48
|
+
type Factor,
|
|
49
|
+
type Ratio,
|
|
50
|
+
} from './money.js';
|
|
51
|
+
export { DEFAULT_ROUNDING, Rounding, RoundingError, type RoundingMode } from './rounding.js';
|
|
52
|
+
export {
|
|
53
|
+
currencyExponent,
|
|
54
|
+
defineCurrency,
|
|
55
|
+
isCurrency,
|
|
56
|
+
ISO_EXPONENTS,
|
|
57
|
+
ISO_4217_PUBLISHED,
|
|
58
|
+
ISO_NON_MONETARY,
|
|
59
|
+
type CurrencyCode,
|
|
60
|
+
type IsoCurrencyCode,
|
|
61
|
+
type CustomCurrencies,
|
|
62
|
+
} from './currency.js';
|
|
63
|
+
export * from './allocate.js';
|
|
64
|
+
export * from './tax.js';
|
|
65
|
+
export * from './convert.js';
|
|
66
|
+
export * from './exact.js';
|
|
67
|
+
export * from './ecb.js';
|
|
68
|
+
export * from './stripe-fx.js';
|
|
69
|
+
export * from './adyen-fx.js';
|
|
70
|
+
export * from './cash.js';
|
|
71
|
+
export * from './format.js';
|
|
72
|
+
export * from './parse.js';
|
|
73
|
+
export * from './stripe.js';
|
|
74
|
+
export * from './adyen.js';
|
|
75
|
+
export * from './checkout.js';
|
|
76
|
+
export * from './paypal.js';
|
|
77
|
+
export * from './braintree.js';
|
|
78
|
+
export * from './klarna.js';
|
|
79
|
+
export * from './prisma.js';
|
|
80
|
+
export * from './fixed.js';
|