From e2f76c5cf58fd82ce6fe8c0df5cc0248a87be1ac Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 09:00:53 +0900 Subject: [PATCH 01/10] test: move tests under src/ --- .npmignore | 3 +++ {test => src}/nodejs/load-json.test.ts | 4 ++-- {test => src}/universal/deduplicate.test.ts | 2 +- {test => src}/universal/get-last-element-of.test.ts | 2 +- {test => src}/universal/sleep.test.ts | 2 +- {test => src}/universal/sort-objects.test.ts | 2 +- .../deindent.test.ts => src/universal/trim-lines.test.ts | 2 +- 7 files changed, 10 insertions(+), 7 deletions(-) rename {test => src}/nodejs/load-json.test.ts (65%) rename {test => src}/universal/deduplicate.test.ts (91%) rename {test => src}/universal/get-last-element-of.test.ts (96%) rename {test => src}/universal/sleep.test.ts (84%) rename {test => src}/universal/sort-objects.test.ts (84%) rename test/universal/deindent.test.ts => src/universal/trim-lines.test.ts (84%) 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/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/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/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 84% rename from test/universal/deindent.test.ts rename to src/universal/trim-lines.test.ts index 10dc789..1a624a9 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 { deindent } from "../universal.ts"; test("deindent", () => { const before = ` From efd7fd08ca174a0c12b399447f7ad99bedc100ff Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 07:02:56 +0900 Subject: [PATCH 02/10] feat: rename `random()` to `pseudoRandomInt()` [BREAKING] --- src/universal.ts | 2 +- src/universal/{random.ts => pseudo-random-int.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/universal/{random.ts => pseudo-random-int.ts} (60%) diff --git a/src/universal.ts b/src/universal.ts index 8fe5516..dac5974 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -2,7 +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 { sleep } from "./universal/sleep.ts"; export { sortObjects } from "./universal/sort-objects.ts"; diff --git a/src/universal/random.ts b/src/universal/pseudo-random-int.ts similarity index 60% rename from src/universal/random.ts rename to src/universal/pseudo-random-int.ts index 33f607f..d506d84 100644 --- a/src/universal/random.ts +++ b/src/universal/pseudo-random-int.ts @@ -3,4 +3,4 @@ * @param max - max number of the generated random number * @returns random number (integer) */ -export const random = (max: number): number => Math.floor(Math.random() * max); +export const pseudoRandomInt = (max: number): number => Math.floor(Math.random() * max); From 8eebd5f0530538469a973c85064b331181051990 Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 08:14:02 +0900 Subject: [PATCH 03/10] fix(pseudoRandomInt): max returning number was `max -1` --- src/universal/pseudo-random-int.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/universal/pseudo-random-int.ts b/src/universal/pseudo-random-int.ts index d506d84..45debac 100644 --- a/src/universal/pseudo-random-int.ts +++ b/src/universal/pseudo-random-int.ts @@ -3,4 +3,4 @@ * @param max - max number of the generated random number * @returns random number (integer) */ -export const pseudoRandomInt = (max: number): number => Math.floor(Math.random() * max); +export const pseudoRandomInt = (max: number): number => Math.floor(Math.random() * (max + 1)); From ea6df376253b3d143550f1957b2deedeb0758474 Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 09:36:28 +0900 Subject: [PATCH 04/10] fix(pseudoRandomInt): validate argument --- src/universal/pseudo-random-int.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/universal/pseudo-random-int.ts b/src/universal/pseudo-random-int.ts index 45debac..8924266 100644 --- a/src/universal/pseudo-random-int.ts +++ b/src/universal/pseudo-random-int.ts @@ -3,4 +3,10 @@ * @param max - max number of the generated random number * @returns random number (integer) */ -export const pseudoRandomInt = (max: number): number => Math.floor(Math.random() * (max + 1)); +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)); +}; From 064422b5a7318b5681a1cea3a33aeab8d4a77f75 Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 07:36:21 +0900 Subject: [PATCH 05/10] test: add test for `pseudoRandomInt()` --- src/universal/pseudo-random-int.test.ts | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/universal/pseudo-random-int.test.ts 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); + } + }); +}); From 1aed87555389d1657887f39a5ec673f817ddd899 Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 07:02:23 +0900 Subject: [PATCH 06/10] doc: make jsdoc more precise for `pseudoRandomInt()` --- src/universal/pseudo-random-int.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/universal/pseudo-random-int.ts b/src/universal/pseudo-random-int.ts index 8924266..742eda7 100644 --- a/src/universal/pseudo-random-int.ts +++ b/src/universal/pseudo-random-int.ts @@ -1,7 +1,8 @@ /** - * Generates random integer number. + * 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 random number (integer) + * @returns pseudo-random non-negative integer number */ export const pseudoRandomInt = (max: number): number => { if (!Number.isFinite(max) || max < 0) { From 6f72b7f6c2e42dcfe31ff026993914a6c9b60c89 Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 07:30:28 +0900 Subject: [PATCH 07/10] feat: add `pseudoRandomString()` --- src/universal.ts | 1 + src/universal/pseudo-random-string-chars.ts | 9 ++++ src/universal/pseudo-random-string.test.ts | 53 +++++++++++++++++++++ src/universal/pseudo-random-string.ts | 19 ++++++++ 4 files changed, 82 insertions(+) create mode 100644 src/universal/pseudo-random-string-chars.ts create mode 100644 src/universal/pseudo-random-string.test.ts create mode 100644 src/universal/pseudo-random-string.ts diff --git a/src/universal.ts b/src/universal.ts index dac5974..73ef2f2 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -3,6 +3,7 @@ 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 { 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"; 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(""); +}; From b9c93f3d79c66b8a76d6b81c65f194eaddbf1953 Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 09:39:20 +0900 Subject: [PATCH 08/10] test: replace deprecated function name with the latest one --- src/universal/trim-lines.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/universal/trim-lines.test.ts b/src/universal/trim-lines.test.ts index 1a624a9..86f448f 100644 --- a/src/universal/trim-lines.test.ts +++ b/src/universal/trim-lines.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { deindent } from "../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); From 91e24937f5f519c57377aaffd857a5793ebd9278 Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 09:40:07 +0900 Subject: [PATCH 09/10] feat: remove deprecated old function name [BREAKING] --- src/universal.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/universal.ts b/src/universal.ts index 73ef2f2..3114f34 100644 --- a/src/universal.ts +++ b/src/universal.ts @@ -6,10 +6,3 @@ 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"; From 5e1c646e17cbd07bea1deaaec97fb2d630c4d197 Mon Sep 17 00:00:00 2001 From: Jumpei Ogawa Date: Thu, 18 Dec 2025 07:51:11 +0900 Subject: [PATCH 10/10] chore: v3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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",