-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add pnum * Add changeset * Refactor + add test cases * Remove eslint comment in test file * Refactor 2 arg to be number or options * Rename 2nd arg for pnum * Rename 2nd arg in jsdoc as well
- Loading branch information
1 parent
b2c408e
commit 712e7b1
Showing
4 changed files
with
282 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@penumbra-zone/types': minor | ||
--- | ||
|
||
Add pnum |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import { pnum } from './pnum.js'; | ||
import { BigNumber } from 'bignumber.js'; | ||
import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb'; | ||
|
||
describe('pnum', () => { | ||
it('should correctly parse and convert a number with decimals', () => { | ||
const result = pnum(123.456, { exponent: 3 }); | ||
|
||
expect(result.toNumber()).toBe(123.456); | ||
expect(result.toString()).toBe('123.456'); | ||
expect(result.toBigInt()).toBe(123456n); | ||
expect(result.toBigNumber()).toStrictEqual(new BigNumber('123.456')); | ||
expect(result.toLoHi().lo).toBe(123456n); | ||
expect(result.toAmount()).toStrictEqual( | ||
new Amount({ | ||
lo: 123456n, | ||
hi: 0n, | ||
}), | ||
); | ||
}); | ||
|
||
it('should correctly parse and convert a string with decimals', () => { | ||
const result = pnum('123456789.01230000', { exponent: 6 }); | ||
|
||
expect(result.toNumber()).toBe(123456789.0123); | ||
expect(result.toRoundedNumber(2)).toBe(123456789.01); | ||
expect(result.toString()).toBe('123456789.0123'); | ||
expect(result.toRoundedString()).toBe('123456789.012300'); | ||
expect(result.toFormattedString()).toBe('123,456,789.012300'); | ||
expect(result.toFormattedString({ trailingZeros: false })).toBe('123,456,789.0123'); | ||
expect(result.toBigInt()).toBe(123456789012300n); | ||
expect(result.toBigNumber()).toStrictEqual(new BigNumber('123456789.0123')); | ||
expect(result.toLoHi().lo).toBe(123456789012300n); | ||
expect(result.toAmount()).toStrictEqual( | ||
new Amount({ | ||
lo: 123456789012300n, | ||
hi: 0n, | ||
}), | ||
); | ||
}); | ||
|
||
it('should correctly parse and convert a bigint', () => { | ||
const result = pnum(9123456789n, { exponent: 6 }); | ||
|
||
expect(result.toNumber()).toBe(9123.456789); | ||
expect(result.toRoundedNumber(5)).toBe(9123.45679); | ||
expect(result.toString()).toBe('9123.456789'); | ||
expect(result.toRoundedString()).toBe('9123.456789'); | ||
expect(result.toFormattedString()).toBe('9,123.456789'); | ||
expect(result.toBigInt()).toBe(9123456789n); | ||
expect(result.toBigNumber()).toStrictEqual(new BigNumber('9123.456789')); | ||
expect(result.toLoHi().lo).toBe(9123456789n); | ||
expect(result.toAmount()).toStrictEqual( | ||
new Amount({ | ||
lo: 9123456789n, | ||
hi: 0n, | ||
}), | ||
); | ||
}); | ||
|
||
it('should correctly parse and convert a LoHi', () => { | ||
const result = pnum({ lo: 99999n, hi: 99999n }, { exponent: 6 }); | ||
|
||
expect(result.toNumber()).toBe(1844655960626881500); | ||
expect(result.toRoundedNumber(5)).toBe(1844655960626881500); | ||
expect(result.toString()).toBe('1844655960626881452.148383'); | ||
expect(result.toRoundedString()).toBe('1844655960626881452.148383'); | ||
expect(result.toFormattedString()).toBe('1,844,655,960,626,881,452.148383'); | ||
expect(result.toBigInt()).toBe(1844655960626881452148383n); | ||
expect(result.toBigNumber()).toStrictEqual(new BigNumber('1844655960626881452.148383')); | ||
expect(result.toLoHi().lo).toBe(99999n); | ||
expect(result.toLoHi().hi).toBe(99999n); | ||
expect(result.toAmount()).toStrictEqual( | ||
new Amount({ | ||
lo: 99999n, | ||
hi: 99999n, | ||
}), | ||
); | ||
}); | ||
|
||
it('should correctly parse and convert an Amount', () => { | ||
const result = pnum( | ||
new Amount({ | ||
lo: 9123456789n, | ||
hi: 0n, | ||
}), | ||
{ exponent: 6 }, | ||
); | ||
|
||
expect(result.toNumber()).toBe(9123.456789); | ||
expect(result.toRoundedNumber(5)).toBe(9123.45679); | ||
expect(result.toString()).toBe('9123.456789'); | ||
expect(result.toRoundedString()).toBe('9123.456789'); | ||
expect(result.toFormattedString()).toBe('9,123.456789'); | ||
expect(result.toBigInt()).toBe(9123456789n); | ||
expect(result.toBigNumber()).toStrictEqual(new BigNumber('9123.456789')); | ||
expect(result.toLoHi().lo).toBe(9123456789n); | ||
expect(result.toAmount()).toStrictEqual( | ||
new Amount({ | ||
lo: 9123456789n, | ||
hi: 0n, | ||
}), | ||
); | ||
}); | ||
|
||
it('should correctly parse and convert undefined', () => { | ||
const result = pnum(); | ||
|
||
expect(result.toString()).toBe('0'); | ||
expect(result.toRoundedString(2)).toBe('0.00'); | ||
expect(result.toRoundedNumber(5)).toBe(0); | ||
expect(result.toFormattedString()).toBe('0'); | ||
expect(result.toNumber()).toBe(0); | ||
expect(result.toLoHi().lo).toBe(0n); | ||
expect(result.toBigInt()).toBe(0n); | ||
expect(result.toBigNumber()).toStrictEqual(new BigNumber('0')); | ||
expect(result.toAmount()).toStrictEqual( | ||
new Amount({ | ||
lo: 0n, | ||
hi: 0n, | ||
}), | ||
); | ||
}); | ||
|
||
it('should correctly parse exponent as number or options', () => { | ||
const result1 = pnum(123455678, 4); | ||
const result2 = pnum(123455678n, { exponent: 4 }); | ||
const result3 = pnum(123455678n, 4); | ||
|
||
expect(result1.toString()).toBe('123455678'); | ||
expect(result1.toRoundedString()).toBe('123455678.0000'); | ||
expect(result1.toFormattedString()).toBe('123,455,678.0000'); | ||
|
||
expect(result2.toFormattedString()).toBe('12,345.5678'); | ||
expect(result2.toFormattedString()).toBe(result3.toFormattedString()); | ||
expect(result3.toString()).toBe('12345.5678'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { BigNumber } from 'bignumber.js'; | ||
import { round } from '@penumbra-zone/types/round'; | ||
import { LoHi, joinLoHi, splitLoHi } from '@penumbra-zone/types/lo-hi'; | ||
import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb'; | ||
import { ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb'; | ||
import { getAmount, getDisplayDenomExponentFromValueView } from '@penumbra-zone/getters/value-view'; | ||
import { removeTrailingZeros } from '@penumbra-zone/types/shortify'; | ||
|
||
/** | ||
* pnum (penumbra number) | ||
* | ||
* In Penumbra a number can be in the form of a base unit (bigint, LoHi, Amount, ValueView) | ||
* or a number with decimals for display purposes (string, number) | ||
* | ||
* This function handles all these cases automatically internally | ||
* - when input is a bigint, LoHi, Amount, or ValueView, it is assumed to be in base units | ||
* - when input is a string or number, it is multiplied by 10^exponent and converted to base units | ||
* | ||
* Likewise for all methods, the outputs are | ||
* - in base units for bigint, LoHi, Amount and ValueView | ||
* - in display form with decimals for string and number | ||
* | ||
* @param input | ||
* @param optionsOrExponent | ||
*/ | ||
function pnum( | ||
input?: string | number | LoHi | bigint | Amount | ValueView | undefined, | ||
optionsOrExponent: { exponent?: number } | number = { exponent: 0 }, | ||
) { | ||
let value: BigNumber; | ||
let exponent = | ||
typeof optionsOrExponent === 'number' ? optionsOrExponent : (optionsOrExponent.exponent ?? 0); | ||
|
||
if (typeof input === 'string' || typeof input === 'number') { | ||
value = new BigNumber(input).shiftedBy(exponent); | ||
} else if (typeof input === 'bigint') { | ||
value = new BigNumber(input.toString()); | ||
} else if (input instanceof ValueView) { | ||
const amount = getAmount(input); | ||
value = new BigNumber(joinLoHi(amount.lo, amount.hi).toString()); | ||
exponent = | ||
input.valueView.case === 'knownAssetId' ? getDisplayDenomExponentFromValueView(input) : 0; | ||
} else if ( | ||
input instanceof Amount || | ||
(typeof input === 'object' && | ||
'lo' in input && | ||
'hi' in input && | ||
typeof input.lo === 'bigint' && | ||
typeof input.hi === 'bigint') | ||
) { | ||
value = new BigNumber(joinLoHi(input.lo, input.hi).toString()); | ||
} else { | ||
value = new BigNumber(0); | ||
} | ||
|
||
return { | ||
toNumber(): number { | ||
const number = value.shiftedBy(-exponent).toNumber(); | ||
if (!Number.isFinite(number)) { | ||
throw new Error('Number exceeds JavaScript numeric limits, convert to other type instead.'); | ||
} | ||
return number; | ||
}, | ||
|
||
toRoundedNumber(decimals = exponent): number { | ||
const number = value.shiftedBy(-exponent).toNumber(); | ||
if (!Number.isFinite(number)) { | ||
throw new Error('Number exceeds JavaScript numeric limits, convert to other type instead.'); | ||
} | ||
return Number(round({ value: number, decimals })); | ||
}, | ||
|
||
toString(): string { | ||
return value.shiftedBy(-exponent).toString(); | ||
}, | ||
|
||
toRoundedString(decimals = exponent): string { | ||
return round({ value: value.shiftedBy(-exponent).toString(), decimals, trailingZeros: true }); | ||
}, | ||
|
||
toFormattedString( | ||
options: { | ||
commas?: boolean; | ||
decimals?: number; | ||
trailingZeros?: boolean; | ||
} = {}, | ||
): string { | ||
const defaultOptions = { | ||
commas: true, | ||
decimals: exponent, | ||
trailingZeros: true, | ||
}; | ||
|
||
const { commas, decimals, trailingZeros } = { | ||
...defaultOptions, | ||
...options, | ||
}; | ||
|
||
const number = value.shiftedBy(-exponent).toFormat(decimals, { | ||
decimalSeparator: '.', | ||
groupSeparator: commas ? ',' : '', | ||
groupSize: 3, | ||
}); | ||
|
||
return trailingZeros ? number : removeTrailingZeros(number); | ||
}, | ||
|
||
toBigInt(): bigint { | ||
return BigInt(value.toFixed(0)); | ||
}, | ||
|
||
toBigNumber(): BigNumber { | ||
return value.shiftedBy(-exponent); | ||
}, | ||
|
||
toLoHi(): LoHi { | ||
return splitLoHi(BigInt(value.toFixed(0))); | ||
}, | ||
|
||
toAmount(): Amount { | ||
return new Amount(splitLoHi(BigInt(value.toFixed(0)))); | ||
}, | ||
}; | ||
} | ||
|
||
export { pnum }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters