Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: unit conversion #153

Merged
merged 6 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
})
})
})
Loading