@arki/money 0.0.0-stage → 0.0.4
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/README.md +238 -2
- package/dist/bag.d.ts +48 -0
- package/dist/bag.d.ts.map +1 -0
- package/dist/bag.js +110 -0
- package/dist/bag.js.map +1 -0
- package/dist/currency.d.ts +64 -0
- package/dist/currency.d.ts.map +1 -0
- package/dist/currency.js +148 -0
- package/dist/currency.js.map +1 -0
- package/dist/data/iso4217.d.ts +9 -0
- package/dist/data/iso4217.d.ts.map +1 -0
- package/dist/data/iso4217.js +11 -0
- package/dist/data/iso4217.js.map +1 -0
- package/dist/drizzle/config.d.ts +27 -0
- package/dist/drizzle/config.d.ts.map +1 -0
- package/dist/drizzle/config.js +16 -0
- package/dist/drizzle/config.js.map +1 -0
- package/dist/drizzle/mysql.d.ts +36 -0
- package/dist/drizzle/mysql.d.ts.map +1 -0
- package/dist/drizzle/mysql.js +45 -0
- package/dist/drizzle/mysql.js.map +1 -0
- package/dist/drizzle/pg.d.ts +38 -0
- package/dist/drizzle/pg.d.ts.map +1 -0
- package/dist/drizzle/pg.js +49 -0
- package/dist/drizzle/pg.js.map +1 -0
- package/dist/drizzle/sqlite.d.ts +38 -0
- package/dist/drizzle/sqlite.d.ts.map +1 -0
- package/dist/drizzle/sqlite.js +45 -0
- package/dist/drizzle/sqlite.js.map +1 -0
- package/dist/drizzle.d.ts +56 -0
- package/dist/drizzle.d.ts.map +1 -0
- package/dist/drizzle.js +77 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/exact.d.ts +20 -0
- package/dist/exact.d.ts.map +1 -0
- package/dist/exact.js +23 -0
- package/dist/exact.js.map +1 -0
- package/dist/exchange.d.ts +84 -0
- package/dist/exchange.d.ts.map +1 -0
- package/dist/exchange.js +215 -0
- package/dist/exchange.js.map +1 -0
- package/dist/format.d.ts +41 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +122 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/columns.d.ts +25 -0
- package/dist/internal/columns.d.ts.map +1 -0
- package/dist/internal/columns.js +47 -0
- package/dist/internal/columns.js.map +1 -0
- package/dist/internal/units.d.ts +66 -0
- package/dist/internal/units.d.ts.map +1 -0
- package/dist/internal/units.js +205 -0
- package/dist/internal/units.js.map +1 -0
- package/dist/money.d.ts +180 -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 +53 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +222 -0
- package/dist/parse.js.map +1 -0
- package/dist/provider.d.ts +22 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +56 -0
- package/dist/provider.js.map +1 -0
- package/dist/rational.d.ts +67 -0
- package/dist/rational.d.ts.map +1 -0
- package/dist/rational.js +169 -0
- package/dist/rational.js.map +1 -0
- package/dist/stripe.d.ts +37 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +75 -0
- package/dist/stripe.js.map +1 -0
- package/dist/zod.d.ts +54 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +96 -0
- package/dist/zod.js.map +1 -0
- package/package.json +137 -4
- package/src/bag.ts +128 -0
- package/src/currency.ts +174 -0
- package/src/data/iso4217.ts +13 -0
- package/src/drizzle/config.ts +39 -0
- package/src/drizzle/mysql.ts +61 -0
- package/src/drizzle/pg.ts +64 -0
- package/src/drizzle/sqlite.ts +61 -0
- package/src/errors.ts +61 -0
- package/src/exact.ts +29 -0
- package/src/exchange.ts +280 -0
- package/src/format.ts +148 -0
- package/src/index.ts +16 -0
- package/src/internal/columns.ts +74 -0
- package/src/internal/units.ts +223 -0
- package/src/money.ts +692 -0
- package/src/parse.ts +278 -0
- package/src/provider.ts +67 -0
- package/src/rational.ts +214 -0
- package/src/stripe.ts +84 -0
- package/src/zod.ts +123 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-unit arithmetic. An amount is a count of whole units, held as a plain
|
|
3
|
+
* `number` while it is a safe integer and as a `bigint` beyond that. Each value
|
|
4
|
+
* has exactly one of the two forms ("canonical form"): a `number` if and only
|
|
5
|
+
* if it is a safe integer, never `-0`.
|
|
6
|
+
*
|
|
7
|
+
* Plain numbers are several times faster than `bigint` on every engine, and
|
|
8
|
+
* 2^53 cents is 90 trillion in a two-decimal currency, so almost every program
|
|
9
|
+
* stays on the number path. The `bigint` path keeps the rest exact.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { RoundingMode } from '@arki/math/rounding';
|
|
13
|
+
import {
|
|
14
|
+
DivisionByZeroError,
|
|
15
|
+
NumberFormatError,
|
|
16
|
+
NumericRangeError,
|
|
17
|
+
ResourceLimitError,
|
|
18
|
+
RoundingNecessaryError,
|
|
19
|
+
} from '@arki/math/errors';
|
|
20
|
+
|
|
21
|
+
export type Units = number | bigint;
|
|
22
|
+
|
|
23
|
+
const MAX = Number.MAX_SAFE_INTEGER;
|
|
24
|
+
const MAX_BIG = BigInt(Number.MAX_SAFE_INTEGER);
|
|
25
|
+
|
|
26
|
+
/** Largest number of decimals a `Money` or a custom currency may have. */
|
|
27
|
+
export const MAX_SCALE = 100;
|
|
28
|
+
/** Longest amount string that is parsed. */
|
|
29
|
+
export const MAX_INPUT_LENGTH = 1000;
|
|
30
|
+
/** Largest number of parts `allocate` and `split` produce. */
|
|
31
|
+
export const MAX_PARTS = 10_000;
|
|
32
|
+
|
|
33
|
+
const POW10 = [1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15];
|
|
34
|
+
|
|
35
|
+
/** Brings a `bigint` to canonical form. */
|
|
36
|
+
export function canon(value: bigint): Units {
|
|
37
|
+
return value >= -MAX_BIG && value <= MAX_BIG ? Number(value) : value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `10^exponent` in canonical form. `exponent` is a small non-negative integer. */
|
|
41
|
+
export function pow10(exponent: number): Units {
|
|
42
|
+
return POW10[exponent] ?? 10n ** BigInt(exponent);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Exact sum. */
|
|
46
|
+
export function addUnits(a: Units, b: Units): Units {
|
|
47
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
48
|
+
const sum = a + b;
|
|
49
|
+
// Two safe integers add exactly whenever the true sum is itself safe.
|
|
50
|
+
if (sum <= MAX && sum >= -MAX) return sum;
|
|
51
|
+
}
|
|
52
|
+
return canon(BigInt(a) + BigInt(b));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Exact product. */
|
|
56
|
+
export function mulUnits(a: Units, b: Units): Units {
|
|
57
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
58
|
+
const product = a * b;
|
|
59
|
+
// Float rounding never moves a value across 2^53, so a product that reads
|
|
60
|
+
// as safe is the exact product. `+ 0` turns `-0` into `0`.
|
|
61
|
+
if (product <= MAX && product >= -MAX) return product + 0;
|
|
62
|
+
}
|
|
63
|
+
return canon(BigInt(a) * BigInt(b));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function negUnits(a: Units): Units {
|
|
67
|
+
return typeof a === 'number' ? 0 - a : -a;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `-1`, `0` or `1`. Mixed `number`/`bigint` comparison is exact in JavaScript. */
|
|
71
|
+
export function compareUnits(a: Units, b: Units): -1 | 0 | 1 {
|
|
72
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The one rounding decision in the package. Given a truncated quotient, says
|
|
77
|
+
* whether to step one further away from zero.
|
|
78
|
+
*
|
|
79
|
+
* `half` compares the dropped remainder with half the divisor: `-1` below,
|
|
80
|
+
* `0` exactly half, `1` above. `odd` is whether the truncated quotient is odd.
|
|
81
|
+
*/
|
|
82
|
+
function roundsAway(mode: RoundingMode | undefined, negative: boolean, half: -1 | 0 | 1, odd: boolean): boolean {
|
|
83
|
+
if (mode === 'halfEven') return half > 0 || (half === 0 && odd);
|
|
84
|
+
if (mode === 'halfUp') return half >= 0;
|
|
85
|
+
if (mode === 'halfDown') return half > 0;
|
|
86
|
+
if (mode === 'down') return false;
|
|
87
|
+
if (mode === 'up') return true;
|
|
88
|
+
if (mode === 'ceiling') return !negative;
|
|
89
|
+
if (mode === 'floor') return negative;
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- plain JavaScript can pass a misspelled mode
|
|
91
|
+
if (mode === undefined || mode === 'unnecessary') {
|
|
92
|
+
throw new RoundingNecessaryError(
|
|
93
|
+
'The exact result does not fit the requested precision; pass a rounding mode such as "halfEven"',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
// Reached only from plain JavaScript with a misspelled mode.
|
|
97
|
+
throw new TypeError(`Unknown rounding mode: ${String(mode)}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `units × numerator ÷ denominator` as a whole number, rounded with `mode`.
|
|
102
|
+
* `denominator` must be positive. With no mode (or `'unnecessary'`) an inexact
|
|
103
|
+
* result throws `RoundingNecessaryError`. Every operation that can lose digits
|
|
104
|
+
* reduces to this call.
|
|
105
|
+
*/
|
|
106
|
+
export function mulDiv(units: Units, numerator: Units, denominator: Units, mode?: RoundingMode): Units {
|
|
107
|
+
if (typeof units === 'number' && typeof numerator === 'number' && typeof denominator === 'number') {
|
|
108
|
+
const product = units * numerator;
|
|
109
|
+
if (product <= MAX && product >= -MAX) {
|
|
110
|
+
// `%` on doubles is exact, so both the remainder and the quotient are.
|
|
111
|
+
const remainder = product % denominator;
|
|
112
|
+
if (remainder === 0) return product / denominator + 0;
|
|
113
|
+
const quotient = (product - remainder) / denominator;
|
|
114
|
+
const twice = (remainder < 0 ? -remainder : remainder) * 2;
|
|
115
|
+
const half = twice > denominator ? 1 : twice < denominator ? -1 : 0;
|
|
116
|
+
if (!roundsAway(mode, product < 0, half, quotient % 2 !== 0)) return quotient;
|
|
117
|
+
return product < 0 ? quotient - 1 : quotient + 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const product = BigInt(units) * BigInt(numerator);
|
|
121
|
+
const divisor = BigInt(denominator);
|
|
122
|
+
const quotient = product / divisor;
|
|
123
|
+
const remainder = product - quotient * divisor;
|
|
124
|
+
if (remainder === 0n) return canon(quotient);
|
|
125
|
+
const twice = (remainder < 0n ? -remainder : remainder) * 2n;
|
|
126
|
+
const half = twice > divisor ? 1 : twice < divisor ? -1 : 0;
|
|
127
|
+
if (!roundsAway(mode, product < 0n, half, (quotient & 1n) !== 0n)) return canon(quotient);
|
|
128
|
+
return canon(product < 0n ? quotient - 1n : quotient + 1n);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Brings `coefficient × 10^-fraction` onto the grid of `scale` decimals, and
|
|
133
|
+
* onto multiples of `step` units when a step is given. Rounds once, never
|
|
134
|
+
* first to the scale and then to the step.
|
|
135
|
+
*/
|
|
136
|
+
export function quantize(
|
|
137
|
+
coefficient: Units,
|
|
138
|
+
fraction: number,
|
|
139
|
+
scale: number,
|
|
140
|
+
mode?: RoundingMode,
|
|
141
|
+
step?: Units,
|
|
142
|
+
): Units {
|
|
143
|
+
if (step === undefined) {
|
|
144
|
+
if (fraction === scale) return coefficient;
|
|
145
|
+
if (fraction < scale) return mulUnits(coefficient, pow10(scale - fraction));
|
|
146
|
+
return mulDiv(coefficient, 1, pow10(fraction - scale), mode);
|
|
147
|
+
}
|
|
148
|
+
const up = fraction < scale ? pow10(scale - fraction) : 1;
|
|
149
|
+
const down = fraction > scale ? pow10(fraction - scale) : 1;
|
|
150
|
+
return mulUnits(mulDiv(coefficient, up, mulUnits(down, step), mode), step);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A decimal read from text: `coefficient × 10^-fraction`, with `fraction >= 0`. */
|
|
154
|
+
export type Parsed = { coefficient: Units; fraction: number };
|
|
155
|
+
|
|
156
|
+
const DECIMAL = /^([+-]?)(\d+)(?:\.(\d+))?(?:e([+-]?\d{1,4}))?$/i;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Reads plain decimal text: an optional sign, digits, an optional fraction, an
|
|
160
|
+
* optional exponent. No grouping, no spaces, no `Infinity`. Throws
|
|
161
|
+
* `NumberFormatError` otherwise.
|
|
162
|
+
*/
|
|
163
|
+
export function parseDecimal(text: string): Parsed {
|
|
164
|
+
if (text.length > MAX_INPUT_LENGTH) {
|
|
165
|
+
throw new ResourceLimitError(`Amount text is longer than ${MAX_INPUT_LENGTH} characters`);
|
|
166
|
+
}
|
|
167
|
+
const match = DECIMAL.exec(text);
|
|
168
|
+
if (match === null) {
|
|
169
|
+
throw new NumberFormatError(`${JSON.stringify(text.slice(0, 40))} is not a decimal number such as "19.99"`);
|
|
170
|
+
}
|
|
171
|
+
const whole = match[2] ?? '';
|
|
172
|
+
const fractionDigits = match[3] ?? '';
|
|
173
|
+
const digits = whole + fractionDigits;
|
|
174
|
+
const magnitude = digits.length <= 15 ? Number(digits) : canon(BigInt(digits));
|
|
175
|
+
const coefficient = match[1] === '-' ? negUnits(magnitude) : magnitude;
|
|
176
|
+
const fraction = fractionDigits.length - (match[4] === undefined ? 0 : Number(match[4]));
|
|
177
|
+
if (fraction > MAX_INPUT_LENGTH || fraction < -MAX_INPUT_LENGTH) {
|
|
178
|
+
throw new ResourceLimitError(`The exponent in ${JSON.stringify(text.slice(0, 40))} is too large`);
|
|
179
|
+
}
|
|
180
|
+
if (fraction >= 0) return { coefficient, fraction };
|
|
181
|
+
return { coefficient: mulUnits(coefficient, pow10(-fraction)), fraction: 0 };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Reads a JS number through its shortest decimal spelling, so `19.99` means
|
|
186
|
+
* exactly 19.99 — what was typed, not the binary approximation. It cannot undo
|
|
187
|
+
* earlier float arithmetic: `0.1 + 0.2` arrives as `0.30000000000000004`.
|
|
188
|
+
*/
|
|
189
|
+
export function parseNumber(value: number): Parsed {
|
|
190
|
+
if (Number.isSafeInteger(value)) return { coefficient: value + 0, fraction: 0 };
|
|
191
|
+
if (!Number.isFinite(value)) throw new NumericRangeError(`${value} is not a finite number`);
|
|
192
|
+
if (Number.isInteger(value)) {
|
|
193
|
+
throw new NumericRangeError(
|
|
194
|
+
`${value} is not a safe integer; its digits are already lost. Pass a string or bigint instead`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return parseDecimal(String(value));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Throws `DivisionByZeroError` for a zero divisor. */
|
|
201
|
+
export function assertNonZero(value: Units): void {
|
|
202
|
+
if (value === 0 || value === 0n) throw new DivisionByZeroError('Division by zero');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Greatest common divisor of two non-negative values, in canonical form. */
|
|
206
|
+
export function gcdUnits(a: Units, b: Units): Units {
|
|
207
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
208
|
+
let x = a;
|
|
209
|
+
let y = b;
|
|
210
|
+
while (y !== 0) [x, y] = [y, x % y];
|
|
211
|
+
return x;
|
|
212
|
+
}
|
|
213
|
+
let x = BigInt(a);
|
|
214
|
+
let y = BigInt(b);
|
|
215
|
+
while (y !== 0n) [x, y] = [y, x % y];
|
|
216
|
+
return canon(x);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** `a / b` for a `b` known to divide `a` exactly. */
|
|
220
|
+
export function divExact(a: Units, b: Units): Units {
|
|
221
|
+
if (typeof a === 'number' && typeof b === 'number') return a / b + 0;
|
|
222
|
+
return canon(BigInt(a) / BigInt(b));
|
|
223
|
+
}
|