Skip to content

Instantly share code, notes, and snippets.

@stevedonovan
Created February 15, 2012 09:36
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save stevedonovan/1834789 to your computer and use it in GitHub Desktop.
Save stevedonovan/1834789 to your computer and use it in GitHub Desktop.
Microlight - a really compact set of general Lua functions
-----------------
-- Microlight - a very compact Lua utilities module
--
-- Steve Donovan, 2012; License MIT
-- @module ml
local ml = {}
--- String utilties.
-- @section string
--- split a string into a list of strings separated by a delimiter.
-- @param s The input string
-- @param re A Lua string pattern; defaults to '%s+'
-- @param n optional maximum number of splits
-- @return a list
function ml.split(s,re,n)
local find,sub,append = string.find, string.sub, table.insert
local i1,ls = 1,{}
if not re then re = '%s+' end
if re == '' then return {s} end
while true do
local i2,i3 = find(s,re,i1)
if not i2 then
local last = sub(s,i1)
if last ~= '' then append(ls,last) end
if #ls == 1 and ls[1] == '' then
return {}
else
return ls
end
end
append(ls,sub(s,i1,i2-1))
if n and #ls == n then
ls[#ls] = sub(s,i1)
return ls
end
i1 = i3+1
end
end
--- escape any 'magic' characters in a string
-- @param s The input string
-- @return an escaped string
function ml.escape(s)
return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
end
--- expand a string containing any ${var} or $var.
-- @param s the string
-- @param subst either a table or a function (as in `string.gsub`)
-- @return expanded string
function ml.expand (s,subst)
local res = s:gsub('%${([%w_]+)}',subst)
return (res:gsub('%$([%w_]+)',subst))
end
--- return the contents of a file as a string
-- @param filename The file path
-- @param is_bin open in binary mode, default false
-- @return file contents
function ml.readfile(filename,is_bin)
local mode = is_bin and 'b' or ''
local f,err = io.open(filename,'r'..mode)
if not f then return nil,err end
local res,err = f:read('*a')
f:close()
if not res then return nil,err end
return res
end
--- File and Path functions
-- @section file
--~ exists(filename)
--- Does a file exist?
-- @param filename a file path
-- @return the file path, otherwise nil
-- @usage exists 'readme' or exists 'readme.txt' or exists 'readme.md'
function ml.exists (filename)
local f = io.open(filename)
if not f then
return nil
else
f:close()
return filename
end
end
local sep, other_sep = package.config:sub(1,1),'/'
--- split a file path.
-- if there's no directory part, the first value will be the empty string
-- @param P A file path
-- @return the directory part
-- @return the file part
function ml.splitpath(P)
local i = #P
local ch = P:sub(i,i)
while i > 0 and ch ~= sep and ch ~= other_sep do
i = i - 1
ch = P:sub(i,i)
end
if i == 0 then
return '',P
else
return P:sub(1,i-1), P:sub(i+1)
end
end
--- given a path, return the root part and the extension part.
-- if there's no extension part, the second value will be empty
-- @param P A file path
-- @return the name part
-- @return the extension
function ml.splitext(P)
local i = #P
local ch = P:sub(i,i)
while i > 0 and ch ~= '.' do
if ch == sep or ch == other_sep then
return P,''
end
i = i - 1
ch = P:sub(i,i)
end
if i == 0 then
return P,''
else
return P:sub(1,i-1),P:sub(i)
end
end
--- Extended table functions.
-- 'list' here is shorthand for 'list-like table'; these functions
-- only operate over the numeric `1..#t` range of a table and are
-- particularly efficient for this purpose.
-- @section table
local function quote (v)
if type(v) == 'string' then
return ('%q'):format(v)
else
return tostring(v)
end
end
local tbuff
function tbuff (t,buff,k)
buff[k] = "{"
k = k + 1
for key,value in pairs(t) do
key = quote(key)
if type(value) ~= 'table' then
value = quote(value)
buff[k] = ('[%s]=%s'):format(key,value)
k = k + 1
if buff.limit and k > buff.limit then
buff[k] = "..."
error("buffer overrun")
end
else
if not buff.tables then buff.tables = {} end
if not buff.tables[value] then
k = tbuff(value,buff,k)
buff.tables[value] = true
else
buff[k] = "<cycle>"
k = k + 1
end
end
buff[k] = ","
k = k + 1
end
if buff[k-1] == "," then k = k - 1 end
buff[k] = "}"
k = k + 1
return k
end
--- return a string representation of a Lua table.
-- Cycles are detected, and a limit on number of items can be imposed.
-- @param t the table
-- @param limit the limit on items, default 1000
-- @return a string
function ml.tstring (t,limit)
local buff = {limit = limit or 1000}
pcall(tbuff,t,buff,1)
return table.concat(buff)
end
--- dump a Lua table to a file object.
-- @param t the table
-- @param f the file object (anything supporting f.write)
function ml.tdump(t,...)
local f = select('#',...) > 0 and select(1,...) or io.stdout
f:write(ml.tstring(t),'\n')
end
--- map a function over a list.
-- The output must always be the same length as the input, so
-- any `nil` values are mapped to `false`.
-- @param f a function of one or more arguments
-- @param t the table
-- @param ... any extra arguments to the function
-- @return a list with elements `f(t[i])`
function ml.imap(f,t,...)
f = ml.function_arg(f)
local res = {}
for i = 1,#t do
local val = f(t[i],...)
if val == nil then val = false end
res[i] = val
end
return res
end
--- filter a list using a predicate.
-- @param t a table
-- @param pred the predicate function
-- @param ... any extra arguments to the predicate
-- @return a list such that `pred(t[i])` is true
function ml.ifilter(t,pred,...)
local res,k = {},1
pred = ml.function_arg(pred)
for i = 1,#t do
if pred(t[i],...) then
res[k] = t[i]
k = k + 1
end
end
return res
end
--- find an item in a list using a predicate.
-- @param t the list
-- @param pred a function of at least one argument
-- @param ... any extra arguments
-- @return the item value
function ml.ifind(t,pred,...)
pred = ml.function_arg(pred)
for i = 1,#t do
if pred(t[i],...) then
return t[i]
end
end
end
--- return the index of an item in a list.
-- @param t the list
-- @param value item value
-- @return index, otherwise `nil`
function ml.index (t,value)
for i = 1,#t do
if t[i] == value then return i end
end
end
--- return a slice of a list.
-- Like string.sub, the end index may be negative.
-- @param t the list
-- @param i1 the start index
-- @param i2 the end index, default #t
function ml.sub(t,i1,i2)
if not i2 or i2 > #t then
i2 = #t
elseif i2 < 0 then
i2 = #t + i2 + 1
end
local res,k = {},1
for i = i1,i2 do
res[k] = t[i]
k = k + 1
end
return res
end
--- map a function over a Lua table.
-- @param f a function of one or more arguments
-- @param t the table
-- @param ... any optional arguments to the function
function ml.tmap(f,t,...)
f = ml.function_arg(f)
local res = {}
for k,v in pairs(t) do
res[k] = f(v,...)
end
return res
end
--- filter a table using a predicate.
-- @param t a table
-- @param pred the predicate function
-- @param ... any extra arguments to the predicate
-- @usage tfilter({a=1,b='boo'},tonumber) == {a=1}
function ml.tfilter (t,pred,...)
local res = {}
pred = ml.function_arg(pred)
for k,v in pairs(t) do
if pred(v,...) then
res[k] = v
end
end
return res
end
--- add the key/value pairs of `other` to `t`.
-- For sets, this is their union. For the same keys,
-- the values from the first table will be overwritten
-- @param t table to be updated
-- @param other table
-- @return the updated table
function ml.update(t,other)
for k,v in pairs(other) do
t[k] = v
end
return t
end
--- extend a list using values from another.
-- @param t the list to be extended
-- @param other a list
-- @return the extended list
function ml.extend(t,other)
local n = #t
for i = 1,#other do
t[n+i] = other[i]
end
return t
end
--- make a set from a list.
-- @param t a list of values
-- @return a table where the keys are the values
-- @usage set{'one','two'} == {one=true,two=true}
function ml.set(t)
local res = {}
for i = 1,#t do
res[t[i]] = true
end
return res
end
--- extract the keys of a table as a list.
-- This is the opposite operation to tset
-- @param t a table
-- @param a list of keys
function ml.keys(t)
local res,k = {},1
for key in pairs(t) do
res[k] = key
k = k + 1
end
return res
end
--- is `other` a subset of `t`?
-- @param t a set
-- @param other a possible subset
-- @return true or false
function ml.subset(t,other)
for k,v in pairs(other) do
if t[k] ~= v then return false end
end
return true
end
--- are these two tables equal?
-- This is shallow equality.
-- @param t a table
-- @param other a table
-- @return true or false
function ml.tequal(t,other)
return ml.subset(t,other) and ml.subset(other,t)
end
--- the intersection of two tables.
-- Works as expected for sets, otherwise note that the first
-- table's values are preseved
-- @param t a table
-- @param other a table
-- @return the intersection of the tables
function ml.intersect(t,other)
local res = {}
for k,v in pairs(t) do
if other[k] then
res[k] = v
end
end
return res
end
--- collect the values of an iterator into a list.
-- @param iter a single or double-valued iterator
-- @param count an optional number of values to collect
-- @return a list of values.
-- @usage collect(ipairs{10,20}) == {{1,10},{2,20}}
function ml.collect (iter, count)
local res,k = {},1
local v1,v2 = iter()
local dbl = v2 ~= nil
while v1 do
if dbl then v1 = {v1,v2} end
res[k] = v1
k = k + 1
if count and k > count then break end
v1,v2 = iter()
end
return res
end
--- Functional helpers.
-- @section function
--- create a function which will throw an error on failure.
-- @param f a function that returns nil,err if it fails
-- @return an equivalent function that raises an error
function ml.throw(f)
f = ml.function_arg(f)
return function(...)
local res,err = f(...)
if err then error(err) end
return res
end
end
--- create a function which will never throw an error.
-- This is the opposite situation to throw; if the
-- original function throws an error e, then this
-- function will return nil,e.
-- @param f a function which can throw an error
-- @return a function which returns nil,error when it fails
function ml.safe(f)
f = ml.function_arg(f)
return function(...)
local ok,r1,r2,r3 = pcall(f,...)
if ok then return r1,r2,r3
else
return nil,r1
end
end
end
--memoize(f)
--- bind the value `v` to the first argument of function `f`.
-- @param f a function of at least one argument
-- @param v a value
-- @return a function of one less argument
-- @usage (bind1(string.match,'hello')('^hell') == 'hell'
function ml.bind1(f,v)
f = ml.function_arg(f)
return function(...)
return f(v,...)
end
end
--- compose two functions.
-- For instance, `printf` can be defined as `compose(io.write,string.format)`
-- @param f1 a function
-- @param f2 a function
-- @return f1(f2(...))
function ml.compose(f1,f2)
f1 = ml.function_arg(f1)
f2 = ml.function_arg(f2)
return function(...)
return f1(f2(...))
end
end
--- is the object either a function or a callable object?.
-- @param obj Object to check.
-- @return true if callable
function ml.callable (obj)
return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call
end
function ml.function_arg(f)
assert(ml.callable(f),"expecting a function or callable object")
return f
end
--- Classes.
-- @section class
--- create a class with an optional base class.
-- The resulting table has a new() function for invoking
-- the constructor, which must be named `_init`. If the base
-- class has a constructor, you can call it as the `super()` method.
-- The `__tostring` metamethod is also inherited, but others need
-- to be brought in explicitly.
-- @param base optional base class
-- @return the metatable representing the class
function ml.class(base)
local klass, base_ctor = {}
klass.__index = klass
if base then
setmetatable(klass,base)
klass._base = base
base_ctor = rawget(base,'_init')
klass.__tostring = base.__tostring
end
function klass.new(...)
local self = setmetatable({},klass)
if rawget(klass,'_init') then
klass.super = base_ctor -- make super available for ctor
klass._init(self,...)
elseif base_ctor then -- call base ctor automatically
base_ctor(self,...)
end
return self
end
return klass
end
--- is an object derived from a class?
-- @param self the object
-- @param klass a class created with `class`
-- @return true or false
function ml.is_a(self,klass)
local m = getmetatable(self)
if not m then return false end --*can't be an object!
while m do
if m == klass then return true end
m = rawget(m,'_base')
end
return false
end
local _type = type
--- extended type of an object.
-- The type of a table is its metatable, otherwise works like standard type()
-- @param obj a value
-- @return the type, either a string or the metatable
function ml.type (obj)
if _type(obj) == 'table' then
return getmetatable(obj) or 'table'
else
return _type(obj)
end
end
return ml
local ml = require 'ml'
ml.update(_G,ml)
t = {one=1,two=2,10,20,{1,2}}
tdump(t)
--print(tequal({1,2,one=1},{1,2,one=2}))
printf = compose(io.write,string.format)
printf("the answer is %d\n",42)
local Table = {}
Table.__index = Table
function Table.new(t)
return setmetatable(t or {},Table)
end
local T = Table.new
update(Table,{
concat=table.concat,sort=table.sort,insert=table.insert,
ifilter=ml.ifilter,filter=ml.tfilter,index=ml.index,
tfind=ml.tfind,ifind=ml.ifind,extend=ml.extend,update=ml.update,
})
function Table:imap(f,...) return T(imap(f,self,...)) end
function Table:map(f,...) return T(tmap(f,self,...)) end
Table.sub = compose(T,sub)
--function Table:sub(i1,i2) return T(sub(self,i1,i2)) end
t = Table.new{10,20,30}
t:insert(1,5)
t = t:imap(function(x) return x*x end)
t:extend {3,2,1}
t = t:sub(2,-2)
print(t:concat ',')
Animal = class()
function Animal:_init (name)
self.name = name
end
function Animal:kind ()
return 'unknown!'
end
function Animal:__tostring ()
return "animal "..self.name
end
print(Animal.new "tiger")
Cat = class(Animal)
--[[
function Cat:_init (name)
--self._base._init(self,name)
self:super(name)
end
--]]
function Cat:kind ()
return 'cat'
end
--~ function Cat:__tostring ()
--~ return "meeoww "..self.name
--~ end
felix = Cat.new 'felix'
print(felix, felix:kind())
print(is_a(felix,Cat),is_a(felix,Animal))
@golgote
Copy link

golgote commented Feb 15, 2012

I'd like a trim function like in Penlight ;)

@stevedonovan
Copy link
Author

Ah, but where do we stop? ;)

(Personally I don't go anywhere without a trimi() either)

@golgote
Copy link

golgote commented Feb 15, 2012

Where do we stop is indeed the question.
There are functions I use all the time and that I have to rewrite/copy between every new projects. Trim and Split for example. But also htmlentities escape but that's more specialized so it shouldn't belong to MicroLight

I can imagine a small website where people could suggest functions (and maybe an implementation) and others would vote. If enough votes, the function would get included in MicroLight... :)

But still, trim is very helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment