Skip to content
Open
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
4 changes: 4 additions & 0 deletions customRandom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export type CustomRandomGenerator = (size: number) => Uint8Array | Uint16Array | Uint32Array;

export const customRandom = (random: CustomRandomGenerator, alphabet: string, size: number) => {
if (size === 0) return () => "";
if (size < 0) throw Error("Size must be positive");
if (alphabet.length > 256) throw Error("Alphabet must contain 256 symbols or less")

const mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1;
const step = -~(1.6 * mask * size / alphabet.length);

Expand Down
83 changes: 83 additions & 0 deletions tests/customRandom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
assert,
assertEquals,
assertMatch,
assertThrows,
} from "https://deno.land/[email protected]/testing/asserts.ts";
import { customRandom } from "../customRandom.ts";
import { random } from "../random.ts";
import { urlAlphabet } from "../urlAlphabet.ts";

Deno.test("customRandom / generates URL safe IDs", () => {
const id = customRandom(random, urlAlphabet, 21)();
assert(typeof (id) === "string");

// https://www.ietf.org/rfc/rfc3986.html#section-2.3
assertMatch(id, /^[a-zA-Z0-9-._~]*$/);
});

Deno.test("customRandom / throws when alphabet length is greater than 255", () => {
const US_ALPHABET =
"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
const JAPANESE_ALPHABET =
"ーぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶ゛゜";
const alphabet = US_ALPHABET + JAPANESE_ALPHABET;
assert(alphabet.length > 255);
assertThrows(() => customRandom(random, alphabet, 21)(), Error);
});

Deno.test("customRandom / handles size of 0", () => {
const id = customRandom(random, urlAlphabet, 0)();
assertEquals(id.length, 0);
});

Deno.test("customRandom / throws when size is negative", () => {
assertThrows(
() => customRandom(random, urlAlphabet, -1)(),
Error,
);
});

Deno.test("customRandom / throws when size too big", () => {
assertThrows(
() => customRandom(random, urlAlphabet, 65537)(),
Error,
"The ArrayBufferView's byte length (103221) exceeds the number of bytes of entropy available via this API (65536)",
);
});

Deno.test("customRandom / has no collisions", () => {
const used = new Map();
for (let i = 0; i < 50 * 1000; i++) {
const id = customRandom(random, urlAlphabet, 21)();
assertEquals(used.has(id), false);
used.set(id, true);
}
});

Deno.test("customRandom / has flat distribution", () => {
const COUNT = 50 * 1000;
const LENGTH = customRandom(random, urlAlphabet, 21)().length;

const used = new Map<string, number>();
for (let i = 0; i < COUNT; i++) {
const id = customRandom(random, urlAlphabet, 21)();
for (const char of id) {
const timesUsed = used.get(char) ?? 0;
used.set(char, timesUsed + 1);
}
}

assertEquals(used.size, urlAlphabet.length);

let max = 0;
let min = Number.MAX_SAFE_INTEGER;
for (const k in used.keys()) {
const occurences = used.get(k) ?? 0;
const distribution = (occurences * urlAlphabet.length) / (COUNT * LENGTH);
if (distribution > max) max = distribution;
if (distribution < min) min = distribution;
}

assert((max - min) <= 0.05, "Max and min difference too big");
});
72 changes: 72 additions & 0 deletions tests/nanoid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
assert,
assertEquals,
assertMatch,
assertThrows,
} from "https://deno.land/[email protected]/testing/asserts.ts";
import { nanoid } from "../nanoid.ts";
import { urlAlphabet } from "../urlAlphabet.ts";

Deno.test("nanoid / generates URL safe IDs", () => {
const id = nanoid();
assert(typeof (id) === "string");
assertMatch(id, /^[a-zA-Z0-9_-]*$/);
});

Deno.test("nanoid / default length of 21", () => {
const id = nanoid();
assertEquals(id.length, 21);
});

Deno.test("nanoid / handles size of 0", () => {
const id = nanoid(0);
assertEquals(id.length, 0);
});

Deno.test("nanoid / throws when size is negative", () => {
assertThrows(() => nanoid(-1), Error, "Invalid typed array length");
});

Deno.test("nanoid / throws when size too big", () => {
assertThrows(
() => nanoid(65537),
Error,
"The ArrayBufferView's byte length (65537) exceeds the number of bytes of entropy available via this API (65536)",
);
});

Deno.test("nanoid / has no collisions", () => {
const used = new Map();
for (let i = 0; i < 50 * 1000; i++) {
const id = nanoid();
assertEquals(used.has(id), false);
used.set(id, true);
}
});

Deno.test("nanoid / has flat distribution", () => {
const COUNT = 50 * 1000;
const LENGTH = nanoid().length;

const used = new Map<string, number>();
for (let i = 0; i < COUNT; i++) {
const id = nanoid();
for (const char of id) {
const timesUsed = used.get(char) ?? 0;
used.set(char, timesUsed + 1);
}
}

assertEquals(used.size, urlAlphabet.length);

let max = 0;
let min = Number.MAX_SAFE_INTEGER;
for (const k in used.keys()) {
const occurences = used.get(k) ?? 0;
const distribution = (occurences * urlAlphabet.length) / (COUNT * LENGTH);
if (distribution > max) max = distribution;
if (distribution < min) min = distribution;
}

assert((max - min) <= 0.05, "Max and min difference too big");
});