----------------- -- 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] = "" 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