@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 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
+ }