@rallycash/numbers 0.0.0-stage → 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/CHANGELOG.md +7 -0
- package/README.md +229 -2
- package/dist/bigint-utils.d.ts +17 -0
- package/dist/bigint-utils.d.ts.map +1 -0
- package/dist/bigint-utils.js +178 -0
- package/dist/decimal.d.ts +12 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +16 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/number-utils.d.ts +31 -0
- package/dist/number-utils.d.ts.map +1 -0
- package/dist/number-utils.js +276 -0
- package/dist/units.d.ts +7 -0
- package/dist/units.d.ts.map +1 -0
- package/dist/units.js +82 -0
- package/package.json +30 -3
- package/src/bigint-utils.test.ts +173 -0
- package/src/bigint-utils.ts +222 -0
- package/src/decimal.ts +25 -0
- package/src/index.ts +7 -0
- package/src/number-utils.test.ts +125 -0
- package/src/number-utils.ts +398 -0
- package/src/units.ts +92 -0
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -1,3 +1,230 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@rallycash/numbers`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared number formatting and fixed-point bigint conversion for Rally Cash apps.
|
|
4
|
+
|
|
5
|
+
Two exports:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { BigIntUtils, NumberUtils } from '@rallycash/numbers'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`NumberUtils` formats values for display. `BigIntUtils` converts and does integer arithmetic. USD and percentage scales are arguments, not constants in this package. Prisma is not a dependency. Pass any object with `toFixed(digits)` and `toNumber()`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add @rallycash/numbers
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## NumberUtils
|
|
20
|
+
|
|
21
|
+
`formatNumber` is the general formatter. The named methods below are presets on top of it, except `formatUsd`, which always abbreviates.
|
|
22
|
+
|
|
23
|
+
### `formatNumber(value, options?)`
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
formatNumber(
|
|
27
|
+
value: number | DecimalLike | null | undefined,
|
|
28
|
+
options?: FormatNumberOptions,
|
|
29
|
+
): string
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Option | Default |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `showExponents` | `true` |
|
|
35
|
+
| `minSignificantDigits` | `4` |
|
|
36
|
+
| `maxDecimals` | `6` |
|
|
37
|
+
| `minDecimals` | `2` |
|
|
38
|
+
| `abbrThreshold` | `10_000_000` |
|
|
39
|
+
| `abbreviateLargeNumbers` | `true` |
|
|
40
|
+
| `abbrMinSignificantDigits` | `3` |
|
|
41
|
+
| `prefix` | `''` |
|
|
42
|
+
|
|
43
|
+
`null`, `undefined`, and `NaN` return `''`. `0` returns `0.00` at the default fraction digits.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
NumberUtils.formatNumber(1234.5) // "1,234.50"
|
|
47
|
+
NumberUtils.formatNumber(1_000_000) // "1,000,000.00"
|
|
48
|
+
NumberUtils.formatNumber(12_345_678) // "12.3M"
|
|
49
|
+
NumberUtils.formatNumber(0.0000001234) // "0.0₆123"
|
|
50
|
+
NumberUtils.formatNumber(null) // ""
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Dust smaller than `maxDecimals` places, but not small enough for a subscript, renders as `0` plus `minDecimals` zeros. Values from 1 up to 10,000 use at most 2 fraction digits. Abbreviation starts at `abbrThreshold` when `abbreviateLargeNumbers` is true (10 million by default): `K`, `M`, `B`, `T`, `P`, `E`. An exact tier such as `1,000,000` uses that suffix, and a coefficient that rounds to `1,000` promotes to the next one.
|
|
54
|
+
|
|
55
|
+
This is the server formatter. The web copy abbreviated from 1 trillion, used 4 fraction digits below 10, and rendered that underflow case as `0.00`.
|
|
56
|
+
|
|
57
|
+
### Presets
|
|
58
|
+
|
|
59
|
+
Each preset calls `formatNumber` with fixed options. Pass `options` to override the preset. `formatUsd` does not. It always abbreviates through `abbreviateNumber`.
|
|
60
|
+
|
|
61
|
+
#### `formatCryptoBalance(value, options?)`
|
|
62
|
+
|
|
63
|
+
Token amount. Same defaults as `formatNumber`, including subscripts for dust.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
NumberUtils.formatCryptoBalance(1234.5) // "1,234.50"
|
|
67
|
+
NumberUtils.formatCryptoBalance(0.0000001234) // "0.0₆123"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Atomic units go through `formatBigInt` or `BigIntUtils.toNumber` first. This method formats an already-human amount.
|
|
71
|
+
|
|
72
|
+
#### `formatUsdBalance(value, options?)`
|
|
73
|
+
|
|
74
|
+
Cash balance. Replaces `formatCurrencyAmount`.
|
|
75
|
+
|
|
76
|
+
Preset options: `minDecimals: 2`, `maxDecimals: 2`, `minSignificantDigits: 0`, `showExponents: false`.
|
|
77
|
+
|
|
78
|
+
The `$` is applied here, with the sign in front. `0`, `null`, `undefined`, and `NaN` return `$0.00`. Amounts at or above 10 million still abbreviate, same as `formatNumber`.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
NumberUtils.formatUsdBalance(1234.5) // "$1,234.50"
|
|
82
|
+
NumberUtils.formatUsdBalance(-1.2) // "-$1.20"
|
|
83
|
+
NumberUtils.formatUsdBalance(0) // "$0.00"
|
|
84
|
+
NumberUtils.formatUsdBalance(12_345_678) // "$12.3M"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### `formatPercentage(value, options?)`
|
|
88
|
+
|
|
89
|
+
Preset options: `minDecimals: 0`, `maxDecimals: 2`, `minSignificantDigits: 0`, `showExponents: false`.
|
|
90
|
+
|
|
91
|
+
Does not append `%`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
NumberUtils.formatPercentage(12.3) // "12.3"
|
|
95
|
+
NumberUtils.formatPercentage(12.345) // "12.35"
|
|
96
|
+
NumberUtils.formatPercentage(0.0000001234) // "0"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### `formatUsd(value)`
|
|
100
|
+
|
|
101
|
+
Abbreviated USD. Not a `formatNumber` preset. Two fraction digits, trailing zeros kept, `$` in front.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
NumberUtils.formatUsd(1.5) // "$1.50"
|
|
105
|
+
NumberUtils.formatUsd(1234.5) // "$1.23K"
|
|
106
|
+
NumberUtils.formatUsd(-12_345_678) // "-$12.35M"
|
|
107
|
+
NumberUtils.formatUsd(null) // "$0.00"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`formatUsd(1234.5)` is `$1.23K`. `formatUsdBalance(1234.5)` is `$1,234.50`.
|
|
111
|
+
|
|
112
|
+
#### `formatInteger(value, options?)`
|
|
113
|
+
|
|
114
|
+
Whole numbers. Preset: `minDecimals: 0`, `maxDecimals: 0`, `minSignificantDigits: 0`, `showExponents: false`.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
NumberUtils.formatInteger(6737233.2323223) // "6,737,233"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Other NumberUtils methods
|
|
121
|
+
|
|
122
|
+
| Method | Role |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `formatTokenPrice(value)` | Alias of `formatNumber`. No extra preset. |
|
|
125
|
+
| `formatBigInt(atomic, decimals, options?)` | Atomic `bigint`, integer string, or `DecimalLike` to a human string, then `formatNumber`. |
|
|
126
|
+
| `abbreviateNumber(value, decimals = 2, preserveTrailingZeros = false)` | `K`/`M`/`B`/`T`/`P`/`E`. Negatives abbreviate. |
|
|
127
|
+
| `formatLargeNumber(value)` | `abbreviateNumber(value, 2)`. |
|
|
128
|
+
| `formatSwapAmount(value)` | Three significant digits after leading zeros. `0.000015045` becomes `0.0000150`. |
|
|
129
|
+
| `expToString(value)` | Scientific notation to a plain decimal string. |
|
|
130
|
+
| `computePercentile(sorted, percentile)` | Linear interpolation. Empty input returns `null`. |
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
NumberUtils.formatBigInt(1_500000n, 6) // "1.50"
|
|
134
|
+
NumberUtils.formatSwapAmount(0.000015045814) // "0.0000150"
|
|
135
|
+
NumberUtils.expToString(1e23) // "100000000000000000000000"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`formatBigInt` on a `DecimalLike` reads `toFixed(0)` and treats that as the atomic integer.
|
|
139
|
+
|
|
140
|
+
## BigIntUtils
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
type BigIntInput = bigint | number | string | DecimalLike
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A `bigint` passed to `toBigInt` is returned as-is. `decimals` applies to numbers, strings, and `DecimalLike`.
|
|
147
|
+
|
|
148
|
+
### Conversion
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
BigIntUtils.toBigInt('1.5', 6) // 1500000n
|
|
152
|
+
BigIntUtils.toBigInt(1e-7, 18) // 100000000000n
|
|
153
|
+
BigIntUtils.toBigInt(5n, 6) // 5n
|
|
154
|
+
BigIntUtils.toNumber(1_500000n, 6) // 1.5
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`DecimalLike` uses `toFixed(decimals)`, then the same parser as a string. That keeps precision that `toNumber()` would lose.
|
|
158
|
+
|
|
159
|
+
### Arithmetic
|
|
160
|
+
|
|
161
|
+
`scale` on `multiplyByNumber` and `createPercentage` is the float-to-rational precision. Default `18` (server). Pass `10` for the web copy. It is not exported.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
BigIntUtils.multiplyByNumber(1000n, 1.5) // 1500n
|
|
165
|
+
BigIntUtils.multiplyByNumber(1000n, 0.5) // 500n
|
|
166
|
+
BigIntUtils.multiplyByNumber(amount, multiplier, 10) // web scale
|
|
167
|
+
|
|
168
|
+
BigIntUtils.createPercentage(10n, 100n) // 0.1
|
|
169
|
+
BigIntUtils.createPercentage(0.0000000005, 0.0000000025) // 0.2
|
|
170
|
+
|
|
171
|
+
BigIntUtils.abs(-3n)
|
|
172
|
+
BigIntUtils.min(1n, 2n)
|
|
173
|
+
BigIntUtils.max(1n, 2n)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
When both `createPercentage` inputs are numbers, it divides them directly and ignores `scale`.
|
|
177
|
+
|
|
178
|
+
`multiplyAsBigInt` and `usdToTokenAmount` take the decimal scale from the caller. There is no `USD_DECIMALS` export.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const USD_DECIMALS = 18 // app constant, not from this package
|
|
182
|
+
|
|
183
|
+
BigIntUtils.multiplyAsBigInt(2, 3, 2) // 6
|
|
184
|
+
BigIntUtils.usdToTokenAmount(5, 2, 6, USD_DECIMALS) // 2500000n
|
|
185
|
+
BigIntUtils.usdToTokenAmount(5, 2500, 18, USD_DECIMALS) // 2000000000000000n
|
|
186
|
+
BigIntUtils.usdToTokenAmount(0, 1, 6, USD_DECIMALS) // 0n
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Splitting
|
|
190
|
+
|
|
191
|
+
From the web bigint helpers. Remainder lands on the last bucket. Totals are exact.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
BigIntUtils.splitEvenly(100n, 3) // [33n, 33n, 34n]
|
|
195
|
+
|
|
196
|
+
BigIntUtils.splitWithVariance(amount, count, 0.2, rng)
|
|
197
|
+
// variance <= 0, count === 1, or amount === 0n falls back to splitEvenly
|
|
198
|
+
|
|
199
|
+
BigIntUtils.clampToCaps([80n, 10n], [50n, 100n]) // [50n, 40n]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`splitEvenly` and `splitWithVariance` throw when `count <= 0`. `clampToCaps` throws when the caps are shorter than the amounts, or when the total cannot fit.
|
|
203
|
+
|
|
204
|
+
## Types
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
type DecimalLike = {
|
|
208
|
+
toFixed(digits: number): string
|
|
209
|
+
toNumber(): number
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
type NumberInput = number | DecimalLike | null | undefined
|
|
213
|
+
type UsdInput = NumberInput | string
|
|
214
|
+
type BigIntInput = bigint | number | string | DecimalLike
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Prisma's `Decimal` and decimal.js both satisfy `DecimalLike`. Detection is structural (`toFixed` and `toNumber`), not `instanceof`.
|
|
218
|
+
|
|
219
|
+
## Names that moved
|
|
220
|
+
|
|
221
|
+
| Wallet today | This package |
|
|
222
|
+
| --- | --- |
|
|
223
|
+
| `formatNumber` | `formatNumber` |
|
|
224
|
+
| `formatCurrencyAmount` | `formatUsdBalance` (adds `$`, sign in front) |
|
|
225
|
+
| `formatUSD` | `formatUsd` |
|
|
226
|
+
| token balance via `formatNumber` | `formatCryptoBalance` |
|
|
227
|
+
| `formatPercentage` | `formatPercentage` |
|
|
228
|
+
| `formatInteger` | `formatInteger` |
|
|
229
|
+
| `formatTokenPrice` | `formatTokenPrice` (still an alias of `formatNumber`) |
|
|
230
|
+
| `USD_DECIMALS` / `PERCENTAGE_DECIMALS` | Not exported. Pass the scale in. |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type DecimalLike } from './decimal.js';
|
|
2
|
+
export type BigIntInput = bigint | number | string | DecimalLike;
|
|
3
|
+
export declare const BigIntUtils: {
|
|
4
|
+
multiplyByNumber: (value: bigint, multiplier: number, scale?: number) => bigint;
|
|
5
|
+
createPercentage: (numerator: BigIntInput, denominator: BigIntInput, scale?: number) => number;
|
|
6
|
+
abs: (value: bigint) => bigint;
|
|
7
|
+
min: (a: bigint, b: bigint) => bigint;
|
|
8
|
+
max: (a: bigint, b: bigint) => bigint;
|
|
9
|
+
toNumber: (value: BigIntInput, decimals?: number) => number;
|
|
10
|
+
toBigInt: (value: BigIntInput, decimals?: number) => bigint;
|
|
11
|
+
multiplyAsBigInt: (a: BigIntInput, b: BigIntInput, decimals: number) => number;
|
|
12
|
+
usdToTokenAmount: (usd: number, tokenPriceUsd: number, tokenDecimals: number, usdDecimals: number) => bigint;
|
|
13
|
+
splitEvenly: (amount: bigint, count: number) => bigint[];
|
|
14
|
+
splitWithVariance: (amount: bigint, count: number, variance: number, rng?: () => number) => bigint[];
|
|
15
|
+
clampToCaps: (initial: bigint[], caps: bigint[]) => bigint[];
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=bigint-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bigint-utils.d.ts","sourceRoot":"","sources":["../src/bigint-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,cAAc,CAAA;AAI9D,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,CAAA;AA4MhE,eAAO,MAAM,WAAW;8BAzLf,MAAM,cACD,MAAM;kCAaP,WAAW,eACT,WAAW;iBAcN,MAAM;aAEV,MAAM,KAAK,MAAM;aACjB,MAAM,KAAK,MAAM;sBApCR,WAAW,wBAAiB,MAAM;sBANlC,WAAW,wBAAiB,MAAM;0BA4C9B,WAAW,KAAK,WAAW,YAAY,MAAM;4BAQnE,MAAM,iBACI,MAAM,iBACN,MAAM,eACR,MAAM,KAClB,MAAM;0BA0BoB,MAAM,SAAS,MAAM,KAAG,MAAM,EAAE;gCA+BnD,MAAM,SACP,MAAM,YACH,MAAM,QACX,MAAM,MAAM,KAChB,MAAM,EAAE;2BA0CmB,MAAM,EAAE,QAAQ,MAAM,EAAE,KAAG,MAAM,EAAE;CAiDhE,CAAA"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { isDecimalLike } from './decimal.js';
|
|
2
|
+
import { expToString } from './number-utils.js';
|
|
3
|
+
import { formatUnits, parseUnits } from './units.js';
|
|
4
|
+
/**
|
|
5
|
+
* Float multipliers are turned into a rational at this scale.
|
|
6
|
+
* Server wallet code used 18. Web used 10. Pass `scale` to pick one.
|
|
7
|
+
* This is not a USD decimal count.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_FLOAT_SCALE = 18;
|
|
10
|
+
const toBigInt = (value, decimals = 0) => {
|
|
11
|
+
if (typeof value === 'bigint')
|
|
12
|
+
return value;
|
|
13
|
+
if (isDecimalLike(value))
|
|
14
|
+
return parseUnits(value.toFixed(decimals), decimals);
|
|
15
|
+
return parseUnits(expToString(value), decimals);
|
|
16
|
+
};
|
|
17
|
+
const toNumber = (value, decimals = 0) => Number(formatUnits(toBigInt(value), decimals));
|
|
18
|
+
const multiplyByNumber = (value, multiplier, scale = DEFAULT_FLOAT_SCALE) => {
|
|
19
|
+
if (!Number.isFinite(multiplier) || multiplier === 0)
|
|
20
|
+
return 0n;
|
|
21
|
+
const factor = 10 ** scale;
|
|
22
|
+
const numerator = multiplier < 1 ? 1n * BigInt(factor) : toBigInt(multiplier * factor);
|
|
23
|
+
const denominator = multiplier < 1 ? toBigInt((1 / multiplier) * factor) : 1n * BigInt(factor);
|
|
24
|
+
return (value * numerator) / denominator;
|
|
25
|
+
};
|
|
26
|
+
const createPercentage = (numerator, denominator, scale = DEFAULT_FLOAT_SCALE) => {
|
|
27
|
+
if (typeof numerator === 'number' && typeof denominator === 'number') {
|
|
28
|
+
return numerator / denominator;
|
|
29
|
+
}
|
|
30
|
+
return Number(formatUnits((toBigInt(numerator) * BigInt(10 ** scale)) / toBigInt(denominator), scale));
|
|
31
|
+
};
|
|
32
|
+
const abs = (value) => (value < 0n ? -value : value);
|
|
33
|
+
const min = (a, b) => (a < b ? a : b);
|
|
34
|
+
const max = (a, b) => (a > b ? a : b);
|
|
35
|
+
const multiplyAsBigInt = (a, b, decimals) => toNumber(toBigInt(a, decimals) * toBigInt(b, decimals), decimals * 2);
|
|
36
|
+
/**
|
|
37
|
+
* USD notional to token atomic units at `tokenPriceUsd`.
|
|
38
|
+
* `usdDecimals` is the caller's USD scale. This package does not pick one.
|
|
39
|
+
*/
|
|
40
|
+
const usdToTokenAmount = (usd, tokenPriceUsd, tokenDecimals, usdDecimals) => {
|
|
41
|
+
if (!(usd > 0) ||
|
|
42
|
+
!(tokenPriceUsd > 0) ||
|
|
43
|
+
tokenDecimals < 0 ||
|
|
44
|
+
usdDecimals < 0)
|
|
45
|
+
return 0n;
|
|
46
|
+
return ((toBigInt(usd, usdDecimals) * 10n ** BigInt(tokenDecimals)) /
|
|
47
|
+
toBigInt(tokenPriceUsd, usdDecimals));
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Split a bigint amount into `count` parts as evenly as possible.
|
|
51
|
+
* Any remainder is distributed starting from the last bucket, so that
|
|
52
|
+
* `buckets[count - 1]` absorbs the remainder bits.
|
|
53
|
+
*
|
|
54
|
+
* Guarantees:
|
|
55
|
+
* - buckets.length === count
|
|
56
|
+
* - buckets.reduce(+, 0n) === amount
|
|
57
|
+
* - every bucket >= 0n
|
|
58
|
+
*
|
|
59
|
+
* Throws when count <= 0.
|
|
60
|
+
*/
|
|
61
|
+
const splitEvenly = (amount, count) => {
|
|
62
|
+
if (count <= 0)
|
|
63
|
+
throw new Error('splitEvenly: count must be > 0');
|
|
64
|
+
if (count === 1)
|
|
65
|
+
return [amount];
|
|
66
|
+
const base = amount / BigInt(count);
|
|
67
|
+
const remainder = amount - base * BigInt(count);
|
|
68
|
+
const buckets = Array.from({ length: count }, () => base);
|
|
69
|
+
const last = buckets[count - 1];
|
|
70
|
+
if (last === undefined)
|
|
71
|
+
throw new Error('splitEvenly: missing bucket');
|
|
72
|
+
buckets[count - 1] = last + remainder;
|
|
73
|
+
return buckets;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Like `splitEvenly`, but each of the first N-1 buckets is jittered within
|
|
77
|
+
* ±`variance` (as a fraction of the even-split base, e.g. 0.2 = ±20%),
|
|
78
|
+
* and the last bucket absorbs whatever's left so the total is preserved
|
|
79
|
+
* exactly.
|
|
80
|
+
*
|
|
81
|
+
* - Returns `splitEvenly` when `variance <= 0`, `count === 1`, or `amount === 0n`.
|
|
82
|
+
* - The last bucket's variance is emergent (it absorbs the complement of
|
|
83
|
+
* the first N-1 jitters). If those buckets already sum past `amount`,
|
|
84
|
+
* the excess is taken back from them so the total stays exact and no
|
|
85
|
+
* bucket goes negative. Callers that need a strict per-bucket cap should
|
|
86
|
+
* compose with `clampToCaps`.
|
|
87
|
+
*
|
|
88
|
+
* Guarantees (same as splitEvenly):
|
|
89
|
+
* - buckets.length === count
|
|
90
|
+
* - buckets.reduce(+, 0n) === amount
|
|
91
|
+
* - every bucket >= 0n (when variance ≤ 1; throws otherwise on negative)
|
|
92
|
+
*/
|
|
93
|
+
const splitWithVariance = (amount, count, variance, rng = Math.random) => {
|
|
94
|
+
if (count <= 0)
|
|
95
|
+
throw new Error('splitWithVariance: count must be > 0');
|
|
96
|
+
if (count === 1 || variance <= 0 || amount === 0n) {
|
|
97
|
+
return splitEvenly(amount, count);
|
|
98
|
+
}
|
|
99
|
+
const clampedVariance = Math.min(1, Math.max(0, variance));
|
|
100
|
+
const precision = 1000000n;
|
|
101
|
+
const base = amount / BigInt(count);
|
|
102
|
+
const buckets = [];
|
|
103
|
+
let assigned = 0n;
|
|
104
|
+
for (let i = 0; i < count - 1; i++) {
|
|
105
|
+
const factor = 1 + (rng() * 2 - 1) * clampedVariance;
|
|
106
|
+
const scaled = BigInt(Math.round(factor * Number(precision)));
|
|
107
|
+
const bucket = (base * scaled) / precision;
|
|
108
|
+
const nonNegative = bucket < 0n ? 0n : bucket;
|
|
109
|
+
buckets.push(nonNegative);
|
|
110
|
+
assigned += nonNegative;
|
|
111
|
+
}
|
|
112
|
+
let remaining = amount - assigned;
|
|
113
|
+
if (remaining < 0n) {
|
|
114
|
+
for (let i = buckets.length - 1; i >= 0 && remaining < 0n; i--) {
|
|
115
|
+
const bucket = buckets[i] ?? 0n;
|
|
116
|
+
const deficit = -remaining;
|
|
117
|
+
const take = bucket < deficit ? bucket : deficit;
|
|
118
|
+
buckets[i] = bucket - take;
|
|
119
|
+
remaining += take;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
buckets.push(remaining < 0n ? 0n : remaining);
|
|
123
|
+
return buckets;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Given an initial per-bucket allocation and per-bucket caps, clamp each
|
|
127
|
+
* bucket to its cap and redistribute the overflow to buckets that still
|
|
128
|
+
* have headroom (largest-headroom-first), preserving the total sum.
|
|
129
|
+
*
|
|
130
|
+
* Throws if `sum(caps) < sum(initial)`.
|
|
131
|
+
*/
|
|
132
|
+
const clampToCaps = (initial, caps) => {
|
|
133
|
+
if (initial.length !== caps.length) {
|
|
134
|
+
throw new Error('clampToCaps: initial and caps must be the same length');
|
|
135
|
+
}
|
|
136
|
+
const total = initial.reduce((sum, amount) => sum + amount, 0n);
|
|
137
|
+
const totalCap = caps.reduce((sum, cap) => sum + cap, 0n);
|
|
138
|
+
if (total > totalCap) {
|
|
139
|
+
throw new Error('clampToCaps: total exceeds aggregate capacity');
|
|
140
|
+
}
|
|
141
|
+
const result = initial.map((amount, i) => {
|
|
142
|
+
const cap = caps[i] ?? 0n;
|
|
143
|
+
return amount > cap ? cap : amount;
|
|
144
|
+
});
|
|
145
|
+
let shortfall = total - result.reduce((sum, amount) => sum + amount, 0n);
|
|
146
|
+
if (shortfall === 0n)
|
|
147
|
+
return result;
|
|
148
|
+
const byHeadroomDesc = result
|
|
149
|
+
.map((_, i) => i)
|
|
150
|
+
.sort((a, b) => {
|
|
151
|
+
const headroomA = (caps[a] ?? 0n) - (result[a] ?? 0n);
|
|
152
|
+
const headroomB = (caps[b] ?? 0n) - (result[b] ?? 0n);
|
|
153
|
+
return headroomA > headroomB ? -1 : headroomA < headroomB ? 1 : 0;
|
|
154
|
+
});
|
|
155
|
+
for (const i of byHeadroomDesc) {
|
|
156
|
+
if (shortfall === 0n)
|
|
157
|
+
break;
|
|
158
|
+
const headroom = (caps[i] ?? 0n) - (result[i] ?? 0n);
|
|
159
|
+
const give = shortfall < headroom ? shortfall : headroom;
|
|
160
|
+
result[i] = (result[i] ?? 0n) + give;
|
|
161
|
+
shortfall -= give;
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
};
|
|
165
|
+
export const BigIntUtils = {
|
|
166
|
+
multiplyByNumber,
|
|
167
|
+
createPercentage,
|
|
168
|
+
abs,
|
|
169
|
+
min,
|
|
170
|
+
max,
|
|
171
|
+
toNumber,
|
|
172
|
+
toBigInt,
|
|
173
|
+
multiplyAsBigInt,
|
|
174
|
+
usdToTokenAmount,
|
|
175
|
+
splitEvenly,
|
|
176
|
+
splitWithVariance,
|
|
177
|
+
clampToCaps,
|
|
178
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural stand-in for Prisma's `Decimal` and decimal.js.
|
|
3
|
+
* Both satisfy this: `toFixed` keeps the decimal string, `toNumber` is the float view.
|
|
4
|
+
*/
|
|
5
|
+
export type DecimalLike = {
|
|
6
|
+
toFixed(digits: number): string;
|
|
7
|
+
toNumber(): number;
|
|
8
|
+
};
|
|
9
|
+
export type NumberInput = number | DecimalLike | null | undefined;
|
|
10
|
+
export declare function isDecimalLike(value: unknown): value is DecimalLike;
|
|
11
|
+
export declare function coerceNumber(value: NumberInput): number | null | undefined;
|
|
12
|
+
//# sourceMappingURL=decimal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decimal.d.ts","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAA;IAC/B,QAAQ,IAAI,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;AAEjE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAMlE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAK1E"}
|
package/dist/decimal.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function isDecimalLike(value) {
|
|
2
|
+
if (typeof value !== 'object' || value === null)
|
|
3
|
+
return false;
|
|
4
|
+
if (!('toFixed' in value) || !('toNumber' in value))
|
|
5
|
+
return false;
|
|
6
|
+
return (typeof value.toFixed === 'function' && typeof value.toNumber === 'function');
|
|
7
|
+
}
|
|
8
|
+
export function coerceNumber(value) {
|
|
9
|
+
if (value == null)
|
|
10
|
+
return value;
|
|
11
|
+
if (typeof value === 'number')
|
|
12
|
+
return value;
|
|
13
|
+
if (isDecimalLike(value))
|
|
14
|
+
return value.toNumber();
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACjE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC5D,OAAO,EACL,KAAK,mBAAmB,EACxB,WAAW,EACX,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type DecimalLike, type NumberInput } from './decimal.js';
|
|
2
|
+
export type FormatNumberOptions = {
|
|
3
|
+
showExponents?: boolean;
|
|
4
|
+
minSignificantDigits?: number;
|
|
5
|
+
maxDecimals?: number;
|
|
6
|
+
minDecimals?: number;
|
|
7
|
+
abbrThreshold?: number;
|
|
8
|
+
prefix?: string;
|
|
9
|
+
abbreviateLargeNumbers?: boolean;
|
|
10
|
+
abbrMinSignificantDigits?: number;
|
|
11
|
+
};
|
|
12
|
+
export type UsdInput = NumberInput | string;
|
|
13
|
+
export declare const expToString: (value: string | number) => string;
|
|
14
|
+
declare function computePercentile(sorted: number[], percentile: number): number | null;
|
|
15
|
+
export declare const NumberUtils: {
|
|
16
|
+
formatNumber: (value: NumberInput, { showExponents, minSignificantDigits, maxDecimals: maxDecimalsParam, minDecimals: minDecimalsParam, abbrThreshold, abbreviateLargeNumbers, abbrMinSignificantDigits, prefix, }?: FormatNumberOptions) => string;
|
|
17
|
+
formatCryptoBalance: (value: NumberInput, options?: FormatNumberOptions) => string;
|
|
18
|
+
formatUsdBalance: (value: UsdInput, options?: FormatNumberOptions) => string;
|
|
19
|
+
formatPercentage: (value: NumberInput, options?: FormatNumberOptions) => string;
|
|
20
|
+
formatUsd: (value: UsdInput) => string;
|
|
21
|
+
formatInteger: (value: NumberInput, options?: FormatNumberOptions) => string;
|
|
22
|
+
formatTokenPrice: (price: NumberInput) => string;
|
|
23
|
+
formatBigInt: (input: bigint | string | DecimalLike, decimals: number, formatOptions?: FormatNumberOptions) => string;
|
|
24
|
+
abbreviateNumber: (num: number, decimals?: number, preserveTrailingZeros?: boolean) => string;
|
|
25
|
+
formatLargeNumber: (value: number | DecimalLike) => string;
|
|
26
|
+
formatSwapAmount: (value: number | string) => string;
|
|
27
|
+
expToString: (value: string | number) => string;
|
|
28
|
+
computePercentile: typeof computePercentile;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=number-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"number-utils.d.ts","sourceRoot":"","sources":["../src/number-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AAG/E,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAA;AAE3C,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,GAAG,MAAM,KAAG,MAMpD,CAAA;AAwVD,iBAAS,iBAAiB,CACxB,MAAM,EAAE,MAAM,EAAE,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAYf;AAED,eAAO,MAAM,WAAW;0BArPf,WAAW,oLAUf,mBAAmB;iCAuKf,WAAW,YACR,mBAAmB;8BAPE,QAAQ,YAAY,mBAAmB;8BAUvC,WAAW,YAAY,mBAAmB;uBAbjD,QAAQ;2BAgBJ,WAAW,YAAY,mBAAmB;8BAGvC,WAAW;0BAGnC,MAAM,GAAG,MAAM,GAAG,WAAW,YAC1B,MAAM,kBACA,mBAAmB;4BAjP9B,MAAM;+BA8PqB,MAAM,GAAG,WAAW;8BAKrB,MAAM,GAAG,MAAM,KAAG,MAAM;yBA5UtB,MAAM,GAAG,MAAM,KAAG,MAAM;;CA6X1D,CAAA"}
|