Skip to content

Commit

Permalink
Feat: unit conversion (#153)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
fazzatti authored Jul 1, 2024
1 parent 4729c52 commit 8739884
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/stellar-plus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
176 changes: 176 additions & 0 deletions src/stellar-plus/utils/unit-conversion/index.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends number | bigint>(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 = <T extends number | bigint>(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 = <T extends number | bigint>(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 = <T extends number | bigint>(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 = <T extends number | bigint>(amount: T, decimal: number): string => {
return typeof amount === 'bigint' ? bigIntBalanceToString(amount, decimal) : numberBalanceToString(amount, decimal)
}
128 changes: 128 additions & 0 deletions src/stellar-plus/utils/unit-conversion/index.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
})

0 comments on commit 8739884

Please sign in to comment.