Created
August 24, 2025 15:41
-
-
Save grilme99/74882d4b9d88ac111b6fa8497a01472f to your computer and use it in GitHub Desktop.
Non-secure nanoid implemented in Luau
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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