@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
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { coerceNumber, type DecimalLike, type NumberInput } from './decimal.js'
|
|
2
|
+
import { formatUnits, parseUnits } from './units.js'
|
|
3
|
+
|
|
4
|
+
export type FormatNumberOptions = {
|
|
5
|
+
showExponents?: boolean
|
|
6
|
+
minSignificantDigits?: number
|
|
7
|
+
maxDecimals?: number
|
|
8
|
+
minDecimals?: number
|
|
9
|
+
abbrThreshold?: number
|
|
10
|
+
prefix?: string
|
|
11
|
+
abbreviateLargeNumbers?: boolean
|
|
12
|
+
abbrMinSignificantDigits?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type UsdInput = NumberInput | string
|
|
16
|
+
|
|
17
|
+
export const expToString = (value: string | number): string => {
|
|
18
|
+
const raw = value.toString()
|
|
19
|
+
const negative = raw.startsWith('-')
|
|
20
|
+
const expanded = expandUnsignedExponent(negative ? raw.slice(1) : raw)
|
|
21
|
+
if (!negative || expanded === '0') return expanded
|
|
22
|
+
return `-${expanded}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const expandUnsignedExponent = (expString: string): string => {
|
|
26
|
+
const [maybeNumber, exponent] = expString.split('e')
|
|
27
|
+
const number = maybeNumber ?? '0'
|
|
28
|
+
|
|
29
|
+
if (!exponent) return number
|
|
30
|
+
const decimalPosition = number.indexOf('.')
|
|
31
|
+
|
|
32
|
+
const removedDecimalNumber = number.replace('.', '')
|
|
33
|
+
|
|
34
|
+
const actualDecimalPosition =
|
|
35
|
+
decimalPosition === -1 ? number.length : decimalPosition
|
|
36
|
+
const decimalMovement = actualDecimalPosition + parseInt(exponent, 10)
|
|
37
|
+
|
|
38
|
+
const additionaltrailingZeros = Math.max(
|
|
39
|
+
decimalMovement - removedDecimalNumber.length,
|
|
40
|
+
0,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const additionalLeadingZeros =
|
|
44
|
+
actualDecimalPosition + parseInt(exponent, 10) < 0
|
|
45
|
+
? Math.abs(decimalMovement)
|
|
46
|
+
: 0
|
|
47
|
+
|
|
48
|
+
const numberArr = Array(additionalLeadingZeros)
|
|
49
|
+
.fill('0')
|
|
50
|
+
.concat(removedDecimalNumber.split(''))
|
|
51
|
+
.concat(Array(additionaltrailingZeros).fill('0'))
|
|
52
|
+
|
|
53
|
+
const newDecimalPosition = Math.max(decimalMovement, 0)
|
|
54
|
+
|
|
55
|
+
if (newDecimalPosition < numberArr.length) {
|
|
56
|
+
const decimalInsertion = newDecimalPosition === 0 ? '0.' : '.'
|
|
57
|
+
numberArr.splice(newDecimalPosition, 0, decimalInsertion)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return numberArr.join('')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getMagnitude(n: number) {
|
|
64
|
+
if (n === 0) return 0
|
|
65
|
+
return Math.floor(Math.log10(Math.abs(n)))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toSubscript(text: string | number) {
|
|
69
|
+
const subscriptMap: Record<string, string> = {
|
|
70
|
+
'0': '₀',
|
|
71
|
+
'1': '₁',
|
|
72
|
+
'2': '₂',
|
|
73
|
+
'3': '₃',
|
|
74
|
+
'4': '₄',
|
|
75
|
+
'5': '₅',
|
|
76
|
+
'6': '₆',
|
|
77
|
+
'7': '₇',
|
|
78
|
+
'8': '₈',
|
|
79
|
+
'9': '₉',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let result = ''
|
|
83
|
+
for (const char of expToString(text)) {
|
|
84
|
+
result += subscriptMap[char] || char
|
|
85
|
+
}
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const abbreviateNumber = (
|
|
90
|
+
num: number,
|
|
91
|
+
decimals = 2,
|
|
92
|
+
preserveTrailingZeros = false,
|
|
93
|
+
) => {
|
|
94
|
+
const lookup = [
|
|
95
|
+
{ value: 1, symbol: '' },
|
|
96
|
+
{ value: 1e3, symbol: 'K' },
|
|
97
|
+
{ value: 1e6, symbol: 'M' },
|
|
98
|
+
{ value: 1e9, symbol: 'B' },
|
|
99
|
+
{ value: 1e12, symbol: 'T' },
|
|
100
|
+
{ value: 1e15, symbol: 'P' },
|
|
101
|
+
{ value: 1e18, symbol: 'E' },
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/
|
|
105
|
+
const formatFixed = (fixed: string) =>
|
|
106
|
+
preserveTrailingZeros ? fixed : fixed.replace(rx, '$1')
|
|
107
|
+
|
|
108
|
+
if (!Number.isFinite(num)) return formatFixed(num.toFixed(decimals))
|
|
109
|
+
|
|
110
|
+
const absNum = Math.abs(num)
|
|
111
|
+
let tier = 0
|
|
112
|
+
for (let i = lookup.length - 1; i >= 0; i--) {
|
|
113
|
+
const entry = lookup[i]
|
|
114
|
+
if (entry && absNum >= entry.value) {
|
|
115
|
+
tier = i
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const renderTier = (index: number) => {
|
|
121
|
+
const entry = lookup[index]
|
|
122
|
+
if (!entry) return { text: formatFixed(num.toFixed(decimals)), rounded: num }
|
|
123
|
+
const rounded = Number((num / entry.value).toFixed(decimals) || 0)
|
|
124
|
+
const formattedNumber = rounded.toLocaleString('en-us', {
|
|
125
|
+
minimumFractionDigits: decimals,
|
|
126
|
+
maximumFractionDigits: decimals,
|
|
127
|
+
})
|
|
128
|
+
return { text: formatFixed(formattedNumber) + entry.symbol, rounded }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let rendered = renderTier(tier)
|
|
132
|
+
if (Math.abs(rendered.rounded) >= 1000 && tier < lookup.length - 1) {
|
|
133
|
+
rendered = renderTier(tier + 1)
|
|
134
|
+
}
|
|
135
|
+
return rendered.text
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const formatNumber = (
|
|
139
|
+
value: NumberInput,
|
|
140
|
+
{
|
|
141
|
+
showExponents = true,
|
|
142
|
+
minSignificantDigits = 4,
|
|
143
|
+
maxDecimals: maxDecimalsParam,
|
|
144
|
+
minDecimals: minDecimalsParam,
|
|
145
|
+
abbrThreshold = 1e7,
|
|
146
|
+
abbreviateLargeNumbers = true,
|
|
147
|
+
abbrMinSignificantDigits = 3,
|
|
148
|
+
prefix = '',
|
|
149
|
+
}: FormatNumberOptions = {},
|
|
150
|
+
) => {
|
|
151
|
+
let minDecimals: number
|
|
152
|
+
let maxDecimals: number
|
|
153
|
+
|
|
154
|
+
if (maxDecimalsParam == null && minDecimalsParam != null) {
|
|
155
|
+
minDecimals = minDecimalsParam
|
|
156
|
+
maxDecimals = Math.max(6, minDecimalsParam)
|
|
157
|
+
} else if (minDecimalsParam == null && maxDecimalsParam != null) {
|
|
158
|
+
maxDecimals = maxDecimalsParam
|
|
159
|
+
minDecimals = Math.min(2, maxDecimalsParam)
|
|
160
|
+
} else if (maxDecimalsParam != null && minDecimalsParam != null) {
|
|
161
|
+
maxDecimals = maxDecimalsParam
|
|
162
|
+
minDecimals = Math.min(maxDecimalsParam, minDecimalsParam)
|
|
163
|
+
} else {
|
|
164
|
+
maxDecimals = 6
|
|
165
|
+
minDecimals = 2
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const num = coerceNumber(value)
|
|
169
|
+
|
|
170
|
+
if (num === 0) {
|
|
171
|
+
const zeros = '0'.repeat(minDecimals)
|
|
172
|
+
return `0${zeros ? `.${zeros}` : ''}`
|
|
173
|
+
}
|
|
174
|
+
if (!num) return ''
|
|
175
|
+
const absNumber = Math.abs(num)
|
|
176
|
+
|
|
177
|
+
const magnitude = getMagnitude(num)
|
|
178
|
+
|
|
179
|
+
if (magnitude <= -Math.abs(maxDecimals) && minSignificantDigits) {
|
|
180
|
+
if (!showExponents) {
|
|
181
|
+
const numberToRound =
|
|
182
|
+
Math.abs(num) * 10 ** (Math.abs(magnitude) + minSignificantDigits - 1)
|
|
183
|
+
|
|
184
|
+
const fullNumber = expToString(
|
|
185
|
+
Math.round(numberToRound) /
|
|
186
|
+
10 ** (Math.abs(magnitude) + minSignificantDigits - 1),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const sign = num < 0 ? '-' : ''
|
|
190
|
+
return `${sign}${prefix}${fullNumber}`
|
|
191
|
+
}
|
|
192
|
+
const unsignedEndPart = Math.abs(num) / 10 ** magnitude
|
|
193
|
+
|
|
194
|
+
const formattedEndPart = Number(
|
|
195
|
+
unsignedEndPart.toFixed(minSignificantDigits - 1),
|
|
196
|
+
)
|
|
197
|
+
.toLocaleString('en-us', {
|
|
198
|
+
minimumFractionDigits: minDecimals,
|
|
199
|
+
maximumFractionDigits: Math.max(
|
|
200
|
+
0,
|
|
201
|
+
Math.min(20, Math.max(minSignificantDigits - 2, minDecimals)),
|
|
202
|
+
),
|
|
203
|
+
})
|
|
204
|
+
.replace(/\./g, '')
|
|
205
|
+
|
|
206
|
+
const sign = num < 0 ? '-' : ''
|
|
207
|
+
|
|
208
|
+
return `${sign}${prefix}0.0${toSubscript(Math.abs(magnitude) - 1)}${formattedEndPart}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const smallestDisplayableNumber = 1 / 10 ** maxDecimals
|
|
212
|
+
|
|
213
|
+
if (absNumber < smallestDisplayableNumber) {
|
|
214
|
+
const zeros = '0'.repeat(minDecimals)
|
|
215
|
+
return `0${zeros ? `.${zeros}` : ''}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (abbreviateLargeNumbers && absNumber >= abbrThreshold) {
|
|
219
|
+
const numbersBeforeDecimal = (magnitude + 1) % 3 || 3
|
|
220
|
+
return `${prefix}${abbreviateNumber(
|
|
221
|
+
num,
|
|
222
|
+
Math.max(abbrMinSignificantDigits - numbersBeforeDecimal, 0),
|
|
223
|
+
)}`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (absNumber < 1) {
|
|
227
|
+
const clampedMaxDecimals = Math.max(0, Math.min(20, maxDecimals))
|
|
228
|
+
return `${prefix}${Number(num.toFixed(maxDecimals)).toLocaleString(
|
|
229
|
+
'en-us',
|
|
230
|
+
{
|
|
231
|
+
minimumFractionDigits: minDecimals,
|
|
232
|
+
maximumFractionDigits: clampedMaxDecimals,
|
|
233
|
+
},
|
|
234
|
+
)}`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (absNumber >= 1 && absNumber < 10000) {
|
|
238
|
+
const clampedMaxDecimals = Math.max(0, Math.min(20, maxDecimals))
|
|
239
|
+
return `${prefix}${Number(
|
|
240
|
+
num.toFixed(Math.min(maxDecimals, 2)),
|
|
241
|
+
).toLocaleString('en-us', {
|
|
242
|
+
minimumFractionDigits: minDecimals,
|
|
243
|
+
maximumFractionDigits: clampedMaxDecimals,
|
|
244
|
+
})}`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const rounded = maxDecimals === 0 ? Math.round(num) : num
|
|
248
|
+
const clampedMaxDecimals = Math.max(0, Math.min(20, maxDecimals))
|
|
249
|
+
|
|
250
|
+
return `${prefix}${Number(rounded.toFixed(maxDecimals)).toLocaleString(
|
|
251
|
+
'en-us',
|
|
252
|
+
{
|
|
253
|
+
minimumFractionDigits: minDecimals,
|
|
254
|
+
maximumFractionDigits: clampedMaxDecimals,
|
|
255
|
+
},
|
|
256
|
+
)}`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const USD_BALANCE_PRESET: FormatNumberOptions = {
|
|
260
|
+
minDecimals: 2,
|
|
261
|
+
maxDecimals: 2,
|
|
262
|
+
minSignificantDigits: 0,
|
|
263
|
+
showExponents: false,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const PERCENTAGE_PRESET: FormatNumberOptions = {
|
|
267
|
+
minDecimals: 0,
|
|
268
|
+
maxDecimals: 2,
|
|
269
|
+
minSignificantDigits: 0,
|
|
270
|
+
showExponents: false,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const INTEGER_PRESET: FormatNumberOptions = {
|
|
274
|
+
minDecimals: 0,
|
|
275
|
+
maxDecimals: 0,
|
|
276
|
+
minSignificantDigits: 0,
|
|
277
|
+
showExponents: false,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function mergePreset(
|
|
281
|
+
preset: FormatNumberOptions,
|
|
282
|
+
options: FormatNumberOptions | undefined,
|
|
283
|
+
): FormatNumberOptions {
|
|
284
|
+
if (options === undefined) return preset
|
|
285
|
+
return { ...preset, ...options }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function coerceUsd(value: UsdInput): number | null | undefined {
|
|
289
|
+
if (typeof value === 'string') {
|
|
290
|
+
const parsed = Number.parseFloat(value)
|
|
291
|
+
return Number.isNaN(parsed) ? undefined : parsed
|
|
292
|
+
}
|
|
293
|
+
return coerceNumber(value)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function formatSignedUsd(
|
|
297
|
+
value: UsdInput,
|
|
298
|
+
formatAbs: (absolute: number) => string,
|
|
299
|
+
): string {
|
|
300
|
+
const numeric = coerceUsd(value)
|
|
301
|
+
if (numeric == null || !Number.isFinite(numeric) || numeric === 0)
|
|
302
|
+
return '$0.00'
|
|
303
|
+
const body = formatAbs(Math.abs(numeric))
|
|
304
|
+
return numeric < 0 ? `-$${body}` : `$${body}`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const formatUsd = (value: UsdInput) =>
|
|
308
|
+
formatSignedUsd(value, (absolute) => abbreviateNumber(absolute, 2, true))
|
|
309
|
+
|
|
310
|
+
const formatUsdBalance = (value: UsdInput, options?: FormatNumberOptions) =>
|
|
311
|
+
formatSignedUsd(value, (absolute) =>
|
|
312
|
+
formatNumber(absolute, mergePreset(USD_BALANCE_PRESET, options)),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
const formatCryptoBalance = (
|
|
316
|
+
value: NumberInput,
|
|
317
|
+
options?: FormatNumberOptions,
|
|
318
|
+
) => formatNumber(value, options)
|
|
319
|
+
|
|
320
|
+
const formatPercentage = (value: NumberInput, options?: FormatNumberOptions) =>
|
|
321
|
+
formatNumber(value, mergePreset(PERCENTAGE_PRESET, options))
|
|
322
|
+
|
|
323
|
+
const formatInteger = (value: NumberInput, options?: FormatNumberOptions) =>
|
|
324
|
+
formatNumber(value, mergePreset(INTEGER_PRESET, options))
|
|
325
|
+
|
|
326
|
+
const formatTokenPrice = (price: NumberInput) => formatNumber(price)
|
|
327
|
+
|
|
328
|
+
const formatBigInt = (
|
|
329
|
+
input: bigint | string | DecimalLike,
|
|
330
|
+
decimals: number,
|
|
331
|
+
formatOptions?: FormatNumberOptions,
|
|
332
|
+
) => {
|
|
333
|
+
let atomic: bigint
|
|
334
|
+
if (typeof input === 'bigint') {
|
|
335
|
+
atomic = input
|
|
336
|
+
} else if (typeof input === 'string') {
|
|
337
|
+
atomic = BigInt(input)
|
|
338
|
+
} else {
|
|
339
|
+
atomic = parseUnits(input.toFixed(0), 0)
|
|
340
|
+
}
|
|
341
|
+
return formatNumber(Number(formatUnits(atomic, decimals)), formatOptions)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const formatLargeNumber = (value: number | DecimalLike) => {
|
|
345
|
+
const numeric = typeof value === 'number' ? value : value.toNumber()
|
|
346
|
+
return abbreviateNumber(numeric, 2)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const formatSwapAmount = (value: number | string): string => {
|
|
350
|
+
const number = typeof value === 'string' ? Number.parseFloat(value) : value
|
|
351
|
+
|
|
352
|
+
if (!number || number === 0) return '0'
|
|
353
|
+
if (number < 0) return '<0'
|
|
354
|
+
|
|
355
|
+
const [integerPart, decimalPart = ''] = expToString(number).split('.')
|
|
356
|
+
|
|
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'
|
|
363
|
+
|
|
364
|
+
return `${integerPart}.${'0'.repeat(leadingZeroLength)}${significantDigits}`
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function computePercentile(
|
|
368
|
+
sorted: number[],
|
|
369
|
+
percentile: number,
|
|
370
|
+
): number | null {
|
|
371
|
+
if (sorted.length === 0) return null
|
|
372
|
+
|
|
373
|
+
const index = (percentile / 100) * (sorted.length - 1)
|
|
374
|
+
const lower = Math.floor(index)
|
|
375
|
+
const upper = Math.ceil(index)
|
|
376
|
+
|
|
377
|
+
if (lower === upper) return sorted[lower] ?? null
|
|
378
|
+
|
|
379
|
+
const lowerValue = sorted[lower] ?? 0
|
|
380
|
+
const upperValue = sorted[upper] ?? lowerValue
|
|
381
|
+
return lowerValue + (upperValue - lowerValue) * (index - lower)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export const NumberUtils = {
|
|
385
|
+
formatNumber,
|
|
386
|
+
formatCryptoBalance,
|
|
387
|
+
formatUsdBalance,
|
|
388
|
+
formatPercentage,
|
|
389
|
+
formatUsd,
|
|
390
|
+
formatInteger,
|
|
391
|
+
formatTokenPrice,
|
|
392
|
+
formatBigInt,
|
|
393
|
+
abbreviateNumber,
|
|
394
|
+
formatLargeNumber,
|
|
395
|
+
formatSwapAmount,
|
|
396
|
+
expToString,
|
|
397
|
+
computePercentile,
|
|
398
|
+
}
|
package/src/units.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `formatUnits` / `parseUnits` behavior from viem's `Value` module.
|
|
3
|
+
* Inlined so this package does not depend on viem.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function formatUnits(value: bigint, decimals: number): string {
|
|
7
|
+
assertDecimals(decimals)
|
|
8
|
+
|
|
9
|
+
let display = value.toString()
|
|
10
|
+
const negative = display.startsWith('-')
|
|
11
|
+
if (negative) display = display.slice(1)
|
|
12
|
+
|
|
13
|
+
display = display.padStart(decimals, '0')
|
|
14
|
+
|
|
15
|
+
const integer = display.slice(0, display.length - decimals)
|
|
16
|
+
let fraction = display.slice(display.length - decimals)
|
|
17
|
+
fraction = fraction.replace(/(0+)$/, '')
|
|
18
|
+
return `${negative ? '-' : ''}${integer || '0'}${fraction ? `.${fraction}` : ''}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseUnits(value: string, decimals: number): bigint {
|
|
22
|
+
assertDecimals(decimals)
|
|
23
|
+
|
|
24
|
+
if (!/^-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$/.test(value)) {
|
|
25
|
+
throw new Error(`\`${value}\` is not a valid decimal number.`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let [integer = '', fraction = '0'] = value.split('.')
|
|
29
|
+
|
|
30
|
+
const negative = integer.startsWith('-')
|
|
31
|
+
if (negative) integer = integer.slice(1)
|
|
32
|
+
if (integer === '') integer = '0'
|
|
33
|
+
|
|
34
|
+
fraction = fraction.replace(/(0+)$/, '')
|
|
35
|
+
|
|
36
|
+
if (decimals === 0) {
|
|
37
|
+
const firstFractionDigit = fraction[0]
|
|
38
|
+
if (
|
|
39
|
+
fraction.length > 0 &&
|
|
40
|
+
firstFractionDigit !== undefined &&
|
|
41
|
+
Number.parseInt(firstFractionDigit, 10) >= 5
|
|
42
|
+
) {
|
|
43
|
+
integer = `${BigInt(integer) + 1n}`
|
|
44
|
+
}
|
|
45
|
+
fraction = ''
|
|
46
|
+
} else if (fraction.length > decimals) {
|
|
47
|
+
const left = fraction.slice(0, decimals)
|
|
48
|
+
const roundDigitChar = fraction.slice(decimals, decimals + 1)
|
|
49
|
+
const roundDigit = Number.parseInt(roundDigitChar, 10)
|
|
50
|
+
|
|
51
|
+
if (roundDigit >= 5) {
|
|
52
|
+
const carried = carry(left)
|
|
53
|
+
if (carried.length > decimals) {
|
|
54
|
+
fraction = carried.slice(1)
|
|
55
|
+
integer = `${BigInt(integer) + 1n}`
|
|
56
|
+
} else {
|
|
57
|
+
fraction = carried
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
fraction = left
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
fraction = fraction.padEnd(decimals, '0')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return BigInt(`${negative ? '-' : ''}${integer}${fraction}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertDecimals(decimals: number): void {
|
|
70
|
+
if (!Number.isInteger(decimals) || decimals < 0) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`decimals must be a non-negative integer. Got \`${decimals}\`.`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function carry(digits: string): string {
|
|
78
|
+
const out = digits.split('')
|
|
79
|
+
let i = out.length - 1
|
|
80
|
+
while (i >= 0) {
|
|
81
|
+
const current = out[i]
|
|
82
|
+
if (current === undefined) break
|
|
83
|
+
const next = Number.parseInt(current, 10) + 1
|
|
84
|
+
if (next < 10) {
|
|
85
|
+
out[i] = String(next)
|
|
86
|
+
return out.join('')
|
|
87
|
+
}
|
|
88
|
+
out[i] = '0'
|
|
89
|
+
i -= 1
|
|
90
|
+
}
|
|
91
|
+
return `1${out.join('')}`
|
|
92
|
+
}
|