diff --git a/.test/tests.lua b/.test/tests.lua index 6b96236..43407fb 100644 --- a/.test/tests.lua +++ b/.test/tests.lua @@ -5,6 +5,7 @@ package.path = package.path .. ";../?.lua" local assert = require("batteries.assert") local tablex = require("batteries.tablex") +local identifier = require("batteries.identifier") -- tablex {{{ @@ -155,3 +156,43 @@ local function test_spairs() 10, 8, 7 })) end + +local function test_uuid4() + for i = 1, 5 do + local id = identifier.uuid4() + + -- right len + assert(#id == 36) + -- right amount of non hyphen characters + assert(#id:gsub("-", "") == 32) + + -- 15th char is always a 4 + assert(id:sub(15, 15) == "4") + -- 20th char is always between 0x8 and 0xb + local y = tonumber("0x" .. id:sub(20, 20)) + assert(y >= 0x8 and y <= 0xb) + + -- everything is a valid 8 bit num + for char in id:gsub("-", ""):gmatch(".") do + local num = assert(tonumber("0x" .. char)) + assert(num >= 0 and num <= 0xf) + end + end +end + +local function test_ulid() + -- bail if there's no appropriate time func + if select(2, pcall(identifier.ulid)):find('time function') then return end + + for i = 1, 5 do + local ulid = assert(identifier.ulid()) + + -- right len + assert(#ulid == 26) + -- have the same timestamp with the same time + local a, b = identifier.ulid(nil, 1):sub(1, 10), identifier.ulid(nil, 1):sub(1, 10) + assert(a == b) + -- don't have characters out of crockford base32 + assert(not ulid:match("[ILOU%l]")) + end +end diff --git a/identifier.lua b/identifier.lua new file mode 100644 index 0000000..e7352e4 --- /dev/null +++ b/identifier.lua @@ -0,0 +1,76 @@ +--[[ + identifier generation + + uuid is version 4, ulid is an alternative to uuid (see + https://github.com/ulid/spec). + + todo: + this ulid isn't guaranteed to be sortable for ulids generated + within the same second yet +]] + +local path = (...):gsub("identifier", "") + +local identifier = {} + +--(internal; use a provided random generator object, or not) +local function _random(rng, ...) + if rng then return rng:random(...) end + if love then return love.math.random(...) end + return math.random(...) +end + +local uuid4_template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" + +--generate a UUID version 4 +function identifier.uuid4(rng) + --x should be 0x0-0xf, the single y should be 0x8-0xb + --4 should always just be 4 (denoting uuid version) + local out = uuid4_template:gsub("[xy]", function (c) + return string.format( + "%x", + c == "x" and _random(rng, 0x0, 0xf) or _random(rng, 0x8, 0xb) + ) + end) + + return out +end + +--crockford's base32 https://en.wikipedia.org/wiki/Base32 +local _encoding = { + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "M", + "N", "P", "Q", "R", "S", "T", "V", "W", "X", "Y", "Z" +} + +--since ulid needs time since unix epoch with miliseconds, we can just +--use socket. if that's not loaded, they'll have to provide their own +local function _now(time_func, ...) + if package.loaded.socket then return package.loaded.socket.gettime(...) end + if pcall(require, "socket") then return require("socket").gettime(...) end + if time_func then return time_func(...) end + error("assertion failed: socket can't be found and no time function provided") +end + +--generate an ULID using this rng at this time (now by default) +--implementation based on https://github.com/Tieske/ulid.lua +function identifier.ulid(rng, time) + time = math.floor((time or _now()) * 1000) + + local time_part = {} + local random_part = {} + + for i = 10, 1, -1 do + local mod = time % #_encoding + time_part[i] = _encoding[mod + 1] + time = (time - mod) / #_encoding + end + + for i = 1, 16 do + random_part[i] = _encoding[math.floor(_random(rng) * #_encoding) + 1] + end + + return table.concat(time_part) .. table.concat(random_part) +end + +return identifier