local help = [[
ezr-lib.lua: the batteries under ezr.lua. No learners here,
just the little functions that make the learners short.
Settings live in `the`, built by The() from this help;
other files extend it with the:also"help". For LuaJIT,
or any Lua from 5.1 up.
core options:
round=2 decimals printed by show
seed=1234567891 every random stream starts here
DATA=data/ bare table names live here (relative
DATA hangs off this script's own dir,
then ../ up: rock bins sit one level
under their rock's data)
file=auto93.csv default table]]
local max,min,floor = math.max, math.min, math.floorAll defs below land in this fresh table (which _G backs for
reads), so function name both defines and exports: the
last line returns _ENV as the module. The local works on
Lua 5.2+; the setfenv line is the same trick for 5.1 and
LuaJIT (where it is a plain local and setfenv is nil).
local _ENV = setmetatable({}, {__index = _G})
if setfenv then setfenv(1, _ENV) endFour verbs the rest of the system leans on: new builds an
object, iter makes lists and generators loop alike,
thing coerces one cell of text, csv streams a table off
disk. The signature above each is a reading aid, not a
check: list is a table used as an array, dict one
used as a map. Note that names after the wide gap in an
argument list are LOCALS, not arguments, so they never
show up in a type.
new(kl:dict, t:dict) -> dict
Class table is also its metatable.
function new(kl,t)
kl.__index=kl;kl.__tostring=show; return setmetatable(t,kl) enditer(src:list|fun) -> fun
Iterate a list or a function.
function iter(src, at)
if type(src) == "function" then return src end
at = 0; return function() at = at + 1; return src[at] end endthing(s:str) -> num|bool|str
String to number, bool, or string.
function thing(s)
s = s:match"^%s*(.-)%s*$"
return tonumber(s) or s=="True" or (s~="False" and s) endpathname(s?:str) -> str
Bare names live in the.DATA; a relative DATA hangs off
this script’s own dir, then ../ up. $VARS expand.
function pathname(s, d,t,f)
s = s or the.file
if not s:find"/" then
d = the.DATA
if d:find"^[/$]" then s = d .. s else
t = (arg and arg[0] or ""):gsub("[^/]*$","")
f = io.open(t .. d .. s)
if f then f:close() end
s = (f and t or t .. "../") .. d .. s end end
return (s:gsub("%$(%w+)", function(k)
return os.getenv(k) or k == "MOOT" and
os.getenv"HOME" .. "/gits/moot" end)) endcsv(file?:str) -> fun -> row?
Stream rows of coerced cells; nil ends the stream.
function csv(file, f)
f = io.lines(pathname(file))
return function( t,l)
for line in f do
l = line:gsub("\239\187\191","") -- strip any BOM
:gsub("%%.*",""):match"^%s*(.-)%s*$"
if l ~= "" then
t={} -- (.-), keeps empty cells
for s in (l..","):gmatch"(.-)," do t[#t+1]=thing(s) end
return t end end end endOne flat table of options, parsed out of the help string
that documents them, so the manual and the defaults cannot
drift apart. Every file adds its own paragraph with also.
THE = {}The(s:str) -> THE
Settings from the k=v words in a string; only the ones
after whitespace, so --k=v in the usage stays prose.
function The(s, i)
i = new(THE, {_help=s})
for k,v in (" "..s):gmatch"%s(%a%w*)=(%S+)" do
i[k] = thing(v) end
return i endTHE:also(t:str|dict) -> THE
Merge in new settings. Same-name fields crash.
function THE.also(i,t)
if type(t) == "string" thennewest file’s full text leads; older helps keep only their options paragraphs
i._help = t.."\n"..(i._help:match"[^\n]*[Oo]ptions:.*" or "")
t = The(t) end
for k,v in pairs(t) do
if k ~= "_help" then
assert(i[k] == nil, "duplicate setting: "..k)
i[k] = v end end
return i endSmall list verbs, all of them shorter than the loop they
replace. fun is why: it lets any of them take a function,
a method name, or a field index, so callers write
map(cols, "mid") instead of a closure.
push(t:list, v:any) -> any
Add v to the end of t, returning v so pushes chain.
function push(t,v) t[1+#t] = v; return v endfun(f:fun|str|int) -> fun
A callable: f itself; or a method name; or a field index.
function fun(f)
if type(f)=="string" then
return function(v,...) return v[f](v,...) end end
if type(f)=="number" then return function(v) return v[f]end end
return f endmap(t:list, f:fun|str|int) -> list
f (function, method name, or index) over a list.
function map(t,f, u)
f = fun(f)
u = {}; for _,v in ipairs(t) do u[1+#u]=f(v) end; return u endkap(t:dict, f:fun) -> list
f(k,v) over all pairs, any order. A nil result vanishes,
so kap also filters.
function kap(t,f, u)
u = {}
for k,v in pairs(t) do u[1+#u] = f(k,v) end; return u endslice(t:list, lo?:int, hi?:int) -> list
Copy t[lo..hi]; any size, any out-of-range bound.
function slice(t,lo,hi, u)
u, hi = {}, min(hi or #t, #t)
for j = max(lo or 1, 1), hi do u[1+#u] = t[j] end
return u endcopy(t:list) -> list
Shallow copy of the list part.
function copy(t)
return map(t, function(v) return v end) endsum(t:list|dict, f:fun) -> num
Add f(v) over the values.
function sum(t,f, n)
n = 0; for _, v in pairs(t) do n = n + f(v) end; return n endSorting, done stably: ties keep their input order, so two runs on one seed print the same lines and the frozen transcripts diff clean across Lua versions.
sorted(t:list, f?:fun) -> list
Sorted copy; the comparator is optional.
function sorted(t,f, s)
s = copy(t); table.sort(s, f); return s endkeysort(t:list, f:fun) -> list
Sort by f(v). Stable, so ties keep their input order.
function keysort(t,f, px,ix)
px, ix = {}, {}
for at, v in ipairs(t) do px[v], ix[v] = f(v), at end
return sorted(t, function(u,v)
if px[u] == px[v] then return ix[u] < ix[v] end
return px[u] < px[v] end) endkeys(t:dict, skip?:str) -> list
Sorted keys, dropping any that start with skip.
function keys(t,skip, u)
u = kap(t, function(k)
if not (skip and tostring(k):sub(1,1) == skip) then
return k end end)
return keysort(u, tostring) endleast() -> fun
Min-so-far reducer: call f{val,..} to offer a candidate,
f() to read the champion. The winner rides in the closure,
so no list of candidates is ever built.
function least( lo)
return function(x)
if x and (lo == nil or x[1] < lo[1]) then lo = x end
return lo end endOwn Park-Miller PRNG: exact doubles, so the same seed
yields the same stream on any Lua, any machine. Lua’s own
math.random is a different generator on 5.1, on 5.4, and
on LuaJIT, so a shared seed there would still fork the
stream and rot every frozen transcript.
Seed:int – state of the one generator in this system.
Seed = 1234567891srand(n?:int)
Reseed with any integer; lands in 1..2^31-2.
function srand(n)
Seed = floor(n or 1234567891) % 2147483647
if Seed <= 0 then Seed = Seed + 2147483646 end endrand(lo?:num, hi?:num) -> num
No args: a float in [0,1). One arg n: an int in 1..n.
Two args: an int in lo..hi.
function rand(lo,hi, x)
Seed = (16807 * Seed) % 2147483647
x = Seed / 2147483647
if not lo then return x end
if not hi then lo, hi = 1, lo end
return lo + floor(x * (hi - lo + 1)) endshuffle(lst:list) -> list
Fisher-Yates; copies first, so the input survives.
function shuffle(lst, t,j)
t = copy(lst)
for at = #t, 2, -1 do
j = rand(at); t[at],t[j] = t[j],t[at] end
return t endsome(lst:list, k:int) -> list
k items at random – or all of them, if k is too big.
function some(lst,k, t)
t = shuffle(lst)
for at = #t, min(k, #t) + 1, -1 do t[at] = nil end
return t endOne number format and one table format for the whole system. Both exist to keep printed output identical across Lua versions, which is what makes a transcript a test.
round(v:num, n?:int) -> num
Round to n (default the.round) places, then re-floor a
whole result: 5.3+ prints the float 15.0 as “15.0”, while
5.1 and LuaJIT print it as “15”.
function round(v,n)
if v % 1 == 0 then return floor(v) end
n = 10 ^ (n or the.round)
v = floor(v * n + 0.5) / n
return v % 1 == 0 and floor(v) or v endshow(x:any) -> str
Render anything. Tables recurse: lists print keyless,
dictionaries as “:k v” pairs, sorted.
function show(t, u)
if type(t) ~= "table" then
return tostring(type(t) == "number" and round(t) or t) end
u = #t > 0 and map(t, show) or
sorted(kap(t, function(k,v)
if tostring(k):sub(1,1) ~= "_" then
return ":"..k.." "..show(v) end end))
return "{"..table.concat(u, " ").."}" endCommand line in, demos out. go is the only entry point a
file needs; it stays silent unless that file is the script
the user actually ran, so require never fires a demo.
cli(d:THE) -> THE
--key=val flags update settings; -h prints the help.
A new value must keep the old value’s type.
function cli(d, v)
for _, s in ipairs(arg) do
if s == "-h" then print(d._help) end
for k in pairs(d) do
v = s:match("^%-%-" .. k .. "=(.*)")
if v then
v = thing(v)
assert(type(v) == type(d[k]),
"bad "..s.." : want "..type(d[k]))
d[k] = v end end end
return d endrun(funs:dict, w:str) -> bool?
One seeded example. Returns nil if there is no such demo,
else whether it ran without crashing.
function run(funs,w, ok,msg)
srand(the.seed)
if funs[w] then
ok, msg = xpcall(funs[w], debug.traceback)
if not ok then print(msg) end
return ok end endrepl(env:dict)
A tiny read-eval-print loop, run via the --repl demo. It
load()s each line inside env (a module _ENV), so bare
names like Tbl and csv resolve.
function repl(env, s,f,ok,r)
while true do
io.write("ezr> ")
s = io.read()
if not s then io.write"\n"; return end
f = load("return "..s, "=repl", "t", env) or
load(s, "=repl", "t", env)
if not f then print("! syntax") else
ok, r = pcall(f)
if not ok then print("! "..tostring(r))
elseif r ~= nil then print(show(r)) end end end endgo(eg:dict)
Parse the flags, run the demos named on the command line,
exit with the failure count. A no-op unless the caller is
the main script.
function go(eg, n)
if arg and arg[0] and
debug.getinfo(2,"S").source == "@"..arg[0] then
cli(the)
n = 0
for _,w in ipairs(arg) do
if run(eg, w) == false then n = n + 1 end end
os.exit(n) end end
the = The(help)
srand(the.seed) -- default stream; runners may reseed later
return _ENV