diff --git a/.npmignore b/.npmignore index 4611446..d56ab20 100644 --- a/.npmignore +++ b/.npmignore @@ -7,6 +7,9 @@ tests/ *.config.js *.config.mjs *.config.ts +*.test.mjs +*.test.js +*.test.ts *.tgz .editorconfig .gitattributes diff --git a/package.json b/package.json index d2a12a5..07a39e5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/nodejs/load-json.test.ts b/src/nodejs/load-json.test.ts similarity index 65% rename from test/nodejs/load-json.test.ts rename to src/nodejs/load-json.test.ts index b3edce0..94e9aa4 100644 --- a/test/nodejs/load-json.test.ts +++ b/src/nodejs/load-json.test.ts @@ -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({ diff --git a/src/universal.ts b/src/universal.ts index 8fe5516..3114f34 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -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"; diff --git a/test/universal/deduplicate.test.ts b/src/universal/deduplicate.test.ts similarity index 91% rename from test/universal/deduplicate.test.ts rename to src/universal/deduplicate.test.ts index 9b534eb..1acad8c 100644 --- a/test/universal/deduplicate.test.ts +++ b/src/universal/deduplicate.test.ts @@ -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([ diff --git a/test/universal/get-last-element-of.test.ts b/src/universal/get-last-element-of.test.ts similarity index 96% rename from test/universal/get-last-element-of.test.ts rename to src/universal/get-last-element-of.test.ts index aa56df7..be35f0d 100644 --- a/test/universal/get-last-element-of.test.ts +++ b/src/universal/get-last-element-of.test.ts @@ -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" ]); diff --git a/src/universal/pseudo-random-int.test.ts b/src/universal/pseudo-random-int.test.ts new file mode 100644 index 0000000..f94d260 --- /dev/null +++ b/src/universal/pseudo-random-int.test.ts @@ -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); + }); + + test("returns values in expected range for small max", () => { + const max = 10; + const results = new Set(); + + // 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); + } + }); +}); diff --git a/src/universal/pseudo-random-int.ts b/src/universal/pseudo-random-int.ts new file mode 100644 index 0000000..742eda7 --- /dev/null +++ b/src/universal/pseudo-random-int.ts @@ -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 + * @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"); + } + + return Math.floor(Math.random() * (max + 1)); +}; diff --git a/src/universal/pseudo-random-string-chars.ts b/src/universal/pseudo-random-string-chars.ts new file mode 100644 index 0000000..7ede00a --- /dev/null +++ b/src/universal/pseudo-random-string-chars.ts @@ -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; diff --git a/src/universal/pseudo-random-string.test.ts b/src/universal/pseudo-random-string.test.ts new file mode 100644 index 0000000..100b692 --- /dev/null +++ b/src/universal/pseudo-random-string.test.ts @@ -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); + }); +}); diff --git a/src/universal/pseudo-random-string.ts b/src/universal/pseudo-random-string.ts new file mode 100644 index 0000000..6b802a5 --- /dev/null +++ b/src/universal/pseudo-random-string.ts @@ -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.`); + } + + return Array.from({ length }, () => { + const index = pseudoRandomInt(chars.length - 1); + return chars[index]; + }).join(""); +}; diff --git a/src/universal/random.ts b/src/universal/random.ts deleted file mode 100644 index 33f607f..0000000 --- a/src/universal/random.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Generates random integer number. - * @param max - max number of the generated random number - * @returns random number (integer) - */ -export const random = (max: number): number => Math.floor(Math.random() * max); diff --git a/test/universal/sleep.test.ts b/src/universal/sleep.test.ts similarity index 84% rename from test/universal/sleep.test.ts rename to src/universal/sleep.test.ts index de45f40..c6c7028 100644 --- a/test/universal/sleep.test.ts +++ b/src/universal/sleep.test.ts @@ -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(); diff --git a/test/universal/sort-objects.test.ts b/src/universal/sort-objects.test.ts similarity index 84% rename from test/universal/sort-objects.test.ts rename to src/universal/sort-objects.test.ts index ac46f05..d40f12c 100644 --- a/test/universal/sort-objects.test.ts +++ b/src/universal/sort-objects.test.ts @@ -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([ diff --git a/test/universal/deindent.test.ts b/src/universal/trim-lines.test.ts similarity index 72% rename from test/universal/deindent.test.ts rename to src/universal/trim-lines.test.ts index 10dc789..86f448f 100644 --- a/test/universal/deindent.test.ts +++ b/src/universal/trim-lines.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { deindent } from "../../src/universal.ts"; +import { trimLines } from "../universal.ts"; test("deindent", () => { const before = ` @@ -11,7 +11,7 @@ test("deindent", () => { "test": "JS object" }`; - const actualAfter = deindent(before); + const actualAfter = trimLines(before); expect(actualAfter).toStrictEqual(expectedAfter); }, 10000);