Created
February 15, 2012 09:36
-
-
Save stevedonovan/1834789 to your computer and use it in GitHub Desktop.
Microlight - a really compact set of general Lua functions
This file contains 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
----------------- | |
-- 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 |
This file contains 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 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)) | |
Ah, but where do we stop? ;)
(Personally I don't go anywhere without a trimi() either)
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
I'd like a trim function like in Penlight ;)