@stellar-expert/formatter 2.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/LICENSE +21 -0
- package/README.md +56 -0
- package/index.js +10 -0
- package/package.json +24 -0
- package/src/approximation.js +14 -0
- package/src/numeric-format.js +163 -0
- package/src/stroops.js +65 -0
- package/src/timestamp-format.js +52 -0
- package/src/truncation.js +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 StellarExpert Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @stellar-expert/formatter
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@stellar-expert/formatter": "github:stellar-expert/formatter#v2.0.0",
|
|
8
|
+
...
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Stroops representation
|
|
16
|
+
|
|
17
|
+
`fromStroops(valueInStroops)` - Convert value in stroops (Int64 amount) to the normal string representation
|
|
18
|
+
|
|
19
|
+
`toStroops(value)` - Convert arbitrary stringified amount to Int64 representation
|
|
20
|
+
|
|
21
|
+
### Number formatting
|
|
22
|
+
|
|
23
|
+
`formatWithPrecision(value, precision = 7, separator = ',')` - Format a number with specified precision and decimals separator
|
|
24
|
+
|
|
25
|
+
`formatWithAutoPrecision(value, separator = ',')` - Format a number using automatically determined precision and decimals separator
|
|
26
|
+
|
|
27
|
+
`formatWithAbbreviation(value, decimals = 2)` - Convert a number to a human-readable format using abbreviation
|
|
28
|
+
|
|
29
|
+
`formatWithGrouping(value, group)` - Format a number with rounding to specific precision group
|
|
30
|
+
|
|
31
|
+
`formatPrice(value, significantDigits = 4)` - Format a number as price with specified significant digits precision
|
|
32
|
+
|
|
33
|
+
`adjustPrecision(amount)` - Format amount according to default Stellar precision
|
|
34
|
+
|
|
35
|
+
`approximatePrice(price)` - Convert rational price representation to float-point number
|
|
36
|
+
|
|
37
|
+
`isValidInt64Amount(value, denominate = true, nonZero = false)` - Check if a provided value can be safely used as token amount in Stellar
|
|
38
|
+
operations
|
|
39
|
+
|
|
40
|
+
### String truncation
|
|
41
|
+
|
|
42
|
+
`shortenString(value, symbols = 8)` - Truncate strings longer than N symbols replacing characters in the middle with ellipsis
|
|
43
|
+
|
|
44
|
+
`stripTrailingZeros(value)` - Remove trailing zero symbols from a formatted numeric string
|
|
45
|
+
|
|
46
|
+
### Date formatting
|
|
47
|
+
|
|
48
|
+
`normalizeDate(date, autoUnixFormatDetection = true)` - Convert any timestamp representation to a valid date format
|
|
49
|
+
|
|
50
|
+
`toUnixTimestamp(date)` - Convert date to a numeric UNIX timestamp representation
|
|
51
|
+
|
|
52
|
+
`formatDateUTC(date)` - Convert date to ISO-like human-readable format
|
|
53
|
+
|
|
54
|
+
## Testing
|
|
55
|
+
|
|
56
|
+
Run `jest` with `--experimental-vm-modules` NodeJS flag.
|
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './src/numeric-format.js'
|
|
2
|
+
export * from './src/timestamp-format.js'
|
|
3
|
+
export * from './src/approximation.js'
|
|
4
|
+
export * from './src/truncation.js'
|
|
5
|
+
export * from './src/stroops.js'
|
|
6
|
+
|
|
7
|
+
//changes
|
|
8
|
+
//formatLongHex -> shortenString
|
|
9
|
+
//adjustAmount -> adjustPrecision
|
|
10
|
+
//formatCurrency deprecated
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stellar-expert/formatter",
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Formatting utils and common formats for numeric, string, timestamp and binary data",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"module": "index.js",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/stellar-expert/formatter.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "orbitlens <orbit@stellar.expert>",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/stellar-expert/formatter/issues"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"jest": "^29.7.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert rational price representation to Number
|
|
3
|
+
* @param {{n: Number, d: Number}|Number|String} price
|
|
4
|
+
* @return {Number}
|
|
5
|
+
*/
|
|
6
|
+
export function approximatePrice(price) {
|
|
7
|
+
if (!price)
|
|
8
|
+
return 0
|
|
9
|
+
if (price.n)
|
|
10
|
+
return price.n / price.d
|
|
11
|
+
if (typeof price === 'string')
|
|
12
|
+
return parseFloat(price)
|
|
13
|
+
return price
|
|
14
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {stripTrailingZeros} from './truncation.js'
|
|
2
|
+
import {toStroops} from './stroops.js'
|
|
3
|
+
|
|
4
|
+
function addDecimalsSeparators(value, separator = ',', trimTrailingZeros = true) {
|
|
5
|
+
//TODO: use Bignumber.toFormat() method instead
|
|
6
|
+
//split numeric to parts
|
|
7
|
+
let [int, reminder] = value.split('.')
|
|
8
|
+
let res = ''
|
|
9
|
+
//split digit groups
|
|
10
|
+
while (int.length > 3) {
|
|
11
|
+
res = separator + int.substring(int.length - 3) + res
|
|
12
|
+
int = int.substring(0, int.length - 3)
|
|
13
|
+
}
|
|
14
|
+
//strip negative sign
|
|
15
|
+
if (int === '-') {
|
|
16
|
+
res = res.substring(1)
|
|
17
|
+
}
|
|
18
|
+
res = int + res
|
|
19
|
+
if (reminder) {
|
|
20
|
+
res += '.' + reminder
|
|
21
|
+
}
|
|
22
|
+
if (trimTrailingZeros) {
|
|
23
|
+
res = stripTrailingZeros(res)
|
|
24
|
+
}
|
|
25
|
+
return res
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a provided value can be safely used as token amount in Stellar operations
|
|
30
|
+
* @param {String} value
|
|
31
|
+
* @param {Boolean} denominate
|
|
32
|
+
* @param {Boolean} nonZero
|
|
33
|
+
* @return {Boolean}
|
|
34
|
+
*/
|
|
35
|
+
export function isValidInt64Amount(value, denominate = true, nonZero = false) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = denominate ?
|
|
38
|
+
toStroops(value) :
|
|
39
|
+
BigInt(value)
|
|
40
|
+
if (nonZero) {
|
|
41
|
+
if (parsed <= 0n)
|
|
42
|
+
return false
|
|
43
|
+
} else {
|
|
44
|
+
if (parsed < 0n)
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
return parsed <= 9223372036854775807n
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Format a number with specified precision and decimals separator
|
|
55
|
+
* @param {String|Number|BigInt} value - Numeric value to format
|
|
56
|
+
* @param {Number} [precision] - Desired precision (7 digits by default)
|
|
57
|
+
* @param {String} [separator] - Decimals separator
|
|
58
|
+
* @return {String}
|
|
59
|
+
*/
|
|
60
|
+
export function formatWithPrecision(value, precision = 7, separator = ',') {
|
|
61
|
+
return addDecimalsSeparators(setPrecision(value, precision), separator, true)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format a number using automatically determined precision
|
|
66
|
+
* @param {String|Number} value - Numeric value to format
|
|
67
|
+
* @param {String} [separator] - Decimals separator
|
|
68
|
+
* @return {String}
|
|
69
|
+
*/
|
|
70
|
+
export function formatWithAutoPrecision(value, separator = ',') {
|
|
71
|
+
if (!value)
|
|
72
|
+
return '0'
|
|
73
|
+
const p = Math.ceil(Math.log10(parseFloat(value)))
|
|
74
|
+
let reminderPrecision = p > 1 ?
|
|
75
|
+
(3 - p) :
|
|
76
|
+
(Math.abs(p) + 2)
|
|
77
|
+
if (reminderPrecision < 0) {
|
|
78
|
+
reminderPrecision = 0
|
|
79
|
+
}
|
|
80
|
+
return formatWithPrecision(value, reminderPrecision, separator)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Convert a number to a human-readable format using abbreviation
|
|
85
|
+
* @param {Number} value - Value to format
|
|
86
|
+
* @param {Number} [decimals] - Precision of the abbreviated string to retain
|
|
87
|
+
* @return {String}
|
|
88
|
+
*/
|
|
89
|
+
export function formatWithAbbreviation(value, decimals = 2) {
|
|
90
|
+
let abs = Math.abs(value)
|
|
91
|
+
const tier = Math.log10(abs) / 3 | 0
|
|
92
|
+
|
|
93
|
+
if (tier <= 0)
|
|
94
|
+
return formatWithAutoPrecision(value)
|
|
95
|
+
|
|
96
|
+
const suffix = ['', 'K', 'M', 'G', 'T', 'P'][tier]
|
|
97
|
+
abs = stripTrailingZeros((abs / Math.pow(10, tier * 3)).toFixed(decimals))
|
|
98
|
+
return `${value < 0 ? '-' : ''}${abs || '0'}${suffix}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Format a number with rounding to specific precision group
|
|
103
|
+
* @param {String|Number|BigInt} value - Value to format
|
|
104
|
+
* @param {Number} group - Logarithmic group rate for rounding
|
|
105
|
+
* @return {String}
|
|
106
|
+
*/
|
|
107
|
+
export function formatWithGrouping(value, group) {
|
|
108
|
+
if (!value)
|
|
109
|
+
return '0'
|
|
110
|
+
const precision = (group >= 1 || group === 0) ?
|
|
111
|
+
0 :
|
|
112
|
+
Math.abs(Math.log10(group))
|
|
113
|
+
if (group >= 1) {
|
|
114
|
+
value = Math.ceil(value / group) * group
|
|
115
|
+
}
|
|
116
|
+
return formatWithPrecision(value, precision)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Format a number as price with specified significant digits precision
|
|
121
|
+
* @param {String|Number|Bignumber} value - Value to format
|
|
122
|
+
* @param {Number} significantDigits - Significant digits for automatic formatting
|
|
123
|
+
* @return {String}
|
|
124
|
+
*/
|
|
125
|
+
export function formatPrice(value, significantDigits = 4) {
|
|
126
|
+
const primitive = (typeof value === 'number') ?
|
|
127
|
+
value.toFixed(7) :
|
|
128
|
+
value.toString()
|
|
129
|
+
const [int, fract] = primitive.split('.')
|
|
130
|
+
if (int.replace('-', '') !== '0') {
|
|
131
|
+
significantDigits -= int.length
|
|
132
|
+
if (significantDigits < 0) {
|
|
133
|
+
significantDigits = 0
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
if (!(fract > 0))
|
|
137
|
+
return '0'
|
|
138
|
+
significantDigits = Math.max(Math.ceil(Math.abs(Math.log10(parseFloat('.' + fract)))), significantDigits)
|
|
139
|
+
}
|
|
140
|
+
return formatWithPrecision(value, significantDigits, '')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Format amount according to default Stellar precision
|
|
145
|
+
* @param {Number|String} amount - Value to format
|
|
146
|
+
* @return {String}
|
|
147
|
+
*/
|
|
148
|
+
export function adjustPrecision(amount = '0') {
|
|
149
|
+
return stripTrailingZeros(setPrecision(amount, 7))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function setPrecision(amount, precision) {
|
|
153
|
+
if (typeof amount === 'number') {
|
|
154
|
+
amount = amount.toFixed(precision)
|
|
155
|
+
} else {
|
|
156
|
+
amount = amount.toString()
|
|
157
|
+
const sidx = amount.indexOf('.')
|
|
158
|
+
if (sidx >= 0) {
|
|
159
|
+
amount = amount.slice(0, sidx + precision + 1)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return amount
|
|
163
|
+
}
|
package/src/stroops.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {stripTrailingZeros} from './truncation.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert value in stroops (Int64 amount) to the normal string representation
|
|
5
|
+
* @param {String|Number|BigInt} valueInStroops
|
|
6
|
+
* @return {String}
|
|
7
|
+
*/
|
|
8
|
+
export function fromStroops(valueInStroops) {
|
|
9
|
+
try {
|
|
10
|
+
let parsed = typeof valueInStroops === 'bigint' ?
|
|
11
|
+
valueInStroops :
|
|
12
|
+
BigInt(valueInStroops.toString().replace(/\.\d*/,''))
|
|
13
|
+
let negative = false
|
|
14
|
+
if (parsed < 0n) {
|
|
15
|
+
negative = true
|
|
16
|
+
parsed *= -1n
|
|
17
|
+
}
|
|
18
|
+
const int = parsed / 10000000n
|
|
19
|
+
const fract = parsed % 10000000n
|
|
20
|
+
let res = int.toString()
|
|
21
|
+
if (fract) {
|
|
22
|
+
res += '.' + fract.toString().padStart(7, '0')
|
|
23
|
+
}
|
|
24
|
+
if (negative) {
|
|
25
|
+
res = '-' + res
|
|
26
|
+
}
|
|
27
|
+
return stripTrailingZeros(res)
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return '0'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert arbitrary stringified amount to int64 representation
|
|
36
|
+
* @param {String|Number} value
|
|
37
|
+
* @return {BigInt}
|
|
38
|
+
*/
|
|
39
|
+
export function toStroops(value) {
|
|
40
|
+
if (!value)
|
|
41
|
+
return 0n
|
|
42
|
+
if (typeof value === 'number') {
|
|
43
|
+
value = value.toFixed(7)
|
|
44
|
+
}
|
|
45
|
+
if (typeof value !== 'string' || !/^-?[\d.,]+$/.test(value))
|
|
46
|
+
return 0n //invalid format
|
|
47
|
+
try {
|
|
48
|
+
let [int, decimal = '0'] = value.split('.', 2)
|
|
49
|
+
let negative = false
|
|
50
|
+
if (int.startsWith('-')) {
|
|
51
|
+
negative = true
|
|
52
|
+
int = int.slice(1)
|
|
53
|
+
}
|
|
54
|
+
let res = BigInt(int) * 10000000n + BigInt(decimal.slice(0, 7).padEnd(7, '0'))
|
|
55
|
+
if (negative) {
|
|
56
|
+
res *= -1n
|
|
57
|
+
if (res < -0x8000000000000000n) //overflow
|
|
58
|
+
return 0n
|
|
59
|
+
} else if (res > 0xFFFFFFFFFFFFFFFFn) //overflow
|
|
60
|
+
return 0n
|
|
61
|
+
return res
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return 0n
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert any timestamp representation to a valid date format
|
|
3
|
+
* @param {Date|String|Number} date - Date to parse
|
|
4
|
+
* @param {Boolean} [autoUnixFormatDetection] - Intelligent guess for dates already represented as UNIX timestamp (true by default)
|
|
5
|
+
* @return {Date} Parsed date
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeDate(date, autoUnixFormatDetection = true) {
|
|
8
|
+
//try parse string representation
|
|
9
|
+
if (typeof date === 'string') {
|
|
10
|
+
if (!/^\d+$/.test(date)) {
|
|
11
|
+
if (!date.endsWith('Z')) {
|
|
12
|
+
date += 'Z'
|
|
13
|
+
}
|
|
14
|
+
date = new Date(date)
|
|
15
|
+
} else {
|
|
16
|
+
date = parseInt(date)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//parse numeric representation
|
|
20
|
+
if (typeof date === 'number') {
|
|
21
|
+
//input resembles a UNIX timestamp
|
|
22
|
+
if (date < 2147483648 && autoUnixFormatDetection) {
|
|
23
|
+
date *= 1000
|
|
24
|
+
}
|
|
25
|
+
date = new Date(date)
|
|
26
|
+
}
|
|
27
|
+
//check validity
|
|
28
|
+
if (!(date instanceof Date) || isNaN(date.valueOf()))
|
|
29
|
+
throw new Error('Invalid timestamp ' + date)
|
|
30
|
+
return date
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert date to a numeric UNIX timestamp representation
|
|
35
|
+
* @param {Date|String|Number} date - Date to convert
|
|
36
|
+
* @return {Number} UNIX timestamp
|
|
37
|
+
*/
|
|
38
|
+
export function toUnixTimestamp(date) {
|
|
39
|
+
return Math.floor(normalizeDate(date).getTime() / 1000)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert date to ISO-like human-readable format
|
|
44
|
+
* @param {Date|String|Number} date - Date to convert
|
|
45
|
+
* @return {String}
|
|
46
|
+
*/
|
|
47
|
+
export function formatDateUTC(date) {
|
|
48
|
+
return normalizeDate(date)
|
|
49
|
+
.toISOString()
|
|
50
|
+
.replace(/(T|\.\d+Z)/g, ' ') // make it more human friendly
|
|
51
|
+
.trim()
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove trailing zero symbols from a formatted numeric string
|
|
3
|
+
* @param {String} value
|
|
4
|
+
* @return {String}
|
|
5
|
+
*/
|
|
6
|
+
export function stripTrailingZeros(value) {
|
|
7
|
+
if (typeof value !== 'string')
|
|
8
|
+
return value
|
|
9
|
+
let [int, reminder] = value.split('.')
|
|
10
|
+
if (!reminder)
|
|
11
|
+
return int
|
|
12
|
+
reminder = reminder.replace(/0+$/, '')
|
|
13
|
+
if (!reminder.length)
|
|
14
|
+
return int
|
|
15
|
+
return int + '.' + reminder
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Truncate strings longer than N symbols replacing characters in the middle with ellipsis
|
|
20
|
+
* @param {String} value - Original string
|
|
21
|
+
* @param {Number} [symbols] - Maximum string length
|
|
22
|
+
* @return {String}
|
|
23
|
+
*/
|
|
24
|
+
export function shortenString(value, symbols = 8) {
|
|
25
|
+
if (!value || value.length <= symbols)
|
|
26
|
+
return value
|
|
27
|
+
const affixLength = Math.max(2, Math.floor(symbols / 2))
|
|
28
|
+
return value.substring(0, affixLength) + '…' + value.substring(value.length - affixLength)
|
|
29
|
+
}
|