@rallycash/numbers 0.2.0 → 0.3.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @rallycash/numbers
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ecdc16e: Rename `formatUsd` to `formatUsdCompact`. Add `showCurrencySymbol`, `showNegativeSign`, and `showPositiveSign` to the display formatters.
8
+
3
9
  ## 0.2.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -18,10 +18,19 @@ yarn add @rallycash/numbers
18
18
 
19
19
  ## NumberUtils
20
20
 
21
- `formatNumber` is the general formatter. The named methods below are presets on top of it, except `formatUsd`, which always abbreviates.
21
+ `formatNumber` is the general formatter. The named formatters below are presets on top of it, except `formatUsdCompact`, `abbreviateNumber`, `formatLargeNumber`, and `formatSwapAmount`.
22
+
23
+ Two pairs that look alike:
24
+
25
+ - `formatUsdBalance` is a cash balance. `$1,234.50` stays written out, and abbreviation starts at 10 million.
26
+ - `formatUsdCompact` is a compact dollar headline. Abbreviation starts at one thousand, so the same amount is `$1.23K`.
27
+ - `formatCryptoBalance` is a token balance. It uses grouping, two fraction digits, and subscript dust (`0.0₆123`).
28
+ - `formatSwapAmount` is a swap size. It keeps three significant digits after leading zeros, with no commas and no subscripts (`0.0000150`).
22
29
 
23
30
  ### `formatNumber(value, options?)`
24
31
 
32
+ Use when no preset fits and you need to set the fraction digits, subscript behavior, or abbreviation threshold yourself.
33
+
25
34
  ```ts
26
35
  formatNumber(
27
36
  value: number | DecimalLike | null | undefined,
@@ -39,6 +48,8 @@ formatNumber(
39
48
  | `abbreviateLargeNumbers` | `true` |
40
49
  | `abbrMinSignificantDigits` | `3` |
41
50
  | `prefix` | `''` |
51
+ | `showNegativeSign` | `true` |
52
+ | `showPositiveSign` | `false` |
42
53
 
43
54
  `null`, `undefined`, and `NaN` return `''`. `0` returns `0.00` at the default fraction digits.
44
55
 
@@ -54,88 +65,159 @@ Dust smaller than `maxDecimals` places, but not small enough for a subscript, re
54
65
 
55
66
  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
67
 
57
- ### Presets
68
+ ### `formatCryptoBalance(value, options?)`
58
69
 
59
- Each preset calls `formatNumber` with fixed options. Pass `options` to override the preset. `formatUsd` does not. It always abbreviates through `abbreviateNumber`.
70
+ Use when the value is a token balance already in human units, and a million tokens should still read as `1,000,000.00`.
60
71
 
61
- #### `formatCryptoBalance(value, options?)`
62
-
63
- Token amount. Same defaults as `formatNumber`, including subscripts for dust.
72
+ Atomic units go through `formatBigInt` first.
64
73
 
65
74
  ```ts
75
+ // equivalent to -> formatNumber(value)
76
+ // Pass options to override.
66
77
  NumberUtils.formatCryptoBalance(1234.5) // "1,234.50"
67
78
  NumberUtils.formatCryptoBalance(0.0000001234) // "0.0₆123"
79
+ NumberUtils.formatCryptoBalance(0.000015045814) // "0.000015"
68
80
  ```
69
81
 
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`.
82
+ ### `formatUsdBalance(value, options?)`
75
83
 
76
- Preset options: `minDecimals: 2`, `maxDecimals: 2`, `minSignificantDigits: 0`, `showExponents: false`.
84
+ Use when the value is someone's cash balance, rewards, or fees, and `$1,234.50` should stay fully written.
77
85
 
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`.
86
+ Replaces `formatCurrencyAmount`. The `$` is applied after formatting, with the sign in front. `showCurrencySymbol` defaults to `true`. `0`, `null`, `undefined`, and `NaN` return `$0.00`. Amounts at or above 10 million still abbreviate. There is no subscript for dust.
79
87
 
80
88
  ```ts
89
+ // equivalent to -> formatNumber(value, { minDecimals: 2, maxDecimals: 2, minSignificantDigits: 0, showExponents: false })
90
+ // Pass options to override. `$` is applied outside that call.
81
91
  NumberUtils.formatUsdBalance(1234.5) // "$1,234.50"
82
92
  NumberUtils.formatUsdBalance(-1.2) // "-$1.20"
83
93
  NumberUtils.formatUsdBalance(0) // "$0.00"
84
94
  NumberUtils.formatUsdBalance(12_345_678) // "$12.3M"
95
+ NumberUtils.formatUsdBalance(1234.5, { showCurrencySymbol: false }) // "1,234.50"
96
+ NumberUtils.formatUsdBalance(1.2, { showPositiveSign: true }) // "+$1.20"
97
+ ```
98
+
99
+ ### `formatUsdCompact(value, options?)`
100
+
101
+ Use when the value is a dollar headline in a tight row: market cap, volume, liquidity, or trade size. Abbreviation starts at one thousand, not ten million.
102
+
103
+ Not a `formatNumber` wrapper. It abbreviates with two fraction digits and keeps trailing zeros, then puts `$` in front. `showCurrencySymbol` defaults to `true`. `0`, `null`, `undefined`, and `NaN` return `$0.00`.
104
+
105
+ ```ts
106
+ NumberUtils.formatUsdCompact(1.5) // "$1.50"
107
+ NumberUtils.formatUsdCompact(1234.5) // "$1.23K"
108
+ NumberUtils.formatUsdCompact(-12_345_678) // "-$12.35M"
109
+ NumberUtils.formatUsdCompact(null) // "$0.00"
110
+ NumberUtils.formatUsdCompact(1234.5, { showCurrencySymbol: false }) // "1.23K"
85
111
  ```
86
112
 
87
- #### `formatPercentage(value, options?)`
113
+ ### `formatPercentage(value, options?)`
88
114
 
89
- Preset options: `minDecimals: 0`, `maxDecimals: 2`, `minSignificantDigits: 0`, `showExponents: false`.
115
+ Use when the value is already a percent (`12.3` means 12.3%) and the caller will add `%` if the UI needs it.
90
116
 
91
- Does not append `%`.
117
+ Does not append `%`. Dust collapses to `0` because subscripts are off.
92
118
 
93
119
  ```ts
120
+ // equivalent to -> formatNumber(value, { minDecimals: 0, maxDecimals: 2, minSignificantDigits: 0, showExponents: false })
121
+ // Pass options to override.
94
122
  NumberUtils.formatPercentage(12.3) // "12.3"
95
123
  NumberUtils.formatPercentage(12.345) // "12.35"
96
124
  NumberUtils.formatPercentage(0.0000001234) // "0"
97
125
  ```
98
126
 
99
- #### `formatUsd(value)`
127
+ ### `formatInteger(value, options?)`
100
128
 
101
- Abbreviated USD. Not a `formatNumber` preset. Two fraction digits, trailing zeros kept, `$` in front.
129
+ Use when the value is a count: transactions, holders, or points. The fraction is dropped.
130
+
131
+ Counts at or above 10 million still abbreviate.
102
132
 
103
133
  ```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"
134
+ // equivalent to -> formatNumber(value, { minDecimals: 0, maxDecimals: 0, minSignificantDigits: 0, showExponents: false })
135
+ // Pass options to override.
136
+ NumberUtils.formatInteger(6737233.2323223) // "6,737,233"
137
+ NumberUtils.formatInteger(12_345_678, { abbreviateLargeNumbers: false }) // "12,345,678"
108
138
  ```
109
139
 
110
- `formatUsd(1234.5)` is `$1.23K`. `formatUsdBalance(1234.5)` is `$1,234.50`.
111
-
112
- #### `formatInteger(value, options?)`
140
+ ### `formatTokenPrice(value)`
113
141
 
114
- Whole numbers. Preset: `minDecimals: 0`, `maxDecimals: 0`, `minSignificantDigits: 0`, `showExponents: false`.
142
+ Use when the value is a token price in USD and it should follow the general formatter, including subscript dust and the 10 million abbreviation threshold. The caller adds `$`.
115
143
 
116
144
  ```ts
117
- NumberUtils.formatInteger(6737233.2323223) // "6,737,233"
145
+ // equivalent to -> formatNumber(value)
146
+ // Pass options to override.
147
+ NumberUtils.formatTokenPrice(1234.5) // "1,234.50"
148
+ NumberUtils.formatTokenPrice(0.0000001234) // "0.0₆123"
118
149
  ```
119
150
 
120
- ### Other NumberUtils methods
151
+ ### `formatBigInt(atomic, decimals, options?)`
121
152
 
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`. |
153
+ Use when the value is still in atomic units (`bigint`, integer string, or `DecimalLike`) and the result should look like `formatNumber`.
154
+
155
+ `decimals` is the token scale. A `DecimalLike` is read with `toFixed(0)` and treated as the atomic integer.
131
156
 
132
157
  ```ts
158
+ // equivalent to -> formatNumber(humanAmount)
159
+ // Pass options to override. `humanAmount` is the atomic value divided by 10 ** decimals.
133
160
  NumberUtils.formatBigInt(1_500000n, 6) // "1.50"
161
+ NumberUtils.formatBigInt('1500000', 6) // "1.50"
162
+ ```
163
+
164
+ ### `abbreviateNumber(value, decimals = 2, preserveTrailingZeros = false)`
165
+
166
+ Use when you want `K` / `M` / `B` / `T` / `P` / `E` and you are choosing the fraction digits yourself. Abbreviation starts at 1,000. This does not add `$` or grouping commas.
167
+
168
+ `preserveTrailingZeros` keeps `1.50K` instead of `1.5K`. An exact tier uses that suffix. A coefficient that rounds to `1,000` promotes to the next suffix. Negatives keep the minus.
169
+
170
+ ```ts
171
+ NumberUtils.abbreviateNumber(1_000_000, 2) // "1M"
172
+ NumberUtils.abbreviateNumber(999_500, 0) // "1M"
173
+ NumberUtils.abbreviateNumber(1500, 2) // "1.5K"
174
+ NumberUtils.abbreviateNumber(1500, 2, true) // "1.50K"
175
+ ```
176
+
177
+ ### `formatLargeNumber(value)`
178
+
179
+ Use when a stat such as market cap, liquidity, or volume should abbreviate from one thousand, with two fraction digits and trailing zeros removed. A `DecimalLike` is read with `toNumber()`. This does not add `$`.
180
+
181
+ `formatLargeNumber(1500)` is `1.5K`. `formatUsdCompact(1500)` is `$1.50K`.
182
+
183
+ ```ts
184
+ NumberUtils.formatLargeNumber(1500) // "1.5K"
185
+ NumberUtils.formatLargeNumber(1_000_000) // "1M"
186
+ ```
187
+
188
+ ### `formatSwapAmount(value)`
189
+
190
+ Use when the value is a swap input or output and the UI needs the first three significant digits, including a small amount that must not become a subscript or a rounded balance.
191
+
192
+ No commas, no `$`, no abbreviation, and no `formatNumber` rounding. A negative returns `<0`. `0` returns `0`. Scientific notation is expanded before the split, so `1.5e-7` stays a decimal.
193
+
194
+ ```ts
195
+ NumberUtils.formatSwapAmount(0.15045814096) // "0.150"
134
196
  NumberUtils.formatSwapAmount(0.000015045814) // "0.0000150"
197
+ NumberUtils.formatSwapAmount(1.5e-7) // "0.00000015"
198
+ NumberUtils.formatSwapAmount(1234.5) // "1234.5"
199
+ ```
200
+
201
+ `formatCryptoBalance(0.000015045814)` is `0.000015`. `formatSwapAmount` keeps the third significant digit: `0.0000150`.
202
+
203
+ ### `expToString(value)`
204
+
205
+ Use when a number or numeric string is in scientific notation and a later step needs the full plain decimal, not a display string. The minus stays on the outside, so `-1e-7` is `-0.0000001`.
206
+
207
+ ```ts
135
208
  NumberUtils.expToString(1e23) // "100000000000000000000000"
209
+ NumberUtils.expToString(-1e-7) // "-0.0000001"
136
210
  ```
137
211
 
138
- `formatBigInt` on a `DecimalLike` reads `toFixed(0)` and treats that as the atomic integer.
212
+ ### `computePercentile(sorted, percentile)`
213
+
214
+ Use when you already have an ascending list of numbers and need a linearly interpolated percentile. Empty input returns `null`. `50` on five sorted values is the middle value.
215
+
216
+ ```ts
217
+ NumberUtils.computePercentile([10, 20, 30, 40, 50], 50) // 30
218
+ NumberUtils.computePercentile([10, 20, 30, 40, 50], 95) // 48
219
+ NumberUtils.computePercentile([], 50) // null
220
+ ```
139
221
 
140
222
  ## BigIntUtils
141
223
 
@@ -222,7 +304,7 @@ Prisma's `Decimal` and decimal.js both satisfy `DecimalLike`. Detection is struc
222
304
  | --- | --- |
223
305
  | `formatNumber` | `formatNumber` |
224
306
  | `formatCurrencyAmount` | `formatUsdBalance` (adds `$`, sign in front) |
225
- | `formatUSD` | `formatUsd` |
307
+ | `formatUSD` | `formatUsdCompact` |
226
308
  | token balance via `formatNumber` | `formatCryptoBalance` |
227
309
  | `formatPercentage` | `formatPercentage` |
228
310
  | `formatInteger` | `formatInteger` |
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { type BigIntInput, BigIntUtils } from './bigint-utils.js';
2
2
  export type { DecimalLike, NumberInput } from './decimal.js';
3
- export { type FormatNumberOptions, NumberUtils, type UsdInput, } from './number-utils.js';
3
+ export { type FormatNumberOptions, type FormatUsdBalanceOptions, type FormatUsdOptions, NumberUtils, type SignFormatOptions, type UsdInput, } from './number-utils.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
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,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,WAAW,EACX,KAAK,iBAAiB,EACtB,KAAK,QAAQ,GACd,MAAM,mBAAmB,CAAA"}
@@ -1,5 +1,9 @@
1
1
  import { type DecimalLike, type NumberInput } from './decimal.js';
2
- export type FormatNumberOptions = {
2
+ export type SignFormatOptions = {
3
+ showNegativeSign?: boolean;
4
+ showPositiveSign?: boolean;
5
+ };
6
+ export type FormatNumberOptions = SignFormatOptions & {
3
7
  showExponents?: boolean;
4
8
  minSignificantDigits?: number;
5
9
  maxDecimals?: number;
@@ -9,21 +13,27 @@ export type FormatNumberOptions = {
9
13
  abbreviateLargeNumbers?: boolean;
10
14
  abbrMinSignificantDigits?: number;
11
15
  };
16
+ export type FormatUsdOptions = SignFormatOptions & {
17
+ showCurrencySymbol?: boolean;
18
+ };
19
+ export type FormatUsdBalanceOptions = FormatNumberOptions & {
20
+ showCurrencySymbol?: boolean;
21
+ };
12
22
  export type UsdInput = NumberInput | string;
13
23
  export declare const expToString: (value: string | number) => string;
14
24
  declare function computePercentile(sorted: number[], percentile: number): number | null;
15
25
  export declare const NumberUtils: {
16
- formatNumber: (value: NumberInput, { showExponents, minSignificantDigits, maxDecimals: maxDecimalsParam, minDecimals: minDecimalsParam, abbrThreshold, abbreviateLargeNumbers, abbrMinSignificantDigits, prefix, }?: FormatNumberOptions) => string;
26
+ formatNumber: (value: NumberInput, options?: FormatNumberOptions) => string;
17
27
  formatCryptoBalance: (value: NumberInput, options?: FormatNumberOptions) => string;
18
- formatUsdBalance: (value: UsdInput, options?: FormatNumberOptions) => string;
28
+ formatUsdBalance: (value: UsdInput, options?: FormatUsdBalanceOptions) => string;
19
29
  formatPercentage: (value: NumberInput, options?: FormatNumberOptions) => string;
20
- formatUsd: (value: UsdInput) => string;
30
+ formatUsdCompact: (value: UsdInput, options?: FormatUsdOptions) => string;
21
31
  formatInteger: (value: NumberInput, options?: FormatNumberOptions) => string;
22
- formatTokenPrice: (price: NumberInput) => string;
32
+ formatTokenPrice: (price: NumberInput, options?: FormatNumberOptions) => string;
23
33
  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;
34
+ abbreviateNumber: (num: number, decimals?: number, preserveTrailingZeros?: boolean, options?: SignFormatOptions) => string;
35
+ formatLargeNumber: (value: number | DecimalLike, options?: SignFormatOptions) => string;
36
+ formatSwapAmount: (value: number | string, options?: SignFormatOptions) => string;
27
37
  expToString: (value: string | number) => string;
28
38
  computePercentile: typeof computePercentile;
29
39
  };
@@ -1 +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"}
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,iBAAiB,GAAG;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG;IACpD,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,gBAAgB,GAAG,iBAAiB,GAAG;IACjD,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG,mBAAmB,GAAG;IAC1D,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG,MAAM,CAAA;AAE3C,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,GAAG,MAAM,KAAG,MAMpD,CAAA;AA4ZD,iBAAS,iBAAiB,CACxB,MAAM,EAAE,MAAM,EAAE,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAYf;AAED,eAAO,MAAM,WAAW;0BA/Kf,WAAW,YACT,mBAAmB;iCA+FrB,WAAW,YACR,mBAAmB;8BAftB,QAAQ,YACL,uBAAuB;8BAiBF,WAAW,YAAY,mBAAmB;8BA1B1C,QAAQ,YAAY,gBAAgB;2BA6BvC,WAAW,YAAY,mBAAmB;8BAGvC,WAAW,YAAY,mBAAmB;0BAIlE,MAAM,GAAG,MAAM,GAAG,WAAW,YAC1B,MAAM,kBACA,mBAAmB;4BA7R9B,MAAM,gEAGD,iBAAiB;+BAwSpB,MAAM,GAAG,WAAW,YACjB,iBAAiB;8BAOpB,MAAM,GAAG,MAAM,YACZ,iBAAiB,KAC1B,MAAM;yBA5Y0B,MAAM,GAAG,MAAM,KAAG,MAAM;;CAic1D,CAAA"}
@@ -56,7 +56,19 @@ function toSubscript(text) {
56
56
  }
57
57
  return result;
58
58
  }
59
- const abbreviateNumber = (num, decimals = 2, preserveTrailingZeros = false) => {
59
+ function applyDisplaySign(formatted, signed, options) {
60
+ if (!formatted)
61
+ return formatted;
62
+ const showNegativeSign = options?.showNegativeSign !== false;
63
+ const showPositiveSign = options?.showPositiveSign === true;
64
+ const unsigned = formatted.startsWith('-') ? formatted.slice(1) : formatted;
65
+ if (signed < 0 && showNegativeSign && formatted.startsWith('-'))
66
+ return formatted;
67
+ if (signed > 0 && showPositiveSign)
68
+ return `+${unsigned}`;
69
+ return unsigned;
70
+ }
71
+ const abbreviateNumber = (num, decimals = 2, preserveTrailingZeros = false, options) => {
60
72
  const lookup = [
61
73
  { value: 1, symbol: '' },
62
74
  { value: 1e3, symbol: 'K' },
@@ -68,8 +80,9 @@ const abbreviateNumber = (num, decimals = 2, preserveTrailingZeros = false) => {
68
80
  ];
69
81
  const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
70
82
  const formatFixed = (fixed) => preserveTrailingZeros ? fixed : fixed.replace(rx, '$1');
71
- if (!Number.isFinite(num))
72
- return formatFixed(num.toFixed(decimals));
83
+ if (!Number.isFinite(num)) {
84
+ return applyDisplaySign(formatFixed(num.toFixed(decimals)), num, options);
85
+ }
73
86
  const absNum = Math.abs(num);
74
87
  let tier = 0;
75
88
  for (let i = lookup.length - 1; i >= 0; i--) {
@@ -94,9 +107,9 @@ const abbreviateNumber = (num, decimals = 2, preserveTrailingZeros = false) => {
94
107
  if (Math.abs(rendered.rounded) >= 1000 && tier < lookup.length - 1) {
95
108
  rendered = renderTier(tier + 1);
96
109
  }
97
- return rendered.text;
110
+ return applyDisplaySign(rendered.text, num, options);
98
111
  };
99
- const formatNumber = (value, { showExponents = true, minSignificantDigits = 4, maxDecimals: maxDecimalsParam, minDecimals: minDecimalsParam, abbrThreshold = 1e7, abbreviateLargeNumbers = true, abbrMinSignificantDigits = 3, prefix = '', } = {}) => {
112
+ const renderFormatNumber = (value, { showExponents = true, minSignificantDigits = 4, maxDecimals: maxDecimalsParam, minDecimals: minDecimalsParam, abbrThreshold = 1e7, abbreviateLargeNumbers = true, abbrMinSignificantDigits = 3, prefix = '', } = {}) => {
100
113
  let minDecimals;
101
114
  let maxDecimals;
102
115
  if (maxDecimalsParam == null && minDecimalsParam != null) {
@@ -172,6 +185,13 @@ const formatNumber = (value, { showExponents = true, minSignificantDigits = 4, m
172
185
  maximumFractionDigits: clampedMaxDecimals,
173
186
  })}`;
174
187
  };
188
+ const formatNumber = (value, options = {}) => {
189
+ const formatted = renderFormatNumber(value, options);
190
+ const numeric = coerceNumber(value);
191
+ if (numeric == null || !Number.isFinite(numeric))
192
+ return formatted;
193
+ return applyDisplaySign(formatted, numeric, options);
194
+ };
175
195
  const USD_BALANCE_PRESET = {
176
196
  minDecimals: 2,
177
197
  maxDecimals: 2,
@@ -202,19 +222,35 @@ function coerceUsd(value) {
202
222
  }
203
223
  return coerceNumber(value);
204
224
  }
205
- function formatSignedUsd(value, formatAbs) {
225
+ function formatSignedUsd(value, formatAbs, options) {
226
+ const showCurrencySymbol = options?.showCurrencySymbol !== false;
227
+ const symbol = showCurrencySymbol ? '$' : '';
206
228
  const numeric = coerceUsd(value);
207
- if (numeric == null || !Number.isFinite(numeric) || numeric === 0)
208
- return '$0.00';
229
+ if (numeric == null || !Number.isFinite(numeric) || numeric === 0) {
230
+ return `${symbol}0.00`;
231
+ }
209
232
  const body = formatAbs(Math.abs(numeric));
210
- return numeric < 0 ? `-$${body}` : `$${body}`;
233
+ if (numeric < 0 && options?.showNegativeSign !== false)
234
+ return `-${symbol}${body}`;
235
+ if (numeric > 0 && options?.showPositiveSign === true)
236
+ return `+${symbol}${body}`;
237
+ return `${symbol}${body}`;
211
238
  }
212
- const formatUsd = (value) => formatSignedUsd(value, (absolute) => abbreviateNumber(absolute, 2, true));
213
- const formatUsdBalance = (value, options) => formatSignedUsd(value, (absolute) => formatNumber(absolute, mergePreset(USD_BALANCE_PRESET, options)));
239
+ function absoluteUsdFormatOptions(options) {
240
+ if (options === undefined)
241
+ return undefined;
242
+ return {
243
+ ...options,
244
+ showNegativeSign: true,
245
+ showPositiveSign: false,
246
+ };
247
+ }
248
+ const formatUsdCompact = (value, options) => formatSignedUsd(value, (absolute) => abbreviateNumber(absolute, 2, true), options);
249
+ const formatUsdBalance = (value, options) => formatSignedUsd(value, (absolute) => formatNumber(absolute, mergePreset(USD_BALANCE_PRESET, absoluteUsdFormatOptions(options))), options);
214
250
  const formatCryptoBalance = (value, options) => formatNumber(value, options);
215
251
  const formatPercentage = (value, options) => formatNumber(value, mergePreset(PERCENTAGE_PRESET, options));
216
252
  const formatInteger = (value, options) => formatNumber(value, mergePreset(INTEGER_PRESET, options));
217
- const formatTokenPrice = (price) => formatNumber(price);
253
+ const formatTokenPrice = (price, options) => formatNumber(price, options);
218
254
  const formatBigInt = (input, decimals, formatOptions) => {
219
255
  let atomic;
220
256
  if (typeof input === 'bigint') {
@@ -228,24 +264,29 @@ const formatBigInt = (input, decimals, formatOptions) => {
228
264
  }
229
265
  return formatNumber(Number(formatUnits(atomic, decimals)), formatOptions);
230
266
  };
231
- const formatLargeNumber = (value) => {
267
+ const formatLargeNumber = (value, options) => {
232
268
  const numeric = typeof value === 'number' ? value : value.toNumber();
233
- return abbreviateNumber(numeric, 2);
269
+ return abbreviateNumber(numeric, 2, false, options);
234
270
  };
235
- const formatSwapAmount = (value) => {
271
+ const formatSwapAmount = (value, options) => {
236
272
  const number = typeof value === 'string' ? Number.parseFloat(value) : value;
237
273
  if (!number || number === 0)
238
274
  return '0';
239
275
  if (number < 0)
240
- return '<0';
276
+ return options?.showNegativeSign === false ? '0' : '<0';
241
277
  const [integerPart, decimalPart = ''] = expToString(number).split('.');
242
- if (!decimalPart)
243
- return integerPart ?? '0';
244
- const leadingZeroLength = (decimalPart.match(/^0+/) || [''])[0].length;
245
- const significantDigits = decimalPart.replace(/^0+/, '').substring(0, 3);
246
- if (!significantDigits)
247
- return '0';
248
- return `${integerPart}.${'0'.repeat(leadingZeroLength)}${significantDigits}`;
278
+ let formatted;
279
+ if (!decimalPart) {
280
+ formatted = integerPart ?? '0';
281
+ }
282
+ else {
283
+ const leadingZeroLength = (decimalPart.match(/^0+/) || [''])[0].length;
284
+ const significantDigits = decimalPart.replace(/^0+/, '').substring(0, 3);
285
+ formatted = significantDigits
286
+ ? `${integerPart}.${'0'.repeat(leadingZeroLength)}${significantDigits}`
287
+ : '0';
288
+ }
289
+ return applyDisplaySign(formatted, number, options);
249
290
  };
250
291
  function computePercentile(sorted, percentile) {
251
292
  if (sorted.length === 0)
@@ -264,7 +305,7 @@ export const NumberUtils = {
264
305
  formatCryptoBalance,
265
306
  formatUsdBalance,
266
307
  formatPercentage,
267
- formatUsd,
308
+ formatUsdCompact,
268
309
  formatInteger,
269
310
  formatTokenPrice,
270
311
  formatBigInt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycash/numbers",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Number formatting and fixed-point bigint conversion",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -2,6 +2,9 @@ export { type BigIntInput, BigIntUtils } from './bigint-utils.js'
2
2
  export type { DecimalLike, NumberInput } from './decimal.js'
3
3
  export {
4
4
  type FormatNumberOptions,
5
+ type FormatUsdBalanceOptions,
6
+ type FormatUsdOptions,
5
7
  NumberUtils,
8
+ type SignFormatOptions,
6
9
  type UsdInput,
7
10
  } from './number-utils.js'
@@ -47,6 +47,17 @@ describe('NumberUtils.formatNumber', () => {
47
47
  NumberUtils.formatNumber(1_000_000_000, { abbreviateLargeNumbers: false }),
48
48
  ).toBe('1,000,000,000.00')
49
49
  })
50
+
51
+ it('shows a minus by default and a plus only when asked', () => {
52
+ expect(NumberUtils.formatNumber(-1.2)).toBe('-1.20')
53
+ expect(NumberUtils.formatNumber(-1.2, { showNegativeSign: false })).toBe(
54
+ '1.20',
55
+ )
56
+ expect(NumberUtils.formatNumber(1.2, { showPositiveSign: true })).toBe(
57
+ '+1.20',
58
+ )
59
+ expect(NumberUtils.formatNumber(0, { showPositiveSign: true })).toBe('0.00')
60
+ })
50
61
  })
51
62
 
52
63
  describe('NumberUtils.abbreviateNumber', () => {
@@ -70,10 +81,34 @@ describe('NumberUtils presets', () => {
70
81
  })
71
82
 
72
83
  it('formats abbreviated USD', () => {
73
- expect(NumberUtils.formatUsd(1.5)).toBe('$1.50')
74
- expect(NumberUtils.formatUsd(1234.5)).toBe('$1.23K')
75
- expect(NumberUtils.formatUsd(-12_345_678)).toBe('-$12.35M')
76
- expect(NumberUtils.formatUsd(null)).toBe('$0.00')
84
+ expect(NumberUtils.formatUsdCompact(1.5)).toBe('$1.50')
85
+ expect(NumberUtils.formatUsdCompact(1234.5)).toBe('$1.23K')
86
+ expect(NumberUtils.formatUsdCompact(-12_345_678)).toBe('-$12.35M')
87
+ expect(NumberUtils.formatUsdCompact(null)).toBe('$0.00')
88
+ })
89
+
90
+ it('can hide the dollar sign and choose the sign character', () => {
91
+ expect(
92
+ NumberUtils.formatUsdBalance(1234.5, { showCurrencySymbol: false }),
93
+ ).toBe('1,234.50')
94
+ expect(
95
+ NumberUtils.formatUsdBalance(-1.2, {
96
+ showCurrencySymbol: false,
97
+ showNegativeSign: false,
98
+ }),
99
+ ).toBe('1.20')
100
+ expect(
101
+ NumberUtils.formatUsdBalance(1.2, { showPositiveSign: true }),
102
+ ).toBe('+$1.20')
103
+ expect(
104
+ NumberUtils.formatUsdCompact(1234.5, { showCurrencySymbol: false }),
105
+ ).toBe('1.23K')
106
+ expect(
107
+ NumberUtils.formatUsdCompact(-12_345_678, { showNegativeSign: false }),
108
+ ).toBe('$12.35M')
109
+ expect(NumberUtils.formatUsdCompact(1.5, { showPositiveSign: true })).toBe(
110
+ '+$1.50',
111
+ )
77
112
  })
78
113
 
79
114
  it('formats a percentage with at most two decimals and no subscript', () => {
@@ -84,6 +119,9 @@ describe('NumberUtils presets', () => {
84
119
 
85
120
  it('formats an integer', () => {
86
121
  expect(NumberUtils.formatInteger(6737233.2323223)).toBe('6,737,233')
122
+ expect(
123
+ NumberUtils.formatInteger(12_345_678, { abbreviateLargeNumbers: false }),
124
+ ).toBe('12,345,678')
87
125
  })
88
126
  })
89
127
 
@@ -122,4 +160,26 @@ describe('NumberUtils.formatSwapAmount', () => {
122
160
  expect(NumberUtils.formatSwapAmount(1.5e-7)).toBe('0.00000015')
123
161
  expect(NumberUtils.formatSwapAmount(0.15045814096)).toBe('0.150')
124
162
  })
163
+
164
+ it('adds a plus only when asked and can hide the negative marker', () => {
165
+ expect(
166
+ NumberUtils.formatSwapAmount(0.15045814096, { showPositiveSign: true }),
167
+ ).toBe('+0.150')
168
+ expect(NumberUtils.formatSwapAmount(-1, { showNegativeSign: false })).toBe(
169
+ '0',
170
+ )
171
+ expect(NumberUtils.formatSwapAmount(-1)).toBe('<0')
172
+ })
173
+ })
174
+
175
+ describe('NumberUtils sign options on abbreviation', () => {
176
+ it('applies to abbreviateNumber and formatLargeNumber', () => {
177
+ expect(NumberUtils.abbreviateNumber(-1500, 2)).toBe('-1.5K')
178
+ expect(
179
+ NumberUtils.abbreviateNumber(-1500, 2, false, { showNegativeSign: false }),
180
+ ).toBe('1.5K')
181
+ expect(NumberUtils.formatLargeNumber(1500, { showPositiveSign: true })).toBe(
182
+ '+1.5K',
183
+ )
184
+ })
125
185
  })
@@ -1,7 +1,12 @@
1
1
  import { coerceNumber, type DecimalLike, type NumberInput } from './decimal.js'
2
2
  import { formatUnits, parseUnits } from './units.js'
3
3
 
4
- export type FormatNumberOptions = {
4
+ export type SignFormatOptions = {
5
+ showNegativeSign?: boolean
6
+ showPositiveSign?: boolean
7
+ }
8
+
9
+ export type FormatNumberOptions = SignFormatOptions & {
5
10
  showExponents?: boolean
6
11
  minSignificantDigits?: number
7
12
  maxDecimals?: number
@@ -12,6 +17,14 @@ export type FormatNumberOptions = {
12
17
  abbrMinSignificantDigits?: number
13
18
  }
14
19
 
20
+ export type FormatUsdOptions = SignFormatOptions & {
21
+ showCurrencySymbol?: boolean
22
+ }
23
+
24
+ export type FormatUsdBalanceOptions = FormatNumberOptions & {
25
+ showCurrencySymbol?: boolean
26
+ }
27
+
15
28
  export type UsdInput = NumberInput | string
16
29
 
17
30
  export const expToString = (value: string | number): string => {
@@ -86,10 +99,25 @@ function toSubscript(text: string | number) {
86
99
  return result
87
100
  }
88
101
 
102
+ function applyDisplaySign(
103
+ formatted: string,
104
+ signed: number,
105
+ options: SignFormatOptions | undefined,
106
+ ): string {
107
+ if (!formatted) return formatted
108
+ const showNegativeSign = options?.showNegativeSign !== false
109
+ const showPositiveSign = options?.showPositiveSign === true
110
+ const unsigned = formatted.startsWith('-') ? formatted.slice(1) : formatted
111
+ if (signed < 0 && showNegativeSign && formatted.startsWith('-')) return formatted
112
+ if (signed > 0 && showPositiveSign) return `+${unsigned}`
113
+ return unsigned
114
+ }
115
+
89
116
  const abbreviateNumber = (
90
117
  num: number,
91
118
  decimals = 2,
92
119
  preserveTrailingZeros = false,
120
+ options?: SignFormatOptions,
93
121
  ) => {
94
122
  const lookup = [
95
123
  { value: 1, symbol: '' },
@@ -105,7 +133,9 @@ const abbreviateNumber = (
105
133
  const formatFixed = (fixed: string) =>
106
134
  preserveTrailingZeros ? fixed : fixed.replace(rx, '$1')
107
135
 
108
- if (!Number.isFinite(num)) return formatFixed(num.toFixed(decimals))
136
+ if (!Number.isFinite(num)) {
137
+ return applyDisplaySign(formatFixed(num.toFixed(decimals)), num, options)
138
+ }
109
139
 
110
140
  const absNum = Math.abs(num)
111
141
  let tier = 0
@@ -132,10 +162,10 @@ const abbreviateNumber = (
132
162
  if (Math.abs(rendered.rounded) >= 1000 && tier < lookup.length - 1) {
133
163
  rendered = renderTier(tier + 1)
134
164
  }
135
- return rendered.text
165
+ return applyDisplaySign(rendered.text, num, options)
136
166
  }
137
167
 
138
- const formatNumber = (
168
+ const renderFormatNumber = (
139
169
  value: NumberInput,
140
170
  {
141
171
  showExponents = true,
@@ -256,6 +286,16 @@ const formatNumber = (
256
286
  )}`
257
287
  }
258
288
 
289
+ const formatNumber = (
290
+ value: NumberInput,
291
+ options: FormatNumberOptions = {},
292
+ ) => {
293
+ const formatted = renderFormatNumber(value, options)
294
+ const numeric = coerceNumber(value)
295
+ if (numeric == null || !Number.isFinite(numeric)) return formatted
296
+ return applyDisplaySign(formatted, numeric, options)
297
+ }
298
+
259
299
  const USD_BALANCE_PRESET: FormatNumberOptions = {
260
300
  minDecimals: 2,
261
301
  maxDecimals: 2,
@@ -296,20 +336,50 @@ function coerceUsd(value: UsdInput): number | null | undefined {
296
336
  function formatSignedUsd(
297
337
  value: UsdInput,
298
338
  formatAbs: (absolute: number) => string,
339
+ options?: FormatUsdOptions,
299
340
  ): string {
341
+ const showCurrencySymbol = options?.showCurrencySymbol !== false
342
+ const symbol = showCurrencySymbol ? '$' : ''
300
343
  const numeric = coerceUsd(value)
301
- if (numeric == null || !Number.isFinite(numeric) || numeric === 0)
302
- return '$0.00'
344
+ if (numeric == null || !Number.isFinite(numeric) || numeric === 0) {
345
+ return `${symbol}0.00`
346
+ }
303
347
  const body = formatAbs(Math.abs(numeric))
304
- return numeric < 0 ? `-$${body}` : `$${body}`
348
+ if (numeric < 0 && options?.showNegativeSign !== false) return `-${symbol}${body}`
349
+ if (numeric > 0 && options?.showPositiveSign === true) return `+${symbol}${body}`
350
+ return `${symbol}${body}`
305
351
  }
306
352
 
307
- const formatUsd = (value: UsdInput) =>
308
- formatSignedUsd(value, (absolute) => abbreviateNumber(absolute, 2, true))
353
+ function absoluteUsdFormatOptions(
354
+ options: FormatUsdBalanceOptions | undefined,
355
+ ): FormatNumberOptions | undefined {
356
+ if (options === undefined) return undefined
357
+ return {
358
+ ...options,
359
+ showNegativeSign: true,
360
+ showPositiveSign: false,
361
+ }
362
+ }
309
363
 
310
- const formatUsdBalance = (value: UsdInput, options?: FormatNumberOptions) =>
311
- formatSignedUsd(value, (absolute) =>
312
- formatNumber(absolute, mergePreset(USD_BALANCE_PRESET, options)),
364
+ const formatUsdCompact = (value: UsdInput, options?: FormatUsdOptions) =>
365
+ formatSignedUsd(
366
+ value,
367
+ (absolute) => abbreviateNumber(absolute, 2, true),
368
+ options,
369
+ )
370
+
371
+ const formatUsdBalance = (
372
+ value: UsdInput,
373
+ options?: FormatUsdBalanceOptions,
374
+ ) =>
375
+ formatSignedUsd(
376
+ value,
377
+ (absolute) =>
378
+ formatNumber(
379
+ absolute,
380
+ mergePreset(USD_BALANCE_PRESET, absoluteUsdFormatOptions(options)),
381
+ ),
382
+ options,
313
383
  )
314
384
 
315
385
  const formatCryptoBalance = (
@@ -323,7 +393,8 @@ const formatPercentage = (value: NumberInput, options?: FormatNumberOptions) =>
323
393
  const formatInteger = (value: NumberInput, options?: FormatNumberOptions) =>
324
394
  formatNumber(value, mergePreset(INTEGER_PRESET, options))
325
395
 
326
- const formatTokenPrice = (price: NumberInput) => formatNumber(price)
396
+ const formatTokenPrice = (price: NumberInput, options?: FormatNumberOptions) =>
397
+ formatNumber(price, options)
327
398
 
328
399
  const formatBigInt = (
329
400
  input: bigint | string | DecimalLike,
@@ -341,27 +412,37 @@ const formatBigInt = (
341
412
  return formatNumber(Number(formatUnits(atomic, decimals)), formatOptions)
342
413
  }
343
414
 
344
- const formatLargeNumber = (value: number | DecimalLike) => {
415
+ const formatLargeNumber = (
416
+ value: number | DecimalLike,
417
+ options?: SignFormatOptions,
418
+ ) => {
345
419
  const numeric = typeof value === 'number' ? value : value.toNumber()
346
- return abbreviateNumber(numeric, 2)
420
+ return abbreviateNumber(numeric, 2, false, options)
347
421
  }
348
422
 
349
- const formatSwapAmount = (value: number | string): string => {
423
+ const formatSwapAmount = (
424
+ value: number | string,
425
+ options?: SignFormatOptions,
426
+ ): string => {
350
427
  const number = typeof value === 'string' ? Number.parseFloat(value) : value
351
428
 
352
429
  if (!number || number === 0) return '0'
353
- if (number < 0) return '<0'
430
+ if (number < 0) return options?.showNegativeSign === false ? '0' : '<0'
354
431
 
355
432
  const [integerPart, decimalPart = ''] = expToString(number).split('.')
356
433
 
357
- if (!decimalPart) return integerPart ?? '0'
358
-
359
- const leadingZeroLength = (decimalPart.match(/^0+/) || [''])[0].length
360
- const significantDigits = decimalPart.replace(/^0+/, '').substring(0, 3)
361
-
362
- if (!significantDigits) return '0'
434
+ let formatted: string
435
+ if (!decimalPart) {
436
+ formatted = integerPart ?? '0'
437
+ } else {
438
+ const leadingZeroLength = (decimalPart.match(/^0+/) || [''])[0].length
439
+ const significantDigits = decimalPart.replace(/^0+/, '').substring(0, 3)
440
+ formatted = significantDigits
441
+ ? `${integerPart}.${'0'.repeat(leadingZeroLength)}${significantDigits}`
442
+ : '0'
443
+ }
363
444
 
364
- return `${integerPart}.${'0'.repeat(leadingZeroLength)}${significantDigits}`
445
+ return applyDisplaySign(formatted, number, options)
365
446
  }
366
447
 
367
448
  function computePercentile(
@@ -386,7 +467,7 @@ export const NumberUtils = {
386
467
  formatCryptoBalance,
387
468
  formatUsdBalance,
388
469
  formatPercentage,
389
- formatUsd,
470
+ formatUsdCompact,
390
471
  formatInteger,
391
472
  formatTokenPrice,
392
473
  formatBigInt,