Skip to content
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
85 changes: 85 additions & 0 deletions packages/fixed-points/src/__tests__/binary-conversions-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import '@solana/test-matchers/toBeFrozenObject';

import { SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE, SolanaError } from '@solana/errors';

import { rawBinaryFixedPoint, toSignedBinaryFixedPoint, toUnsignedBinaryFixedPoint } from '../binary';

describe('toUnsignedBinaryFixedPoint', () => {
it('converts a signed non-negative value to unsigned', () => {
const signed = rawBinaryFixedPoint('signed', 8, 4)(100n);
const unsigned = toUnsignedBinaryFixedPoint(signed);
expect(unsigned.signedness).toBe('unsigned');
expect(unsigned.raw).toBe(100n);
});

it('returns the same reference when the input is already unsigned', () => {
const unsigned = rawBinaryFixedPoint('unsigned', 8, 4)(100n);
expect(toUnsignedBinaryFixedPoint(unsigned)).toBe(unsigned);
});

it('preserves totalBits and fractionalBits', () => {
const signed = rawBinaryFixedPoint('signed', 16, 15)(1n);
const unsigned = toUnsignedBinaryFixedPoint(signed);
expect(unsigned.totalBits).toBe(16);
expect(unsigned.fractionalBits).toBe(15);
});

it('returns a frozen value', () => {
const signed = rawBinaryFixedPoint('signed', 8, 4)(100n);
expect(toUnsignedBinaryFixedPoint(signed)).toBeFrozenObject();
});

it('throws VALUE_OUT_OF_RANGE when converting a negative value', () => {
const signed = rawBinaryFixedPoint('signed', 8, 4)(-1n);
expect(() => toUnsignedBinaryFixedPoint(signed)).toThrow(
new SolanaError(SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE, {
kind: 'binaryFixedPoint',
max: 255n,
min: 0n,
raw: -1n,
signedness: 'unsigned',
totalBits: 8,
}),
);
});
});

describe('toSignedBinaryFixedPoint', () => {
it('converts an unsigned value that fits the signed range', () => {
const unsigned = rawBinaryFixedPoint('unsigned', 8, 4)(100n);
const signed = toSignedBinaryFixedPoint(unsigned);
expect(signed.signedness).toBe('signed');
expect(signed.raw).toBe(100n);
});

it('returns the same reference when the input is already signed', () => {
const signed = rawBinaryFixedPoint('signed', 8, 4)(-50n);
expect(toSignedBinaryFixedPoint(signed)).toBe(signed);
});

it('preserves totalBits and fractionalBits', () => {
const unsigned = rawBinaryFixedPoint('unsigned', 16, 15)(1n);
const signed = toSignedBinaryFixedPoint(unsigned);
expect(signed.totalBits).toBe(16);
expect(signed.fractionalBits).toBe(15);
});

it('returns a frozen value', () => {
const unsigned = rawBinaryFixedPoint('unsigned', 8, 4)(100n);
expect(toSignedBinaryFixedPoint(unsigned)).toBeFrozenObject();
});

it('throws VALUE_OUT_OF_RANGE when the unsigned value exceeds the signed upper bound', () => {
const unsigned = rawBinaryFixedPoint('unsigned', 8, 4)(200n);
expect(() => toSignedBinaryFixedPoint(unsigned)).toThrow(
new SolanaError(SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE, {
kind: 'binaryFixedPoint',
max: 127n,
min: -128n,
raw: 200n,
signedness: 'signed',
totalBits: 8,
}),
);
});
});
85 changes: 85 additions & 0 deletions packages/fixed-points/src/__tests__/decimal-conversions-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import '@solana/test-matchers/toBeFrozenObject';

import { SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE, SolanaError } from '@solana/errors';

import { rawDecimalFixedPoint, toSignedDecimalFixedPoint, toUnsignedDecimalFixedPoint } from '../decimal';

describe('toUnsignedDecimalFixedPoint', () => {
it('converts a signed non-negative value to unsigned', () => {
const signed = rawDecimalFixedPoint('signed', 8, 2)(100n);
const unsigned = toUnsignedDecimalFixedPoint(signed);
expect(unsigned.signedness).toBe('unsigned');
expect(unsigned.raw).toBe(100n);
});

it('returns the same reference when the input is already unsigned', () => {
const unsigned = rawDecimalFixedPoint('unsigned', 8, 2)(100n);
expect(toUnsignedDecimalFixedPoint(unsigned)).toBe(unsigned);
});

it('preserves totalBits and decimals', () => {
const signed = rawDecimalFixedPoint('signed', 64, 6)(1n);
const unsigned = toUnsignedDecimalFixedPoint(signed);
expect(unsigned.totalBits).toBe(64);
expect(unsigned.decimals).toBe(6);
});

it('returns a frozen value', () => {
const signed = rawDecimalFixedPoint('signed', 8, 2)(100n);
expect(toUnsignedDecimalFixedPoint(signed)).toBeFrozenObject();
});

it('throws VALUE_OUT_OF_RANGE when converting a negative value', () => {
const signed = rawDecimalFixedPoint('signed', 8, 2)(-1n);
expect(() => toUnsignedDecimalFixedPoint(signed)).toThrow(
new SolanaError(SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE, {
kind: 'decimalFixedPoint',
max: 255n,
min: 0n,
raw: -1n,
signedness: 'unsigned',
totalBits: 8,
}),
);
});
});

describe('toSignedDecimalFixedPoint', () => {
it('converts an unsigned value that fits the signed range', () => {
const unsigned = rawDecimalFixedPoint('unsigned', 8, 2)(100n);
const signed = toSignedDecimalFixedPoint(unsigned);
expect(signed.signedness).toBe('signed');
expect(signed.raw).toBe(100n);
});

it('returns the same reference when the input is already signed', () => {
const signed = rawDecimalFixedPoint('signed', 8, 2)(-50n);
expect(toSignedDecimalFixedPoint(signed)).toBe(signed);
});

it('preserves totalBits and decimals', () => {
const unsigned = rawDecimalFixedPoint('unsigned', 64, 6)(1n);
const signed = toSignedDecimalFixedPoint(unsigned);
expect(signed.totalBits).toBe(64);
expect(signed.decimals).toBe(6);
});

it('returns a frozen value', () => {
const unsigned = rawDecimalFixedPoint('unsigned', 8, 2)(100n);
expect(toSignedDecimalFixedPoint(unsigned)).toBeFrozenObject();
});

it('throws VALUE_OUT_OF_RANGE when the unsigned value exceeds the signed upper bound', () => {
const unsigned = rawDecimalFixedPoint('unsigned', 8, 2)(200n);
expect(() => toSignedDecimalFixedPoint(unsigned)).toThrow(
new SolanaError(SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE, {
kind: 'decimalFixedPoint',
max: 127n,
min: -128n,
raw: 200n,
signedness: 'signed',
totalBits: 8,
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type BinaryFixedPoint, toSignedBinaryFixedPoint, toUnsignedBinaryFixedPoint } from '../binary';

// [DESCRIBE] toUnsignedBinaryFixedPoint.
{
// It returns an unsigned value regardless of the input's signedness.
{
const fromSigned = {} as BinaryFixedPoint<'signed', 16, 15>;
toUnsignedBinaryFixedPoint(fromSigned) satisfies BinaryFixedPoint<'unsigned', 16, 15>;
const fromUnsigned = {} as BinaryFixedPoint<'unsigned', 16, 15>;
toUnsignedBinaryFixedPoint(fromUnsigned) satisfies BinaryFixedPoint<'unsigned', 16, 15>;
}

// It preserves totalBits and fractionalBits in the return type.
{
const value = {} as BinaryFixedPoint<'signed', 8, 4>;
toUnsignedBinaryFixedPoint(value) satisfies BinaryFixedPoint<'unsigned', 8, 4>;
}
}

// [DESCRIBE] toSignedBinaryFixedPoint.
{
// It returns a signed value regardless of the input's signedness.
{
const fromSigned = {} as BinaryFixedPoint<'signed', 16, 15>;
toSignedBinaryFixedPoint(fromSigned) satisfies BinaryFixedPoint<'signed', 16, 15>;
const fromUnsigned = {} as BinaryFixedPoint<'unsigned', 16, 15>;
toSignedBinaryFixedPoint(fromUnsigned) satisfies BinaryFixedPoint<'signed', 16, 15>;
}

// It preserves totalBits and fractionalBits in the return type.
{
const value = {} as BinaryFixedPoint<'unsigned', 8, 4>;
toSignedBinaryFixedPoint(value) satisfies BinaryFixedPoint<'signed', 8, 4>;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type DecimalFixedPoint, toSignedDecimalFixedPoint, toUnsignedDecimalFixedPoint } from '../decimal';

// [DESCRIBE] toUnsignedDecimalFixedPoint.
{
// It returns an unsigned value regardless of the input's signedness.
{
const fromSigned = {} as DecimalFixedPoint<'signed', 64, 2>;
toUnsignedDecimalFixedPoint(fromSigned) satisfies DecimalFixedPoint<'unsigned', 64, 2>;
const fromUnsigned = {} as DecimalFixedPoint<'unsigned', 64, 2>;
toUnsignedDecimalFixedPoint(fromUnsigned) satisfies DecimalFixedPoint<'unsigned', 64, 2>;
}

// It preserves totalBits and decimals in the return type.
{
const value = {} as DecimalFixedPoint<'signed', 8, 2>;
toUnsignedDecimalFixedPoint(value) satisfies DecimalFixedPoint<'unsigned', 8, 2>;
}
}

// [DESCRIBE] toSignedDecimalFixedPoint.
{
// It returns a signed value regardless of the input's signedness.
{
const fromSigned = {} as DecimalFixedPoint<'signed', 64, 2>;
toSignedDecimalFixedPoint(fromSigned) satisfies DecimalFixedPoint<'signed', 64, 2>;
const fromUnsigned = {} as DecimalFixedPoint<'unsigned', 64, 2>;
toSignedDecimalFixedPoint(fromUnsigned) satisfies DecimalFixedPoint<'signed', 64, 2>;
}

// It preserves totalBits and decimals in the return type.
{
const value = {} as DecimalFixedPoint<'unsigned', 8, 2>;
toSignedDecimalFixedPoint(value) satisfies DecimalFixedPoint<'signed', 8, 2>;
}
}
63 changes: 63 additions & 0 deletions packages/fixed-points/src/binary/conversions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { assertRawFitsInRange } from '../assertions';
import type { Signedness } from '../signedness';
import type { BinaryFixedPoint } from './core';

/**
* Converts a {@link BinaryFixedPoint} to its unsigned equivalent at the
* same `totalBits` and `fractionalBits`.
*
* Unsigned inputs are returned by reference unchanged; signed inputs are
* accepted as long as their raw value is non-negative.
*
* Throws `SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE` when the input
* represents a negative value that cannot be stored as unsigned.
*
* @example
* ```ts
* const signedUsd = binaryFixedPoint('signed', 16, 8);
* toUnsignedBinaryFixedPoint(signedUsd('1.5')); // unsigned, raw unchanged
* toUnsignedBinaryFixedPoint(signedUsd('-1')); // throws
* ```
*
* @see {@link toSignedBinaryFixedPoint}
*/
export function toUnsignedBinaryFixedPoint<TTotalBits extends number, TFractionalBits extends number>(
value: BinaryFixedPoint<Signedness, TTotalBits, TFractionalBits>,
): BinaryFixedPoint<'unsigned', TTotalBits, TFractionalBits> {
if (value.signedness === 'unsigned') {
return value as BinaryFixedPoint<'unsigned', TTotalBits, TFractionalBits>;
}
assertRawFitsInRange('binaryFixedPoint', 'unsigned', value.totalBits, value.raw);
return Object.freeze({ ...value, signedness: 'unsigned' });
}

/**
* Converts a {@link BinaryFixedPoint} to its signed equivalent at the same
* `totalBits` and `fractionalBits`.
*
* Signed inputs are returned by reference unchanged; unsigned inputs are
* accepted as long as their raw value fits the signed range, i.e.
* `raw <= 2 ** (totalBits - 1) - 1`.
*
* Throws `SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE` when the input's
* raw value exceeds the maximum representable signed value at its
* `totalBits`.
*
* @example
* ```ts
* const unsigned = rawBinaryFixedPoint('unsigned', 8, 0);
* toSignedBinaryFixedPoint(unsigned(100n)); // signed, raw === 100n
* toSignedBinaryFixedPoint(unsigned(200n)); // throws (200 > 127)
* ```
*
* @see {@link toUnsignedBinaryFixedPoint}
*/
export function toSignedBinaryFixedPoint<TTotalBits extends number, TFractionalBits extends number>(
value: BinaryFixedPoint<Signedness, TTotalBits, TFractionalBits>,
): BinaryFixedPoint<'signed', TTotalBits, TFractionalBits> {
if (value.signedness === 'signed') {
return value as BinaryFixedPoint<'signed', TTotalBits, TFractionalBits>;
}
assertRawFitsInRange('binaryFixedPoint', 'signed', value.totalBits, value.raw);
return Object.freeze({ ...value, signedness: 'signed' });
}
1 change: 1 addition & 0 deletions packages/fixed-points/src/binary/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './arithmetics';
export * from './comparisons';
export * from './conversions';
export * from './core';
export * from './guards';
63 changes: 63 additions & 0 deletions packages/fixed-points/src/decimal/conversions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { assertRawFitsInRange } from '../assertions';
import type { Signedness } from '../signedness';
import type { DecimalFixedPoint } from './core';

/**
* Converts a {@link DecimalFixedPoint} to its unsigned equivalent at the
* same `totalBits` and `decimals`.
*
* Unsigned inputs are returned by reference unchanged; signed inputs are
* accepted as long as their raw value is non-negative.
*
* Throws `SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE` when the input
* represents a negative value that cannot be stored as unsigned.
*
* @example
* ```ts
* const signedUsd = decimalFixedPoint('signed', 64, 2);
* toUnsignedDecimalFixedPoint(signedUsd('1.50')); // unsigned, raw unchanged
* toUnsignedDecimalFixedPoint(signedUsd('-1')); // throws
* ```
*
* @see {@link toSignedDecimalFixedPoint}
*/
export function toUnsignedDecimalFixedPoint<TTotalBits extends number, TDecimals extends number>(
value: DecimalFixedPoint<Signedness, TTotalBits, TDecimals>,
): DecimalFixedPoint<'unsigned', TTotalBits, TDecimals> {
if (value.signedness === 'unsigned') {
return value as DecimalFixedPoint<'unsigned', TTotalBits, TDecimals>;
}
assertRawFitsInRange('decimalFixedPoint', 'unsigned', value.totalBits, value.raw);
return Object.freeze({ ...value, signedness: 'unsigned' });
}

/**
* Converts a {@link DecimalFixedPoint} to its signed equivalent at the same
* `totalBits` and `decimals`.
*
* Signed inputs are returned by reference unchanged; unsigned inputs are
* accepted as long as their raw value fits the signed range, i.e.
* `raw <= 2 ** (totalBits - 1) - 1`.
*
* Throws `SOLANA_ERROR__FIXED_POINTS__VALUE_OUT_OF_RANGE` when the input's
* raw value exceeds the maximum representable signed value at its
* `totalBits`.
*
* @example
* ```ts
* const unsigned = rawDecimalFixedPoint('unsigned', 8, 0);
* toSignedDecimalFixedPoint(unsigned(100n)); // signed, raw === 100n
* toSignedDecimalFixedPoint(unsigned(200n)); // throws (200 > 127)
* ```
*
* @see {@link toUnsignedDecimalFixedPoint}
*/
export function toSignedDecimalFixedPoint<TTotalBits extends number, TDecimals extends number>(
value: DecimalFixedPoint<Signedness, TTotalBits, TDecimals>,
): DecimalFixedPoint<'signed', TTotalBits, TDecimals> {
if (value.signedness === 'signed') {
return value as DecimalFixedPoint<'signed', TTotalBits, TDecimals>;
}
assertRawFitsInRange('decimalFixedPoint', 'signed', value.totalBits, value.raw);
return Object.freeze({ ...value, signedness: 'signed' });
}
1 change: 1 addition & 0 deletions packages/fixed-points/src/decimal/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './arithmetics';
export * from './comparisons';
export * from './conversions';
export * from './core';
export * from './guards';
Loading