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
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"@commitlint/config-conventional": "^20.5.0",
"@commitlint/types": "^20.5.0",
"@types/node": "^24.12.2",
"@vitest/coverage-v8": "^4.1.3",
"@vitest/coverage-v8": "^4.1.4",
"@webdeveric/eslint-config-ts": "^0.12.0",
"@webdeveric/prettier-config": "^0.4.0",
"commitlint": "^20.5.0",
Expand All @@ -102,11 +102,11 @@
"husky": "^9.1.7",
"jsdom": "^29.0.2",
"lint-staged": "^16.4.0",
"prettier": "^3.8.1",
"prettier": "^3.8.3",
"rimraf": "^6.1.3",
"semantic-release": "^25.0.3",
"typescript": "^6.0.2",
"typescript": "^6.0.3",
"validate-package-exports": "^0.23.0",
"vitest": "^4.1.3"
"vitest": "^4.1.4"
}
}
616 changes: 312 additions & 304 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions src/assertion/assertIsSafeInteger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { isSafeInteger } from '../predicate/isSafeInteger.js';

import { getError } from './getError.js';

export function assertIsSafeInteger(
input: unknown,
error: string | Error = 'input is not a safe integer',
): asserts input is number {
if (!isSafeInteger(input)) {
throw getError(error);
}
}
1 change: 1 addition & 0 deletions src/assertion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export * from './assertIsPrimitive.js';
export * from './assertIsPromiseFulfilledResult.js';
export * from './assertIsPromiseRejectedResult.js';
export * from './assertIsPropertyKey.js';
export * from './assertIsSafeInteger.js';
export * from './assertIsSizeAware.js';
export * from './assertIsString.js';
export * from './assertIsStringArray.js';
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export * from './joinStrings.js';
export * from './jsonParse.js';
export * from './looksLikeURL.js';
export * from './normalize.js';
export * from './numbers.js';
export * from './parseNumber.js';
export * from './pathParts.js';
export * from './prefix.js';
Expand Down
7 changes: 7 additions & 0 deletions src/numbers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const int32Min = -0x80000000; // -2_147_483_648;

export const int32Max = 0x7fffffff; // 2_147_483_647;

export const uInt32Min = 0;

export const uInt32Max = 0xffffffff; // 4_294_967_295;
2 changes: 2 additions & 0 deletions src/predicate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ export * from './isPromiseFulfilledResult.js';
export * from './isPromiseLike.js';
export * from './isPromiseRejectedResult.js';
export * from './isPropertyKey.js';
export * from './isSafeInteger.js';
export * from './isSizeAware.js';
export * from './isString.js';
export * from './isStringArray.js';
export * from './isStringRecord.js';
export * from './isStringWithLength.js';
export * from './isSymbol.js';
export * from './isSymbolArray.js';
export * from './isUInt32.js';
export * from './isUndefined.js';
export * from './isUndefinedArray.js';
export * from './isUnknown.js';
Expand Down
5 changes: 2 additions & 3 deletions src/predicate/isInt32.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { int32Max, int32Min } from '../numbers.js';

import type { Int32 } from '../types/numbers.js';

export function isInt32(input: unknown): input is Int32 {
const int32Min = -2_147_483_648;
const int32Max = 2_147_483_647;

return typeof input === 'number' && Number.isInteger(input) && input >= int32Min && input <= int32Max;
}
1 change: 1 addition & 0 deletions src/predicate/isSafeInteger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isSafeInteger = (input: unknown): input is number => Number.isSafeInteger(input);
7 changes: 7 additions & 0 deletions src/predicate/isUInt32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { uInt32Max, uInt32Min } from '../numbers.js';

import type { UInt32 } from '../types/numbers.js';

export function isUInt32(input: unknown): input is UInt32 {
return typeof input === 'number' && Number.isInteger(input) && input >= uInt32Min && input <= uInt32Max;
}
30 changes: 18 additions & 12 deletions src/randomInt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertIsInteger } from './assertion/assertIsInteger.js';
import { assertIsSafeInteger } from './assertion/assertIsSafeInteger.js';
import { isObject } from './predicate/isObject.js';

import type { RequireAtLeastOne } from './types/records.js';
Expand All @@ -18,20 +18,26 @@ export function randomInt(options?: RandomIntOptions): number;
export function randomInt(min: number, max?: number): number;

export function randomInt(arg1?: number | RandomIntOptions, arg2?: number): number {
const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = isObject(arg1)
? arg1
: { min: arg1, max: arg2 };
const { min = 0, max = Number.MAX_SAFE_INTEGER } = isObject(arg1) ? arg1 : { min: arg1, max: arg2 };

assertIsInteger(min, 'min must be an integer');
assertIsInteger(max, 'max must be an integer');
assertIsSafeInteger(min, 'min must be a safe integer');
assertIsSafeInteger(max, 'max must be a safe integer');

if (min === max) {
return min;
}

if (min > max) {
if (min >= max) {
throw new RangeError('min must be less than max');
}

return Math.floor(Math.random() * (max - min)) + min;
const minBigInt = BigInt(min);
const range = BigInt(max) - minBigInt;
const limit = (2n ** 64n / range) * range;
const data = new BigInt64Array(1);

let value: bigint;

do {
crypto.getRandomValues(data);
value = BigInt.asUintN(64, data[0]!);
} while (value >= limit);

return Number(minBigInt + (value % range));
}
4 changes: 3 additions & 1 deletion src/sort/byRandom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
export const byRandom = (): number => {
let value: number;

const data = new Uint8Array(1);

do {
value = crypto.getRandomValues(new Uint8Array(1))[0]!;
value = crypto.getRandomValues(data)[0]!;
} while (value >= 252); // Avoid modulo bias

return (value % 3) - 1;
Expand Down
8 changes: 8 additions & 0 deletions src/types/numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ export type BinaryNumberString = `0${'b' | 'B'}${number}`;
*/
export type ExponentiationString = `${number}${'e' | 'E'}${number}`;

/**
* Signed int 32
*/
export type Int32 = Branded<number, 'Int32'>;

/**
* Unsigned int 32
*/
export type UInt32 = Branded<number, 'UInt32'>;

export type NumberRange =
| [min: number]
| [min: number, max: undefined]
Expand Down
20 changes: 14 additions & 6 deletions test/randomInt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import { describe, expect, it } from 'vitest';
import { randomInt } from '../src/randomInt.js';

describe('randomInt()', () => {
it('Returns early when min and max are the same', () => {
expect(randomInt(0, 0)).toEqual(0);
});

it('Returns a random integer', () => {
const value = randomInt(100, 200);

Expand All @@ -27,16 +23,28 @@ describe('randomInt()', () => {
expect(value).lessThan(1000);
});

it('Defaults to SAFE_INTEGER range', () => {
it('Defaults to [0, MAX_SAFE_INTEGER) range', () => {
const value = randomInt();

expect(value).greaterThanOrEqual(Number.MIN_SAFE_INTEGER);
expect(value).greaterThanOrEqual(0);
expect(value).lessThan(Number.MAX_SAFE_INTEGER);
});

it('Throws when min and max are equal', () => {
expect(() => {
randomInt(0, 0);
}).toThrow();
});

it('Throws when min > max', () => {
expect(() => {
randomInt(100, 0);
}).toThrow();
});

it('Throws when range is too large', () => {
expect(() => {
randomInt(Number.MIN_VALUE, Number.MAX_VALUE);
}).toThrow();
});
});
Loading