· 7 years ago · Sep 11, 2018, 09:10 PM
1-----------------
2-- Microlight - a very compact Lua utilities module
3--
4-- Steve Donovan, 2012; License MIT
5-- @module ml
6
7local ml = {}
8
9--- String utilties.
10-- @section string
11
12--- split a string into a list of strings separated by a delimiter.
13-- @param s The input string
14-- @param re A Lua string pattern; defaults to '%s+'
15-- @param n optional maximum number of splits
16-- @return a list
17function ml.split(s,re,n)
18 local find,sub,append = string.find, string.sub, table.insert
19 local i1,ls = 1,{}
20 if not re then re = '%s+' end
21 if re == '' then return {s} end
22 while true do
23 local i2,i3 = find(s,re,i1)
24 if not i2 then
25 local last = sub(s,i1)
26 if last ~= '' then append(ls,last) end
27 if #ls == 1 and ls[1] == '' then
28 return {}
29 else
30 return ls
31 end
32 end
33 append(ls,sub(s,i1,i2-1))
34 if n and #ls == n then
35 ls[#ls] = sub(s,i1)
36 return ls
37 end
38 i1 = i3+1
39 end
40end
41
42--- escape any 'magic' characters in a string
43-- @param s The input string
44-- @return an escaped string
45function ml.escape(s)
46 return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
47end
48
49--- expand a string containing any ${var} or $var.
50-- @param s the string
51-- @param subst either a table or a function (as in `string.gsub`)
52-- @return expanded string
53function ml.expand (s,subst)
54 local res = s:gsub('%${([%w_]+)}',subst)
55 return (res:gsub('%$([%w_]+)',subst))
56end
57
58--- return the contents of a file as a string
59-- @param filename The file path
60-- @param is_bin open in binary mode, default false
61-- @return file contents
62function ml.readfile(filename,is_bin)
63 local mode = is_bin and 'b' or ''
64 local f,err = io.open(filename,'r'..mode)
65 if not f then return nil,err end
66 local res,err = f:read('*a')
67 f:close()
68 if not res then return nil,err end
69 return res
70end
71
72--- File and Path functions
73-- @section file
74
75--~ exists(filename)
76--- Does a file exist?
77-- @param filename a file path
78-- @return the file path, otherwise nil
79-- @usage exists 'readme' or exists 'readme.txt' or exists 'readme.md'
80function ml.exists (filename)
81 local f = io.open(filename)
82 if not f then
83 return nil
84 else
85 f:close()
86 return filename
87 end
88end
89
90local sep, other_sep = package.config:sub(1,1),'/'
91
92
93--- split a file path.
94-- if there's no directory part, the first value will be the empty string
95-- @param P A file path
96-- @return the directory part
97-- @return the file part
98function ml.splitpath(P)
99 local i = #P
100 local ch = P:sub(i,i)
101 while i > 0 and ch ~= sep and ch ~= other_sep do
102 i = i - 1
103 ch = P:sub(i,i)
104 end
105 if i == 0 then
106 return '',P
107 else
108 return P:sub(1,i-1), P:sub(i+1)
109 end
110end
111
112--- given a path, return the root part and the extension part.
113-- if there's no extension part, the second value will be empty
114-- @param P A file path
115-- @return the name part
116-- @return the extension
117function ml.splitext(P)
118 local i = #P
119 local ch = P:sub(i,i)
120 while i > 0 and ch ~= '.' do
121 if ch == sep or ch == other_sep then
122 return P,''
123 end
124 i = i - 1
125 ch = P:sub(i,i)
126 end
127 if i == 0 then
128 return P,''
129 else
130 return P:sub(1,i-1),P:sub(i)
131 end
132end
133
134--- Extended table functions.
135-- 'list' here is shorthand for 'list-like table'; these functions
136-- only operate over the numeric `1..#t` range of a table and are
137-- particularly efficient for this purpose.
138-- @section table
139
140local function quote (v)
141 if type(v) == 'string' then
142 return ('%q'):format(v)
143 else
144 return tostring(v)
145 end
146end
147
148local tbuff
149function tbuff (t,buff,k)
150 buff[k] = "{"
151 k = k + 1
152 for key,value in pairs(t) do
153 key = quote(key)
154 if type(value) ~= 'table' then
155 value = quote(value)
156 buff[k] = ('[%s]=%s'):format(key,value)
157 k = k + 1
158 if buff.limit and k > buff.limit then
159 buff[k] = "..."
160 error("buffer overrun")
161 end
162 else
163 if not buff.tables then buff.tables = {} end
164 if not buff.tables[value] then
165 k = tbuff(value,buff,k)
166 buff.tables[value] = true
167 else
168 buff[k] = "<cycle>"
169 k = k + 1
170 end
171 end
172 buff[k] = ","
173 k = k + 1
174 end
175 if buff[k-1] == "," then k = k - 1 end
176 buff[k] = "}"
177 k = k + 1
178 return k
179end
180
181--- return a string representation of a Lua table.
182-- Cycles are detected, and a limit on number of items can be imposed.
183-- @param t the table
184-- @param limit the limit on items, default 1000
185-- @return a string
186function ml.tstring (t,limit)
187 local buff = {limit = limit or 1000}
188 pcall(tbuff,t,buff,1)
189 return table.concat(buff)
190end
191
192--- dump a Lua table to a file object.
193-- @param t the table
194-- @param f the file object (anything supporting f.write)
195function ml.tdump(t,...)
196 local f = select('#',...) > 0 and select(1,...) or io.stdout
197 f:write(ml.tstring(t),'\n')
198end
199
200--- map a function over a list.
201-- The output must always be the same length as the input, so
202-- any `nil` values are mapped to `false`.
203-- @param f a function of one or more arguments
204-- @param t the table
205-- @param ... any extra arguments to the function
206-- @return a list with elements `f(t[i])`
207function ml.imap(f,t,...)
208 f = ml.function_arg(f)
209 local res = {}
210 for i = 1,#t do
211 local val = f(t[i],...)
212 if val == nil then val = false end
213 res[i] = val
214 end
215 return res
216end
217
218--- filter a list using a predicate.
219-- @param t a table
220-- @param pred the predicate function
221-- @param ... any extra arguments to the predicate
222-- @return a list such that `pred(t[i])` is true
223function ml.ifilter(t,pred,...)
224 local res,k = {},1
225 pred = ml.function_arg(pred)
226 for i = 1,#t do
227 if pred(t[i],...) then
228 res[k] = t[i]
229 k = k + 1
230 end
231 end
232 return res
233end
234
235--- find an item in a list using a predicate.
236-- @param t the list
237-- @param pred a function of at least one argument
238-- @param ... any extra arguments
239-- @return the item value
240function ml.ifind(t,pred,...)
241 pred = ml.function_arg(pred)
242 for i = 1,#t do
243 if pred(t[i],...) then
244 return t[i]
245 end
246 end
247end
248
249--- return the index of an item in a list.
250-- @param t the list
251-- @param value item value
252-- @return index, otherwise `nil`
253function ml.index (t,value)
254 for i = 1,#t do
255 if t[i] == value then return i end
256 end
257end
258
259--- return a slice of a list.
260-- Like string.sub, the end index may be negative.
261-- @param t the list
262-- @param i1 the start index
263-- @param i2 the end index, default #t
264function ml.sub(t,i1,i2)
265 if not i2 or i2 > #t then
266 i2 = #t
267 elseif i2 < 0 then
268 i2 = #t + i2 + 1
269 end
270 local res,k = {},1
271 for i = i1,i2 do
272 res[k] = t[i]
273 k = k + 1
274 end
275 return res
276end
277
278--- map a function over a Lua table.
279-- @param f a function of one or more arguments
280-- @param t the table
281-- @param ... any optional arguments to the function
282function ml.tmap(f,t,...)
283 f = ml.function_arg(f)
284 local res = {}
285 for k,v in pairs(t) do
286 res[k] = f(v,...)
287 end
288 return res
289end
290
291--- filter a table using a predicate.
292-- @param t a table
293-- @param pred the predicate function
294-- @param ... any extra arguments to the predicate
295-- @usage tfilter({a=1,b='boo'},tonumber) == {a=1}
296function ml.tfilter (t,pred,...)
297 local res = {}
298 pred = ml.function_arg(pred)
299 for k,v in pairs(t) do
300 if pred(v,...) then
301 res[k] = v
302 end
303 end
304 return res
305end
306
307--- add the key/value pairs of `other` to `t`.
308-- For sets, this is their union. For the same keys,
309-- the values from the first table will be overwritten
310-- @param t table to be updated
311-- @param other table
312-- @return the updated table
313function ml.update(t,other)
314 for k,v in pairs(other) do
315 t[k] = v
316 end
317 return t
318end
319
320--- extend a list using values from another.
321-- @param t the list to be extended
322-- @param other a list
323-- @return the extended list
324function ml.extend(t,other)
325 local n = #t
326 for i = 1,#other do
327 t[n+i] = other[i]
328 end
329 return t
330end
331
332--- make a set from a list.
333-- @param t a list of values
334-- @return a table where the keys are the values
335-- @usage set{'one','two'} == {one=true,two=true}
336function ml.set(t)
337 local res = {}
338 for i = 1,#t do
339 res[t[i]] = true
340 end
341 return res
342end
343
344--- extract the keys of a table as a list.
345-- This is the opposite operation to tset
346-- @param t a table
347-- @param a list of keys
348function ml.keys(t)
349 local res,k = {},1
350 for key in pairs(t) do
351 res[k] = key
352 k = k + 1
353 end
354 return res
355end
356
357--- is `other` a subset of `t`?
358-- @param t a set
359-- @param other a possible subset
360-- @return true or false
361function ml.subset(t,other)
362 for k,v in pairs(other) do
363 if t[k] ~= v then return false end
364 end
365 return true
366end
367
368--- are these two tables equal?
369-- This is shallow equality.
370-- @param t a table
371-- @param other a table
372-- @return true or false
373function ml.tequal(t,other)
374 return ml.subset(t,other) and ml.subset(other,t)
375end
376
377--- the intersection of two tables.
378-- Works as expected for sets, otherwise note that the first
379-- table's values are preseved
380-- @param t a table
381-- @param other a table
382-- @return the intersection of the tables
383function ml.intersect(t,other)
384 local res = {}
385 for k,v in pairs(t) do
386 if other[k] then
387 res[k] = v
388 end
389 end
390 return res
391end
392
393--- collect the values of an iterator into a list.
394-- @param iter a single or double-valued iterator
395-- @param count an optional number of values to collect
396-- @return a list of values.
397-- @usage collect(ipairs{10,20}) == {{1,10},{2,20}}
398function ml.collect (iter, count)
399 local res,k = {},1
400 local v1,v2 = iter()
401 local dbl = v2 ~= nil
402 while v1 do
403 if dbl then v1 = {v1,v2} end
404 res[k] = v1
405 k = k + 1
406 if count and k > count then break end
407 v1,v2 = iter()
408 end
409 return res
410end
411
412--- Functional helpers.
413-- @section function
414
415--- create a function which will throw an error on failure.
416-- @param f a function that returns nil,err if it fails
417-- @return an equivalent function that raises an error
418function ml.throw(f)
419 f = ml.function_arg(f)
420 return function(...)
421 local res,err = f(...)
422 if err then error(err) end
423 return res
424 end
425end
426
427--- create a function which will never throw an error.
428-- This is the opposite situation to throw; if the
429-- original function throws an error e, then this
430-- function will return nil,e.
431-- @param f a function which can throw an error
432-- @return a function which returns nil,error when it fails
433function ml.safe(f)
434 f = ml.function_arg(f)
435 return function(...)
436 local ok,r1,r2,r3 = pcall(f,...)
437 if ok then return r1,r2,r3
438 else
439 return nil,r1
440 end
441 end
442end
443--memoize(f)
444
445--- bind the value `v` to the first argument of function `f`.
446-- @param f a function of at least one argument
447-- @param v a value
448-- @return a function of one less argument
449-- @usage (bind1(string.match,'hello')('^hell') == 'hell'
450function ml.bind1(f,v)
451 f = ml.function_arg(f)
452 return function(...)
453 return f(v,...)
454 end
455end
456
457--- compose two functions.
458-- For instance, `printf` can be defined as `compose(io.write,string.format)`
459-- @param f1 a function
460-- @param f2 a function
461-- @return f1(f2(...))
462function ml.compose(f1,f2)
463 f1 = ml.function_arg(f1)
464 f2 = ml.function_arg(f2)
465 return function(...)
466 return f1(f2(...))
467 end
468end
469
470--- is the object either a function or a callable object?.
471-- @param obj Object to check.
472-- @return true if callable
473function ml.callable (obj)
474 return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call
475end
476
477function ml.function_arg(f)
478 assert(ml.callable(f),"expecting a function or callable object")
479 return f
480end
481
482--- Classes.
483-- @section class
484
485--- create a class with an optional base class.
486-- The resulting table has a new() function for invoking
487-- the constructor, which must be named `_init`. If the base
488-- class has a constructor, you can call it as the `super()` method.
489-- The `__tostring` metamethod is also inherited, but others need
490-- to be brought in explicitly.
491-- @param base optional base class
492-- @return the metatable representing the class
493function ml.class(base)
494 local klass, base_ctor = {}
495 klass.__index = klass
496 if base then
497 setmetatable(klass,base)
498 klass._base = base
499 base_ctor = rawget(base,'_init')
500 klass.__tostring = base.__tostring
501 end
502 function klass.new(...)
503 local self = setmetatable({},klass)
504 if rawget(klass,'_init') then
505 klass.super = base_ctor -- make super available for ctor
506 klass._init(self,...)
507 elseif base_ctor then -- call base ctor automatically
508 base_ctor(self,...)
509 end
510 return self
511 end
512 return klass
513end
514
515--- is an object derived from a class?
516-- @param self the object
517-- @param klass a class created with `class`
518-- @return true or false
519function ml.is_a(self,klass)
520 local m = getmetatable(self)
521 if not m then return false end --*can't be an object!
522 while m do
523 if m == klass then return true end
524 m = rawget(m,'_base')
525 end
526 return false
527end
528
529local _type = type
530
531--- extended type of an object.
532-- The type of a table is its metatable, otherwise works like standard type()
533-- @param obj a value
534-- @return the type, either a string or the metatable
535function ml.type (obj)
536 if _type(obj) == 'table' then
537 return getmetatable(obj) or 'table'
538 else
539 return _type(obj)
540 end
541end
542
543return ml