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
3 changes: 3 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ tests/
*.config.js
*.config.mjs
*.config.ts
*.test.mjs
*.test.js
*.test.ts
*.tgz
.editorconfig
.gitattributes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@phanect/utils",
"version": "2.0.4",
"version": "3.0.0",
"description": "@phanect's personal utility library for JavaScript & TypeScript",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { join } from "node:path";
import { expect, test } from "vitest";
import { loadJSON } from "../../src/nodejs.ts";
import { loadJSON } from "../nodejs.ts";

test("loadJSON", async () => {
const obj = await loadJSON(
join(import.meta.dirname, "../fixtures/load-json/fixture.json"),
join(import.meta.dirname, "../../test/fixtures/load-json/fixture.json"),
);

expect(obj).toStrictEqual({
Expand Down
10 changes: 2 additions & 8 deletions src/universal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ export { deduplicate } from "./universal/deduplicate.ts";
export { trimLines } from "./universal/trim-lines.ts";
export { getLastElementOf } from "./universal/get-last-element-of.ts";
export { npmPackageExists } from "./universal/npm-package-exists.ts";
export { random } from "./universal/random.ts";
export { pseudoRandomInt } from "./universal/pseudo-random-int.ts";
export { pseudoRandomString } from "./universal/pseudo-random-string.ts";
export { sleep } from "./universal/sleep.ts";
export { sortObjects } from "./universal/sort-objects.ts";

//
// Backward compatibility
//

/** @deprecated `deindent()` is renamed to `trimLines()` */
export { trimLines as deindent } from "./universal/trim-lines.ts";
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { deduplicate } from "../../src/universal.ts";
import { deduplicate } from "../universal.ts";

test("deduplicate", () => {
const deduplicated = deduplicate([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { getLastElementOf } from "../../src/universal.ts";
import { getLastElementOf } from "../universal.ts";

test("getLastElementOf (without index)", () => {
const lastElement = getLastElementOf([ "a", "b", "c" ]);
Expand Down
56 changes: 56 additions & 0 deletions src/universal/pseudo-random-int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ok } from "node:assert";
import { describe, expect, test } from "vitest";
import { pseudoRandomInt } from "./pseudo-random-int.ts";

describe("pseudoRandomInt()", () => {
test("returns a number within valid range", () => {
const generatedNumber = pseudoRandomInt(100);

expect(generatedNumber).toBeLessThanOrEqual(100);
expect(generatedNumber).toBeGreaterThanOrEqual(0);
expect(typeof generatedNumber).toBe("number");
});

test("returns an integer (no decimals)", () => {
const generatedNumber = pseudoRandomInt(100);
ok(Number.isInteger(generatedNumber));
});

test("handles max value of 0 (returns 0)", () => {
const generatedNumber = pseudoRandomInt(0);
expect(generatedNumber).toBe(0);
});

test("returns 0 or 1 for max value of 1", () => {
const generatedNumber = pseudoRandomInt(1);
expect([ 0, 1 ]).toContain(generatedNumber);
Comment thread
phanect marked this conversation as resolved.
});

test("returns values in expected range for small max", () => {
const max = 10;
const results = new Set<number>();

// Generate multiple random numbers to test distribution
for (let i = 0; i < 100; i++) {
const generatedNumber = pseudoRandomInt(max);
results.add(generatedNumber);
expect(generatedNumber).toBeGreaterThanOrEqual(0);
expect(generatedNumber).toBeLessThanOrEqual(max);
expect(Number.isInteger(generatedNumber)).toBe(true);
}

// Should generate at least some variety in 100 attempts
expect(results.size).toBeGreaterThan(1);
});

test("returns values in expected range for large max", () => {
const max = 1000000;

for (let i = 0; i < 100; i++) {
const generatedNumber = pseudoRandomInt(max);
expect(generatedNumber).toBeGreaterThanOrEqual(0);
expect(generatedNumber).toBeLessThanOrEqual(max);
expect(Number.isInteger(generatedNumber)).toBe(true);
}
});
});
13 changes: 13 additions & 0 deletions src/universal/pseudo-random-int.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Generates pseudo-random non-negative integer number.
* Don't use this function for security purpose. (e.g., generating password)
* @param max - max number of the generated random number
Comment thread
phanect marked this conversation as resolved.
* @returns pseudo-random non-negative integer number
*/
export const pseudoRandomInt = (max: number): number => {
if (!Number.isFinite(max) || max < 0) {
throw new RangeError("max must be a finite, non-negative number");
Comment on lines +8 to +9

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation only checks if max is non-negative, but doesn't validate that it's an integer. The function is documented to return an "integer number" and the implementation uses Math.floor(), but accepting non-integer values for max could lead to unexpected behavior. Consider adding a check like !Number.isInteger(max) to ensure max is an integer, or document that non-integer values are acceptable.

Suggested change
if (!Number.isFinite(max) || max < 0) {
throw new RangeError("max must be a finite, non-negative number");
if (!Number.isFinite(max) || !Number.isInteger(max) || max < 0) {
throw new RangeError("max must be a finite, non-negative integer number");

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the validation logic. The function throws a RangeError when max is negative or not finite, but there are no tests verifying this behavior. Add tests for these edge cases, such as passing negative numbers, NaN, or Infinity.

Copilot uses AI. Check for mistakes.
}

return Math.floor(Math.random() * (max + 1));
};
9 changes: 9 additions & 0 deletions src/universal/pseudo-random-string-chars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const chars = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z",
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
"u", "v", "w", "x", "y", "z",
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
] as const;
53 changes: 53 additions & 0 deletions src/universal/pseudo-random-string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, test } from "vitest";
import { pseudoRandomInt } from "./pseudo-random-int.ts";
import { pseudoRandomString } from "./pseudo-random-string.ts";

describe("pseudoRandomString()", () => {
test("returns a string with the correct length", () => {
const generatedString = pseudoRandomString(10);

expect(generatedString).toHaveLength(10);
expect(typeof generatedString).toBe("string");
});

test("returns different strings on multiple calls", () => {
const generatedString1 = pseudoRandomString(20);
const generatedString2 = pseudoRandomString(20);
const generatedString3 = pseudoRandomString(20);

expect(generatedString1).not.toBe(generatedString2);
expect(generatedString2).not.toBe(generatedString3);
expect(generatedString1).not.toBe(generatedString3);
});

test("throws an Error when length is 0", () =>
expect(() => pseudoRandomString(0)).toThrowError()
);

test("throws an Error when length is a negative number", () =>
expect(() => pseudoRandomString(-4)).toThrowError()
);

test("generates string with only valid characters (alphanumeric)", () => {
const generatedString = pseudoRandomString(100);
expect(generatedString).toMatch(/^[A-Za-z0-9]+$/);
});

test("works with different length values", () => {
for (const length of Array.from({ length: 5 }, () => pseudoRandomInt(500) + 1)) {
const generatedString = pseudoRandomString(length);
expect(generatedString).toHaveLength(length);
expect(typeof generatedString).toBe("string");
}
});

test("generates strings with good distribution of characters", () => {
// Generate a long string and check that multiple different characters appear
const generatedString = pseudoRandomString(1000);
const uniqueChars = new Set(generatedString.split(""));

// With 1000 characters, we should see a good variety
// (at least 40 different characters out of 62 possible)
expect(uniqueChars.size).toBeGreaterThan(40);
});
});
19 changes: 19 additions & 0 deletions src/universal/pseudo-random-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { pseudoRandomInt } from "./pseudo-random-int.ts";
import { chars } from "./pseudo-random-string-chars.ts";

/**
* Generates pseudo-random string.
* Don't use this function for security purpose. (e.g., generating password)
* @param length - length of the generated string
* @returns pseudo-random string
*/
export const pseudoRandomString = (length: number): string => {
if (!Number.isInteger(length) || length <= 0) {
throw new Error(`\`length\` must be a natural number, but \`${ length }\` is given.`);
Comment thread
phanect marked this conversation as resolved.
Comment on lines +11 to +12

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for non-integer length values. The validation on line 11 checks !Number.isInteger(length), but there are no tests verifying that the function throws an error for non-integer values like 5.5 or NaN. Add test cases for these scenarios.

Copilot uses AI. Check for mistakes.
}

return Array.from({ length }, () => {
const index = pseudoRandomInt(chars.length - 1);
return chars[index];
}).join("");
};
6 changes: 0 additions & 6 deletions src/universal/random.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { sleep } from "../../src/universal.ts";
import { sleep } from "../universal.ts";

test("sleep", async () => {
const startAt = new Date();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { sortObjects } from "../../src/universal.ts";
import { sortObjects } from "../universal.ts";

test("sortObjects", () => {
const result = sortObjects([
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import { deindent } from "../../src/universal.ts";
import { trimLines } from "../universal.ts";

test("deindent", () => {
const before = `
Expand All @@ -11,7 +11,7 @@ test("deindent", () => {
"test": "JS object"
}`;

const actualAfter = deindent(before);
const actualAfter = trimLines(before);

expect(actualAfter).toStrictEqual(expectedAfter);
}, 10000);