From 8739884756fd56c2b381b6fbd6b874a422f407ef Mon Sep 17 00:00:00 2001 From: "Fifo (Fabricius Zatti)" <62725221+fazzatti@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:08:32 -0300 Subject: [PATCH] Feat: unit conversion (#153) * feat: add unit conversion utils * test: add unit conversion unit tests * feat: add balance conversion with strings * test: add unit test to balance conversion with strings * docs: improve ts-docs for unit conversion --- src/stellar-plus/index.ts | 1 + .../utils/unit-conversion/index.ts | 176 ++++++++++++++++++ .../utils/unit-conversion/index.unit.test.ts | 128 +++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 src/stellar-plus/utils/unit-conversion/index.ts create mode 100644 src/stellar-plus/utils/unit-conversion/index.unit.test.ts diff --git a/src/stellar-plus/index.ts b/src/stellar-plus/index.ts index 33312d4..58a6279 100644 --- a/src/stellar-plus/index.ts +++ b/src/stellar-plus/index.ts @@ -9,6 +9,7 @@ export * as Account from 'stellar-plus/account/index' export * as Asset from 'stellar-plus/asset/index' export * as Network from 'stellar-plus/network' export { HorizonHandlerClient as HorizonHandler } from 'stellar-plus/horizon/index' +export * from 'stellar-plus/utils/unit-conversion' export { Core } from 'stellar-plus/core/index' diff --git a/src/stellar-plus/utils/unit-conversion/index.ts b/src/stellar-plus/utils/unit-conversion/index.ts new file mode 100644 index 0000000..9bbd795 --- /dev/null +++ b/src/stellar-plus/utils/unit-conversion/index.ts @@ -0,0 +1,176 @@ +/** + * Converts an amount from its decimal representation. + * + * This function takes an amount, either as a number or bigint, and a specified number of decimal places, + * and returns the amount divided by 10 to the power of the decimal places. + * + * @param amount - The amount to convert, either as a number or bigint. + * @param decimal - The number of decimal places to consider. + * @returns The amount converted from its decimal representation. + * + * @example + * // Converts 10000000 to 1 with 7 decimal places + * const result = fromDecimals(10000000, 7); + */ +export const fromDecimals = (amount: T, decimal: number): T => { + const multiplier = typeof amount === 'bigint' ? BigInt(10 ** decimal) : 10 ** decimal + return (amount / (multiplier as any)) as T +} + +/** + * Converts an amount to its decimal representation. + * + * This function takes an amount, either as a number or bigint, and a specified number of decimal places, + * and returns the amount multiplied by 10 to the power of the decimal places. + * + * @param amount - The amount to convert, either as a number or bigint. + * @param decimal - The number of decimal places to consider. + * @returns The amount converted to its decimal representation. + * + * @example + * // Converts 1 to 10000000 with 7 decimal places + * const result = toDecimals(1, 7); + */ +export const toDecimals = (amount: T, decimal: number): T => { + const multiplier = typeof amount === 'bigint' ? BigInt(10 ** decimal) : 10 ** decimal + return (amount * (multiplier as any)) as T +} + +/** + * Converts an amount from Stroops to the standard unit. + * + * This function takes an amount in Stroops (smallest unit), either as a number or bigint, + * and converts it to the standard unit by dividing by 10^7. + * + * @param amount - The amount in Stroops, either as a number or bigint. + * @returns The amount converted from Stroops. + * + * @example + * // Converts 10000000 Stroops to 1 standard unit + * const result = fromStroops(10000000); + */ +export const fromStroops = (amount: T): T => { + return fromDecimals(amount, 7) as T +} + +/** + * Converts an amount to Stroops from the standard unit. + * + * This function takes an amount in the standard unit, either as a number or bigint, + * and converts it to Stroops (smallest unit) by multiplying by 10^7. + * + * @param amount - The amount to convert, either as a number or bigint. + * @returns The amount converted to Stroops. + * + * @example + * // Converts 1 standard unit to 10000000 Stroops + * const result = toStroops(1); + */ +export const toStroops = (amount: T): T => { + return toDecimals(amount, 7) as T +} + +/** + * Converts a number balance to its string representation with a specified number of decimal places. + * + * This function takes a number and converts it to a string with the specified number of decimal places. + * It handles rounding and ensures the fractional part is padded with zeros if necessary. + * + * @param amount - The number balance to convert. + * @param decimal - The number of decimal places to include in the string. + * @returns The string representation of the number balance. + * + * @example + * // Converts 123.456 to '123.4560' with 4 decimal places + * const result = numberBalanceToString(123.456, 4); + */ +export const numberBalanceToString = (amount: number, decimal: number): string => { + const integerPart = Math.floor(amount).toString() + const multiplier = 10 ** decimal + const fractionalPart = Math.round((amount - Math.floor(amount)) * multiplier) + .toString() + .padStart(decimal, '0') + return `${integerPart}.${fractionalPart}` +} + +/** + * Converts a string representation of a number balance to its numeric form. + * + * This function takes a string representing a number with a decimal point and converts it to a number. + * It handles both integer and fractional parts. + * + * @param amountStr - The string representation of the number balance. + * @returns The numeric form of the balance. + * + * @example + * // Converts '123.456' to 123.456 + * const result = numberBalanceFromString('123.456'); + */ +export const numberBalanceFromString = (amountStr: string): number => { + const [integerPart, fractionalPart = ''] = amountStr.split('.') + const integerAmount = parseInt(integerPart, 10) + const fractionalAmount = fractionalPart ? parseInt(fractionalPart, 10) / 10 ** fractionalPart.length : 0 + return integerAmount + fractionalAmount +} + +/** + * Converts a bigint balance to its string representation with a specified number of decimal places. + * + * The bigint number will be considered as the whole number to be partitioned with the specified decimals. + * When the number of decimals is 0, no "." will be added. + * + * @param amount - The bigint balance to convert. + * @param decimal - The number of decimal places to include in the string. + * @returns The string representation of the bigint balance. + * + * @example + * // Converts 123456n to '1234.56' with 2 decimal places + * const result = bigIntBalanceToString(123456n, 2); + */ +export const bigIntBalanceToString = (amount: bigint, decimal: number): string => { + if (decimal === 0) return amount.toString() + const amountStr = amount.toString() + const integerPart = amountStr.slice(0, -decimal) || '0' + const fractionalPart = amountStr.slice(-decimal).padStart(decimal, '0') + return `${integerPart}.${fractionalPart}` +} + +/** + * Converts a string representation of a bigint balance to its bigint form. + * + * This function takes a string representing a bigint with a decimal point and converts it to a bigint. + * It handles both integer and fractional parts by removing the decimal point and concatenating the parts. + * + * @param amountStr - The string representation of the bigint balance. + * @returns The bigint form of the balance. + * + * @example + * // Converts '1234.56' to 123456n + * const result = bigIntBalanceFromString('1234.56'); + */ +export const bigIntBalanceFromString = (amountStr: string): bigint => { + const [integerPart, fractionalPart = ''] = amountStr.split('.') + const combinedStr = integerPart + fractionalPart.padEnd(fractionalPart.length, '0') + return BigInt(combinedStr) +} + +/** + * Converts a balance to its string representation with a specified number of decimal places. + * + * This function takes a balance, either as a number or bigint, and converts it to a string with the specified number of decimal places. + * It delegates the conversion to either `numberBalanceToString` or `bigIntBalanceToString` based on the type of the balance. + * + * @param amount - The balance to convert, either as a number or bigint. + * @param decimal - The number of decimal places to include in the string. + * @returns The string representation of the balance. + * + * @example + * // Converts 123.456 to '123.4560' with 4 decimal places + * const result = balanceToString(123.456, 4); + * + * // Converts 123456n to '1234.56' with 2 decimal places + * const result = balanceToString(123456n, 2); + */ +export const balanceToString = (amount: T, decimal: number): string => { + return typeof amount === 'bigint' ? bigIntBalanceToString(amount, decimal) : numberBalanceToString(amount, decimal) +} diff --git a/src/stellar-plus/utils/unit-conversion/index.unit.test.ts b/src/stellar-plus/utils/unit-conversion/index.unit.test.ts new file mode 100644 index 0000000..08419e4 --- /dev/null +++ b/src/stellar-plus/utils/unit-conversion/index.unit.test.ts @@ -0,0 +1,128 @@ +import { + fromDecimals, + fromStroops, + toDecimals, + toStroops, + bigIntBalanceToString, + bigIntBalanceFromString, + numberBalanceToString, + numberBalanceFromString, + balanceToString, +} from '.' + +describe('Unit conversion', () => { + describe('function toStroops', () => { + it('should convert number amounts to stroops(7 decimals) and return a number', () => { + expect(toStroops(152)).toBe(1520000000) + }) + + it('should convert bigint amounts to stroops(7 decimals) and return a bigint', () => { + expect(toStroops(BigInt(131))).toBe(BigInt(1310000000)) + }) + }) + + describe('function fromStroops', () => { + it('should convert number amounts from stroops(7 decimals) and return a number', () => { + expect(fromStroops(1520000000)).toBe(152) + }) + + it('should convert bigint amounts from stroops(7 decimals) and return a bigint', () => { + expect(fromStroops(BigInt(1310000000))).toBe(BigInt(131)) + }) + }) + + describe('function toDecimal', () => { + it('should convert number amounts to any decimals and return a number', () => { + expect(toDecimals(297, 2)).toBe(29700) + }) + + it('should convert bigint amounts to any decimals and return a bigint', () => { + expect(toDecimals(BigInt(987), 4)).toBe(BigInt(9870000)) + }) + }) + + describe('function fromStroops', () => { + it('should convert number amounts from any decimals and return a number', () => { + expect(fromDecimals(67500000000, 8)).toBe(675) + }) + + it('should convert bigint amounts from any decimals and return a bigint', () => { + expect(fromDecimals(BigInt(454000000), 6)).toBe(BigInt(454)) + }) + }) + + describe('number balances and strings', () => { + it('numberBalanceToString should convert number amounts to strings with a "." separator and the number of explicit decimals', () => { + expect(numberBalanceToString(329, 4)).toBe('329.0000') + expect(numberBalanceToString(329.6, 4)).toBe('329.6000') + expect(numberBalanceToString(0.006, 4)).toBe('0.0060') + }) + it('numberBalanceToString should round numbers properly when shortenning the decimals', () => { + expect(numberBalanceToString(0.3297, 3)).toBe('0.330') + expect(numberBalanceToString(0.3294, 3)).toBe('0.329') + expect(numberBalanceToString(1.006, 2)).toBe('1.01') + expect(numberBalanceToString(1.005, 2)).toBe('1.00') + }) + + it('numberBalanceFromString should convert string amounts to number considering a "." separator', () => { + expect(numberBalanceFromString('329.0000')).toBe(329) + expect(numberBalanceFromString('329.6000')).toBe(329.6) + expect(numberBalanceFromString('721.00006000')).toBe(721.00006) + expect(numberBalanceFromString('0.329')).toBe(0.329) + expect(numberBalanceFromString('0.00001000')).toBe(0.00001) + expect(numberBalanceFromString('1.01')).toBe(1.01) + expect(numberBalanceFromString('123')).toBe(123) + }) + }) + describe('bigint balances and strings', () => { + it('bigIntBalanceToString should convert bigint amounts to strings with a "." separator and the number of explicit decimals', () => { + expect(bigIntBalanceToString(BigInt(32900000000), 4)).toBe('3290000.0000') + expect(bigIntBalanceToString(BigInt(32960000000), 4)).toBe('3296000.0000') + expect(bigIntBalanceToString(BigInt(32960000000), 5)).toBe('329600.00000') + expect(bigIntBalanceToString(BigInt(32960000000), 6)).toBe('32960.000000') + expect(bigIntBalanceToString(BigInt(32960000000), 7)).toBe('3296.0000000') + expect(bigIntBalanceToString(BigInt(32960000000), 8)).toBe('329.60000000') + expect(bigIntBalanceToString(BigInt(3296), 3)).toBe('3.296') + expect(bigIntBalanceToString(BigInt(3296), 4)).toBe('0.3296') + expect(bigIntBalanceToString(BigInt(3296), 5)).toBe('0.03296') + expect(bigIntBalanceToString(BigInt(3296), 6)).toBe('0.003296') + expect(bigIntBalanceToString(BigInt(3296), 7)).toBe('0.0003296') + }) + + it('bigIntBalanceFromStringshould convert strings with a "." separator to bigint amounts', () => { + expect(bigIntBalanceFromString('3290.0000').toString()).toBe(BigInt(32900000).toString()) + expect(bigIntBalanceFromString('3296.0000').toString()).toBe(BigInt(32960000).toString()) + expect(bigIntBalanceFromString('0.3296').toString()).toBe(BigInt(3296).toString()) + expect(bigIntBalanceFromString('123').toString()).toBe(BigInt(123).toString()) + }) + + it('bigIntBalanceToString and bigIntBalanceFromStringshould should be consisten both ways', () => { + expect(bigIntBalanceToString(bigIntBalanceFromString('3290.0000'), 4)).toBe('3290.0000') + expect(bigIntBalanceToString(bigIntBalanceFromString('3290.0000'), 3)).toBe('32900.000') + expect(bigIntBalanceToString(bigIntBalanceFromString('3290.0000'), 2)).toBe('329000.00') + expect(bigIntBalanceToString(bigIntBalanceFromString('3290.0000'), 1)).toBe('3290000.0') + expect(bigIntBalanceToString(bigIntBalanceFromString('3290.0000'), 0)).toBe('32900000') + + expect(bigIntBalanceFromString(bigIntBalanceToString(BigInt(1234), 0)).toString()).toBe(BigInt(1234).toString()) + expect(bigIntBalanceFromString(bigIntBalanceToString(BigInt(5678), 1)).toString()).toBe(BigInt(5678).toString()) + expect(bigIntBalanceFromString(bigIntBalanceToString(BigInt(9876), 2)).toString()).toBe(BigInt(9876).toString()) + expect(bigIntBalanceFromString(bigIntBalanceToString(BigInt(5432), 3)).toString()).toBe(BigInt(5432).toString()) + expect(bigIntBalanceFromString(bigIntBalanceToString(BigInt(1122), 4)).toString()).toBe(BigInt(1122).toString()) + expect(bigIntBalanceFromString(bigIntBalanceToString(BigInt(1), 5)).toString()).toBe(BigInt(1).toString()) + }) + }) + + describe('general balances and strings', () => { + it('should convert number amounts to strings with a "." separator and the number of explicit decimals', () => { + expect(balanceToString(329, 4)).toBe('329.0000') + expect(balanceToString(329.6, 4)).toBe('329.6000') + expect(balanceToString(0.3296, 4)).toBe('0.3296') + }) + + it('should convert bigint amounts to strings with a "." separator and the number of explicit decimals', () => { + expect(balanceToString(BigInt(32900000000), 4)).toBe('3290000.0000') + expect(balanceToString(BigInt(32960000000), 4)).toBe('3296000.0000') + expect(balanceToString(BigInt(3296), 4)).toBe('0.3296') + }) + }) +})