Skip to content

Instantly share code, notes, and snippets.

@grilme99
Created August 24, 2025 15:41
Show Gist options
  • Select an option

  • Save grilme99/74882d4b9d88ac111b6fa8497a01472f to your computer and use it in GitHub Desktop.

Select an option

Save grilme99/74882d4b9d88ac111b6fa8497a01472f to your computer and use it in GitHub Desktop.
Non-secure nanoid implemented in Luau
-- This alphabet uses `A-Za-z0-9_-` symbols.
-- The order of characters is optimized for better gzip and brotli compression.
-- Same as in non-secure/index.js
local URL_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"
local DEFAULT_SIZE = 21
export type NanoIdFn = (size: number?) -> string
local function makeGenerator(alphabet: string, defaultSize: number?, rng: Random?): NanoIdFn
assert(type(alphabet) == "string" and #alphabet > 0, "alphabet must be a non-empty string")
local alphaLen = #alphabet
local sizeDefault = defaultSize or DEFAULT_SIZE
local r = rng or Random.new()
return function(size: number?): string
local n = math.floor(size or sizeDefault)
if n <= 0 then
return ""
end
-- build with a table to avoid repeated string concatenation
local out = table.create(n)
for i = 1, n do
local idx = r:NextInteger(1, alphaLen)
out[i] = string.sub(alphabet, idx, idx)
end
return table.concat(out)
end
end
local Nanoid = {}
function Nanoid.customAlphabet(alphabet: string, defaultSize: number?): NanoIdFn
return makeGenerator(alphabet, defaultSize, nil)
end
local defaultNanoid = makeGenerator(URL_ALPHABET, DEFAULT_SIZE, nil)
function Nanoid.nanoid(size: number?): string
return defaultNanoid(size)
end
-- Export the alphabet as well, like the JS package does
Nanoid.urlAlphabet = URL_ALPHABET
return Nanoid
local Packages = script.Parent.Parent
local RegExp = require(Packages.Dev.RegExp)
local LuauPolyfill = require(Packages.Dev.LuauPolyfill)
local Object = LuauPolyfill.Object
local JestGlobals = require(Packages.Dev.JestGlobals)
local describe = JestGlobals.describe
local it = JestGlobals.it
local expect = JestGlobals.expect
local NonSecure = require(script.Parent.NonSecure)
local nanoid = NonSecure.nanoid
local customAlphabet = NonSecure.customAlphabet
local urlAlphabet = NonSecure.urlAlphabet
describe("non secure", function()
it("generates URL-friendly IDs", function()
for _ = 1, 10 do
local id = nanoid()
expect(#id).toBe(21)
for _, char in string.split(id, "") do
expect(char).toMatch(RegExp(char))
end
end
end)
it("changes ID length", function()
expect(#nanoid(10)).toBe(10)
end)
it("has no collisions", function()
local used = {}
for _ = 1, 100 * 1000 do
local id = nanoid()
expect(used[id]).toBe(nil)
used[id] = true
end
end)
it("has flat distribution", function()
local COUNT = 100 * 1000
local LENGTH = #nanoid()
local chars = {}
for _ = 1, COUNT do
local id = nanoid()
for _, char in string.split(id, "") do
if not chars[char] then
chars[char] = 0
end
chars[char] += 1
end
end
expect(#Object.keys(chars)).toBe(#urlAlphabet)
local max = 0
local min = math.huge
for _, v in chars do
local distribution = (v * #urlAlphabet) / (COUNT * LENGTH)
if distribution > max then
max = distribution
end
if distribution < min then
min = distribution
end
end
expect(max - min).toBeLessThanOrEqual(0.05)
end)
it("nanoid / avoids pool pollution, infinite loop", function()
nanoid(2.1)
local second = nanoid()
local third = nanoid()
expect(second).never.toBe(third)
end)
it("customAlphabet / has options", function()
local nanoidA = customAlphabet("a", 5)
expect(nanoidA()).toBe("aaaaa")
end)
it("customAlphabet / has flat distribution", function()
local COUNT = 100 * 1000
local LENGTH = 5
local ALPHABET = "abcdefghijklmnopqrstuvwxyz"
local nanoid2 = customAlphabet(ALPHABET, LENGTH)
local chars = {}
for _ = 1, COUNT do
local id = nanoid2()
for _, char in string.split(id, "") do
if not chars[char] then
chars[char] = 0
end
chars[char] += 1
end
end
expect(#Object.keys(chars)).toBe(#ALPHABET)
local max = 0
local min = math.huge
for _, v in chars do
local distribution = (v * #ALPHABET) / (COUNT * LENGTH)
if distribution > max then
max = distribution
end
if distribution < min then
min = distribution
end
end
expect(max - min).toBeLessThanOrEqual(0.05)
end)
it("customAlphabet / avoids pool pollution, infinite loop", function()
local ALPHABET = "abcdefghijklmnopqrstuvwxyz"
local nanoid2 = customAlphabet(ALPHABET)
nanoid2(2.1)
local second = nanoid2()
local third = nanoid2()
expect(second).never.toBe(third)
end)
end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment