diff --git a/src/__fixtures__/index.ts b/src/__fixtures__/index.ts index 57ee7ca55..545885647 100644 --- a/src/__fixtures__/index.ts +++ b/src/__fixtures__/index.ts @@ -1,3 +1,4 @@ export * from './bytes'; export * from './coercions'; export * from './json'; +export * from './numbers'; diff --git a/src/__fixtures__/numbers.ts b/src/__fixtures__/numbers.ts new file mode 100644 index 000000000..6328cb980 --- /dev/null +++ b/src/__fixtures__/numbers.ts @@ -0,0 +1,52 @@ +export const NUMBER_VALUES = [ + { + number: 0, + bigint: BigInt(0), + hex: '0x0', + }, + { + number: 1, + bigint: BigInt(1), + hex: '0x1', + }, + { + number: 16, + bigint: BigInt(16), + hex: '0x10', + }, + { + number: 255, + bigint: BigInt(255), + hex: '0xff', + }, + { + number: 256, + bigint: BigInt(256), + hex: '0x100', + }, + { + number: 65535, + bigint: BigInt(65535), + hex: '0xffff', + }, + { + number: 65536, + bigint: BigInt(65536), + hex: '0x10000', + }, + { + number: 4294967295, + bigint: BigInt(4294967295), + hex: '0xffffffff', + }, + { + number: 4294967296, + bigint: BigInt(4294967296), + hex: '0x100000000', + }, + { + number: 9007199254740991, + bigint: BigInt(9007199254740991), + hex: '0x1fffffffffffff', + }, +]; diff --git a/src/index.ts b/src/index.ts index 45eeb19e8..cae8ac09d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,4 +6,5 @@ export * from './hex'; export * from './json'; export * from './logging'; export * from './misc'; +export * from './number'; export * from './time'; diff --git a/src/number.test.ts b/src/number.test.ts new file mode 100644 index 000000000..a73e248a7 --- /dev/null +++ b/src/number.test.ts @@ -0,0 +1,101 @@ +import { bigIntToHex, hexToBigInt, hexToNumber, numberToHex } from './number'; +import { NUMBER_VALUES } from './__fixtures__'; + +describe('numberToHex', () => { + it.each(NUMBER_VALUES)( + 'converts a number to a hex string', + ({ number, hex }) => { + expect(numberToHex(number)).toBe(hex); + }, + ); + + it.each([true, false, null, undefined, {}, [], '', '0x', '0x0', BigInt(1)])( + 'throws if the value is not a number', + (value) => { + // @ts-expect-error Invalid type. + expect(() => numberToHex(value)).toThrow('Value must be a number.'); + }, + ); + + it.each([-1, -1e100, -Infinity, NaN])( + 'throws if the value is negative', + (value) => { + expect(() => numberToHex(value)).toThrow( + 'Value must be a non-negative number.', + ); + }, + ); + + it.each([1.1, 1e100, Infinity])( + 'throws if the value is not a safe integer', + (value) => { + expect(() => numberToHex(value)).toThrow( + 'Value is not a safe integer. Use `bigIntToHex` instead.', + ); + }, + ); +}); + +describe('bigIntToHex', () => { + it.each(NUMBER_VALUES)( + 'converts a bigint to a hex string', + ({ bigint, hex }) => { + expect(bigIntToHex(bigint)).toBe(hex); + }, + ); + + it.each([true, false, null, undefined, {}, [], '', '0x', '0x0', 1])( + 'throws if the value is not a bigint', + (value) => { + // @ts-expect-error Invalid type. + expect(() => bigIntToHex(value)).toThrow('Value must be a bigint.'); + }, + ); + + it.each([BigInt(-1), BigInt('-100')])( + 'throws if the value is negative', + (value) => { + expect(() => bigIntToHex(value)).toThrow( + 'Value must be a non-negative bigint.', + ); + }, + ); +}); + +describe('hexToNumber', () => { + it.each(NUMBER_VALUES)( + 'converts a hex string to a number', + ({ number, hex }) => { + expect(hexToNumber(hex)).toBe(number); + }, + ); + + it.each([true, false, null, undefined, 0, 1, '', [], {}, BigInt(1)])( + 'throws if the value is not a hexadecimal string', + (value) => { + // @ts-expect-error Invalid type. + expect(() => hexToNumber(value)).toThrow( + 'Value must be a hexadecimal string.', + ); + }, + ); +}); + +describe('hexToBigInt', () => { + it.each(NUMBER_VALUES)( + 'converts a hex string to a bigint', + ({ bigint, hex }) => { + expect(hexToBigInt(hex)).toBe(bigint); + }, + ); + + it.each([true, false, null, undefined, 0, 1, '', [], {}, BigInt(1)])( + 'throws if the value is not a hexadecimal string', + (value) => { + // @ts-expect-error Invalid type. + expect(() => hexToBigInt(value)).toThrow( + 'Value must be a hexadecimal string.', + ); + }, + ); +}); diff --git a/src/number.ts b/src/number.ts new file mode 100644 index 000000000..7fcc09e76 --- /dev/null +++ b/src/number.ts @@ -0,0 +1,110 @@ +import { add0x, assertIsHexString } from './hex'; +import { assert } from './assert'; + +/** + * Convert a number to a hexadecimal string. This verifies that the number is a + * non-negative safe integer. + * + * To convert a `bigint` to a hexadecimal string instead, use + * {@link bigIntToHex}. + * + * @example + * ```typescript + * numberToHex(0); // '0x0' + * numberToHex(1); // '0x1' + * numberToHex(16); // '0x10' + * ``` + * @param value - The number to convert to a hexadecimal string. + * @returns The hexadecimal string, with the "0x"-prefix. + * @throws If the number is not a non-negative safe integer. + */ +export const numberToHex = (value: number): string => { + assert(typeof value === 'number', 'Value must be a number.'); + assert(value >= 0, 'Value must be a non-negative number.'); + assert( + Number.isSafeInteger(value), + 'Value is not a safe integer. Use `bigIntToHex` instead.', + ); + + return add0x(value.toString(16)); +}; + +/** + * Convert a `bigint` to a hexadecimal string. This verifies that the `bigint` + * is a non-negative integer. + * + * To convert a number to a hexadecimal string instead, use {@link numberToHex}. + * + * @example + * ```typescript + * bigIntToHex(0n); // '0x0' + * bigIntToHex(1n); // '0x1' + * bigIntToHex(16n); // '0x10' + * ``` + * @param value - The `bigint` to convert to a hexadecimal string. + * @returns The hexadecimal string, with the "0x"-prefix. + * @throws If the `bigint` is not a non-negative integer. + */ +export const bigIntToHex = (value: bigint): string => { + assert(typeof value === 'bigint', 'Value must be a bigint.'); + assert(value >= 0, 'Value must be a non-negative bigint.'); + + return add0x(value.toString(16)); +}; + +/** + * Convert a hexadecimal string to a number. This verifies that the string is a + * valid hex string, and that the resulting number is a safe integer. Both + * "0x"-prefixed and unprefixed strings are supported. + * + * To convert a hexadecimal string to a `bigint` instead, use + * {@link hexToBigInt}. + * + * @example + * ```typescript + * hexToNumber('0x0'); // 0 + * hexToNumber('0x1'); // 1 + * hexToNumber('0x10'); // 16 + * ``` + * @param value - The hexadecimal string to convert to a number. + * @returns The number. + * @throws If the value is not a valid hexadecimal string, or if the resulting + * number is not a safe integer. + */ +export const hexToNumber = (value: string): number => { + assertIsHexString(value); + + // `parseInt` accepts values without the "0x"-prefix, whereas `Number` does + // not. Using this is slightly faster than `Number(add0x(value))`. + const numberValue = parseInt(value, 16); + + assert( + Number.isSafeInteger(numberValue), + 'Value is not a safe integer. Use `hexToBigInt` instead.', + ); + + return numberValue; +}; + +/** + * Convert a hexadecimal string to a `bigint`. This verifies that the string is + * a valid hex string. Both "0x"-prefixed and unprefixed strings are supported. + * + * To convert a hexadecimal string to a number instead, use {@link hexToNumber}. + * + * @example + * ```typescript + * hexToBigInt('0x0'); // 0n + * hexToBigInt('0x1'); // 1n + * hexToBigInt('0x10'); // 16n + * ``` + * @param value - The hexadecimal string to convert to a `bigint`. + * @returns The `bigint`. + * @throws If the value is not a valid hexadecimal string. + */ +export const hexToBigInt = (value: string): bigint => { + assertIsHexString(value); + + // The `BigInt` constructor requires the "0x"-prefix to parse a hex string. + return BigInt(add0x(value)); +};