diff --git a/packages/bigint-constrained/README.md b/packages/bigint-constrained/README.md new file mode 100644 index 0000000..0f47eb7 --- /dev/null +++ b/packages/bigint-constrained/README.md @@ -0,0 +1,90 @@ +# @vekexasia/bigint-constrained: BigInt wrapper for boundaries checking + + + +This project is part of the [bigint-swissknife](https://github.com/vekexasia/bigint-swissknife) project. It aims to monkeypatch the Buffer native class adding +support for BigInts. This is useful when working with Node.js. + +## Why? + +Sometimes you need to work with a bounded BigInt. This library provides a simple wrapper around BigInt that allows you to specify a minimum and maximum value for the BigInt. + +For example, if you need to make sure the bigint you work with is at max 255 (aka uint8), you can use the following: + +```typescript +import { u8 } from '@vekexasia/bigint-constrained'; + +const a = u8(255n); +const b = u8(256n); // throws an error +const c = u8(-1n); // throws an error + +a.add(1n); // throws an error +``` + +Please notice: + + - Every operation performs boundaries checking and throws an error if the operation would result in a value outside the boundaries. + - Every operation is **idempotent** on the calling instance and returns a **new instance** of the bounded BigInt with the same boundaries. + + +## Documentation + +You can find typedoc documentation [here](https://vekexasia.github.io/bigint-swissknife/modules/_vekexasia_bigint_constrained.html). + +## Installation + +Add the library to your project: + +```bash +npm install @vekexasia/bigint-constrained +``` + +or + +```bash +yarn add @vekexasia/bigint-constrained +``` + +## Usage + +Simply import the library like shown above and then you can start using the methods to work with bounded BigInts. + +Right now the library exposes the following bounded BigInts: + +- `u8` (uint8) +- `u16` (uint16) +- `u32` (uint32) +- `u64` (uint64) +- `u128` (uint64) +- `u256` (uint64) +- `i8` (int8) +- ... +- `i256` (int256) + +If these are not sufficient you can always create your own like so: + +```typescript +import {CheckedBigInt} from '@vekexasia/bigint-constrained'; + +const u1024 = new CheckedBigInt(0n /*value*/, 1024 /*bits*/, false /*unsigned*/); +``` + +or custom bounds: + +```typescript +import {CheckedBigInt} from '@vekexasia/bigint-constrained'; + +const between10And20 = new CheckedBigInt(10n, {min: 10n, max: 20n}); + +``` + +## TypeScript + +The library is entirely written in TypeScript and comes with its own type definitions. + +## License + +This project is licensed under the MIT License - see the [LICENSE](../LICENSE) file for details. diff --git a/packages/bigint-constrained/src/CheckedBigInt.ts b/packages/bigint-constrained/src/CheckedBigInt.ts index bd5a6a2..3604ab1 100644 --- a/packages/bigint-constrained/src/CheckedBigInt.ts +++ b/packages/bigint-constrained/src/CheckedBigInt.ts @@ -4,51 +4,58 @@ export interface Bounds { min: bigint, max: bigint } * A class that represents a signed or unsigned integer of a specific bit size. */ export class CheckedBigInt { - readonly #bits: number public readonly value: bigint readonly boundaries: Bounds /** - * Creates a new CheckedBigInt. - * @param bits - bits size for this instance + * Creates a new CheckedBigInt. with custom boundaries * @param value - initial value - * @param signed - if true, the integer is signed, otherwise it is unsigned . If Bounds, it is used as boundaries. + * @param bounds - the set boundaries for the instance * @example * ```ts - * const a = new CheckedBigInt(8, 1n, false) // u8 - * const b = new CheckedBigInt(8, 1n, true) // i8 - * const c = new CheckedBigInt(8, 1n, { min: 0n, max: 2n }) // u8 with custom boundaries + * const a = new CheckedBigInt(1n, {min: 1n, max: 10n}) * ``` */ - constructor (bits: number, value: bigint, signed: boolean | Bounds) { - if (bits <= 0) { - throw new RangeError('CheckedBigInt: bits must be greater than 0') - } - this.#bits = bits - this.value = value - if (typeof (signed) === 'boolean') { + constructor(value:bigint, bounds: Bounds); + /** + * Creates a new CheckedBigInt. with a specific bit size and signedness + * @param value - initial value + * @param bits - the bit size of the integer + * @param signed - whether the integer is signed or not + * @example + * ```ts + * const a = new CheckedBigInt(1n /* value *\/, 8 /*bits*\/, false/*unsigned*\/) + * ``` + */ + constructor(value:bigint, bits: number, signed: boolean); + + constructor (value: bigint, info: number|Bounds, signed?: boolean ) { + if (typeof (info ) === 'number') { + const bits = info; + if (bits <=0) { + throw new RangeError('CheckedBigInt: bit size must be positive') + } if (signed) { - if (bits < 2) { - throw new RangeError('CheckedBigInt: signed bit size must be at least 2') - } this.boundaries = { - min: -(2n ** BigInt(this.#bits - 1)), - max: (2n ** BigInt(this.#bits - 1)) - 1n + min: -(2n ** BigInt(bits - 1)), + max: (2n ** BigInt(bits - 1)) - 1n } } else { this.boundaries = { min: 0n, - max: (2n ** BigInt(this.#bits)) - 1n + max: (2n ** BigInt(bits)) - 1n } } } else { - this.boundaries = signed + this.boundaries = info; } + this.value = value + if (this.value > this.boundaries.max) { - throw new RangeError(`CheckedBigInt: value ${this.value} exceeds bit size ${this.#bits} - max ${this.boundaries.max}`) + throw new RangeError(`CheckedBigInt: value ${this.value} exceeds boundaries - > max ${this.boundaries.max}`) } if (this.value < this.boundaries.min) { - throw new RangeError(`CheckedBigInt: value ${this.value} exceeds bit size ${this.#bits} - min ${this.boundaries.min}`) + throw new RangeError(`CheckedBigInt: value ${this.value} exceeds boundaries - < min ${this.boundaries.min}`) } } @@ -59,12 +66,12 @@ export class CheckedBigInt { * @throws RangeError If the result of the addition would exceed the bit size. * @example * ```ts - * const a = i8(1n).checkedAdd(1n) // 2 - * const b = i8(127n).checkedAdd(1n) // throws RangeError + * const a = i8(1n).add(1n) // 2 + * const b = i8(127n).add(1n) // throws RangeError * ``` */ - checkedAdd (other: bigint): CheckedBigInt { - return new CheckedBigInt(this.#bits, this.value + other, this.boundaries) + add (other: bigint): CheckedBigInt { + return new CheckedBigInt(this.value + other, this.boundaries) } /** @@ -73,8 +80,8 @@ export class CheckedBigInt { * @returns A new CheckedBigInt with the result of the subtraction. * @throws RangeError If the result of the subtraction would exceed the bit size. */ - checkedSub (other: bigint): CheckedBigInt { - return new CheckedBigInt(this.#bits, this.value - other, this.boundaries) + sub (other: bigint): CheckedBigInt { + return new CheckedBigInt(this.value - other, this.boundaries) } /** @@ -83,8 +90,8 @@ export class CheckedBigInt { * @returns A new CheckedBigInt with the result of the multiplication. * @throws RangeError If the result of the multiplication would exceed the bit size. */ - checkedMul (other: bigint): CheckedBigInt { - return new CheckedBigInt(this.#bits, this.value * other, this.boundaries) + mul (other: bigint): CheckedBigInt { + return new CheckedBigInt(this.value * other, this.boundaries) } /** @@ -94,14 +101,14 @@ export class CheckedBigInt { * @throws RangeError If the result of the division would exceed the bit size. * @throws RangeError If the divisor is zero. */ - checkedDiv (other: bigint): CheckedBigInt { + div (other: bigint): CheckedBigInt { if (other === 0n) { throw new RangeError('CheckedBigInt: division by zero') } if (this.boundaries.min !== 0n && this.value === this.boundaries.min && other === -1n) { throw new RangeError('CheckedBigInt: division overflow') } - return new CheckedBigInt(this.#bits, this.value / other, this.boundaries) + return new CheckedBigInt(this.value / other, this.boundaries) } /** @@ -111,14 +118,14 @@ export class CheckedBigInt { * @throws RangeError If the result of the remainder would exceed the bit size. * @throws RangeError If the divisor is zero. */ - checkedRem (other: bigint): CheckedBigInt { + rem (other: bigint): CheckedBigInt { if (other === 0n) { throw new RangeError('CheckedBigInt: division by zero') } if (this.boundaries.min !== 0n && this.value === this.boundaries.min && other === -1n) { throw new RangeError('CheckedBigInt: division overflow') } - return new CheckedBigInt(this.#bits, this.value % other, this.boundaries) + return new CheckedBigInt(this.value % other, this.boundaries) } /** @@ -127,8 +134,8 @@ export class CheckedBigInt { * @returns A new CheckedBigInt with the result of the power. * @throws RangeError If the result of the power would exceed the bit size. */ - checkedPow (exponent: bigint): CheckedBigInt { - return new CheckedBigInt(this.#bits, this.value ** exponent, this.boundaries) + pow (exponent: bigint): CheckedBigInt { + return new CheckedBigInt(this.value ** exponent, this.boundaries) } /** @@ -138,11 +145,11 @@ export class CheckedBigInt { * @throws RangeError If the number of bits is negative. * @throws RangeError If the resulting number would exceed the bit size. */ - checkedShl (bits: number | bigint): CheckedBigInt { + shl (bits: number | bigint): CheckedBigInt { if (bits < 0n) { throw new RangeError('CheckedBigInt: shift count must be non-negative') } - return new CheckedBigInt(this.#bits, this.value << BigInt(bits), this.boundaries) + return new CheckedBigInt(this.value << BigInt(bits), this.boundaries) } /** @@ -152,13 +159,11 @@ export class CheckedBigInt { * @throws RangeError If the number of bits is negative. * @throws RangeError If bits is greater than the bit size. */ - checkedShr (bits: number | bigint): CheckedBigInt { + shr (bits: number | bigint): CheckedBigInt { if (bits < 0n) { throw new RangeError('CheckedBigInt: shift count must be non-negative') } - if (bits > this.#bits) { - throw new RangeError('CheckedBigInt: shift count must be less than bit size') - } - return new CheckedBigInt(this.#bits, this.value >> BigInt(bits), this.boundaries) + + return new CheckedBigInt(this.value >> BigInt(bits), this.boundaries) } } diff --git a/packages/bigint-constrained/src/index.ts b/packages/bigint-constrained/src/index.ts index 89b9a7e..98f791c 100644 --- a/packages/bigint-constrained/src/index.ts +++ b/packages/bigint-constrained/src/index.ts @@ -8,7 +8,7 @@ export * from './CheckedBigInt.js' * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to u8 */ -export const u8 = (value: bigint): CheckedBigInt => new CheckedBigInt(8, value, false) +export const u8 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,8, false) /** * Creates an u16 CheckedBigInt. @@ -16,7 +16,7 @@ export const u8 = (value: bigint): CheckedBigInt => new CheckedBigInt(8, value, * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to u16 */ -export const u16 = (value: bigint): CheckedBigInt => new CheckedBigInt(16, value, false) +export const u16 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,16, false) /** * Creates an u32 CheckedBigInt. @@ -24,7 +24,7 @@ export const u16 = (value: bigint): CheckedBigInt => new CheckedBigInt(16, value * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to u32 */ -export const u32 = (value: bigint): CheckedBigInt => new CheckedBigInt(32, value, false) +export const u32 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,32, false) /** * Creates an u64 CheckedBigInt. @@ -32,7 +32,7 @@ export const u32 = (value: bigint): CheckedBigInt => new CheckedBigInt(32, value * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to u64 */ -export const u64 = (value: bigint): CheckedBigInt => new CheckedBigInt(64, value, false) +export const u64 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,64, false) /** * Creates an u128 CheckedBigInt. @@ -40,7 +40,7 @@ export const u64 = (value: bigint): CheckedBigInt => new CheckedBigInt(64, value * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to u128 */ -export const u128 = (value: bigint): CheckedBigInt => new CheckedBigInt(128, value, false) +export const u128 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,128, false) /** * Creates an u256 CheckedBigInt. @@ -48,7 +48,7 @@ export const u128 = (value: bigint): CheckedBigInt => new CheckedBigInt(128, val * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to u256 */ -export const u256 = (value: bigint): CheckedBigInt => new CheckedBigInt(256, value, false) +export const u256 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,256, false) /** * Creates an i8 CheckedBigInt. @@ -56,7 +56,7 @@ export const u256 = (value: bigint): CheckedBigInt => new CheckedBigInt(256, val * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to i8 */ -export const i8 = (value: bigint): CheckedBigInt => new CheckedBigInt(8, value, true) +export const i8 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,8, true) /** * Creates an i16 CheckedBigInt. @@ -64,7 +64,7 @@ export const i8 = (value: bigint): CheckedBigInt => new CheckedBigInt(8, value, * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to i16 */ -export const i16 = (value: bigint): CheckedBigInt => new CheckedBigInt(16, value, true) +export const i16 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,16, true) /** * Creates an i32 CheckedBigInt. @@ -72,7 +72,7 @@ export const i16 = (value: bigint): CheckedBigInt => new CheckedBigInt(16, value * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to i32 */ -export const i32 = (value: bigint): CheckedBigInt => new CheckedBigInt(32, value, true) +export const i32 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,32, true) /** * Creates an i64 CheckedBigInt. @@ -80,7 +80,7 @@ export const i32 = (value: bigint): CheckedBigInt => new CheckedBigInt(32, value * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to i64 */ -export const i64 = (value: bigint): CheckedBigInt => new CheckedBigInt(64, value, true) +export const i64 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,64, true) /** * Creates an i128 CheckedBigInt. @@ -88,7 +88,7 @@ export const i64 = (value: bigint): CheckedBigInt => new CheckedBigInt(64, value * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to i128 */ -export const i128 = (value: bigint): CheckedBigInt => new CheckedBigInt(128, value, true) +export const i128 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,128, true) /** * Creates an i256 CheckedBigInt. @@ -96,4 +96,4 @@ export const i128 = (value: bigint): CheckedBigInt => new CheckedBigInt(128, val * @throws RangeError - If the value exceeds the bit size. * @returns A new CheckedBigInt constrained to i256 */ -export const i256 = (value: bigint): CheckedBigInt => new CheckedBigInt(256, value, true) +export const i256 = (value: bigint): CheckedBigInt => new CheckedBigInt(value,256, true) diff --git a/packages/bigint-constrained/test/CheckedBigInt.test.ts b/packages/bigint-constrained/test/CheckedBigInt.test.ts index 0ce48c6..ae9e40a 100644 --- a/packages/bigint-constrained/test/CheckedBigInt.test.ts +++ b/packages/bigint-constrained/test/CheckedBigInt.test.ts @@ -17,114 +17,114 @@ describe('CheckedBigInt', () => { expect(u8(127n).boundaries).to.deep.eq({ min: 0n, max: 255n }) }) it('should throw on invalid bit size', () => { - expect(() => new CheckedBigInt(0, 0n, false)).to.throw(RangeError) - expect(() => new CheckedBigInt(-1, 0n, false)).to.throw(RangeError) + expect(() => new CheckedBigInt(0n, 0, false)).to.throw(RangeError) + expect(() => new CheckedBigInt(0n, -1, false)).to.throw(RangeError) }) it('should allow custom boundaries', () => { - const a = new CheckedBigInt(8, 0n, { min: 0n, max: 2n }) + const a = new CheckedBigInt(0n, { min: 0n, max: 2n }) expect(a.boundaries).to.deep.eq({ min: 0n, max: 2n }) }) }) - describe('checkedAdd', () => { + describe('add', () => { it('should should return added value', () => { - const a = i8(1n).checkedAdd(1n) + const a = i8(1n).add(1n) expect(a.value).to.eq(2n) }) it('should throw on overflow', () => { - expect(() => i8(127n).checkedAdd(1n)).to.throw(RangeError) - expect(() => i8(-128n).checkedAdd(-1n)).to.throw(RangeError) + expect(() => i8(127n).add(1n)).to.throw(RangeError) + expect(() => i8(-128n).add(-1n)).to.throw(RangeError) }) }) - describe('checkedSub', () => { + describe('sub', () => { it('should should return subtracted value', () => { - const a = i8(1n).checkedSub(1n) + const a = i8(1n).sub(1n) expect(a.value).to.eq(0n) }) it('should throw on overflow', () => { - expect(() => i8(-128n).checkedSub(1n)).to.throw(RangeError) - expect(() => i8(127n).checkedSub(-1n)).to.throw(RangeError) + expect(() => i8(-128n).sub(1n)).to.throw(RangeError) + expect(() => i8(127n).sub(-1n)).to.throw(RangeError) }) }) - describe('checkedRem', () => { + describe('rem', () => { it('should return remainder', () => { - const a = i8(5n).checkedRem(3n) + const a = i8(5n).rem(3n) expect(a.value).to.eq(2n) }) it('should throw on division by zero', () => { - expect(() => i8(5n).checkedRem(0n)).to.throw(RangeError) + expect(() => i8(5n).rem(0n)).to.throw(RangeError) }) it('should throw on overflow', () => { - expect(() => i8(-128n).checkedRem(-1n)).to.throw(RangeError) + expect(() => i8(-128n).rem(-1n)).to.throw(RangeError) }) }) - describe('checkedMul', () => { + describe('mul', () => { it('should return multiplication', () => { - const a = i8(5n).checkedMul(3n) + const a = i8(5n).mul(3n) expect(a.value).to.eq(15n) }) it('should throw on overflow', () => { - expect(() => i8(15n).checkedMul(8n)).to.not.throw(RangeError) - expect(() => i8(16n).checkedMul(8n)).to.throw(RangeError) + expect(() => i8(15n).mul(8n)).to.not.throw(RangeError) + expect(() => i8(16n).mul(8n)).to.throw(RangeError) }) it('should throw if multiplication turns to negative in UINT', () => { - expect(() => u8(1n).checkedMul(-1n)).to.throw(RangeError) + expect(() => u8(1n).mul(-1n)).to.throw(RangeError) }) }) - describe('checkedDiv', () => { + describe('div', () => { it('should return division', () => { - const a = i8(5n).checkedDiv(3n) + const a = i8(5n).div(3n) expect(a.value).to.eq(1n) }) it('should throw on division by zero', () => { - expect(() => i8(5n).checkedDiv(0n)).to.throw(RangeError) + expect(() => i8(5n).div(0n)).to.throw(RangeError) }) it('should throw on overflow', () => { - expect(() => i8(-128n).checkedDiv(-1n)).to.throw(RangeError) + expect(() => i8(-128n).div(-1n)).to.throw(RangeError) }) }) - describe('checkedPow', () => { + describe('pow', () => { it('should return power', () => { - const a = i8(2n).checkedPow(3n) + const a = i8(2n).pow(3n) expect(a.value).to.eq(8n) }) it('should throw on overflow', () => { - expect(() => i8(2n).checkedPow(7n)).to.throw(RangeError) + expect(() => i8(2n).pow(7n)).to.throw(RangeError) }) it('should throw on negative exponent', () => { - expect(() => i8(2n).checkedPow(-1n)).to.throw(RangeError) + expect(() => i8(2n).pow(-1n)).to.throw(RangeError) }) }) - describe('checkedShl', () => { + describe('shl', () => { it('should shift left', () => { - const a = i8(1n).checkedShl(1n) + const a = i8(1n).shl(1n) expect(a.value).to.eq(2n) }) it('should throw on negative shift', () => { - expect(() => i8(1n).checkedShl(-1n)).to.throw(RangeError) + expect(() => i8(1n).shl(-1n)).to.throw(RangeError) }) it('should throw on overflow', () => { - expect(() => i8(64n).checkedShl(1n)).to.throw(RangeError) + expect(() => i8(64n).shl(1n)).to.throw(RangeError) }) }) - describe('checkedShr', () => { + describe('shr', () => { it('should shift right', () => { - const a = i8(2n).checkedShr(1n) + const a = i8(2n).shr(1n) expect(a.value).to.eq(1n) }) it('should throw on negative shift', () => { - expect(() => i8(1n).checkedShr(-1n)).to.throw(RangeError) + expect(() => i8(1n).shr(-1n)).to.throw(RangeError) }) it('should throw on more bits', () => { - expect(() => i8(-128n).checkedShr(8n)).to.not.throw(RangeError) - expect(() => i8(-128n).checkedShr(9n)).to.throw(RangeError) + expect(() => i8(-128n).shr(8n)).to.not.throw(RangeError) + }) })