This is an important refactor of the way documentation generation works

* Installs luarocks WITH it's executable (easy to install other rocks if necessary)
* Use Lua supplied with luarocks
* Create Utils/luadocumentor.bat, which works with RELATIVE PATH ! -> Everybody can generate the doc
* Updated launch files accordingly
This commit is contained in:
Grey-Echo
2017-04-05 01:26:39 +02:00
parent 513a103947
commit 3b69cf992e
313 changed files with 16547 additions and 36807 deletions
@@ -0,0 +1,546 @@
--- Date and Date Format classes.
-- See <a href="../../index.html#date">the Guide</a>.
-- @class module
-- @name pl.Date
-- @pragma nostrip
--[[
module("pl.Date")
]]
local class = require 'pl.class'
local os_time, os_date = os.time, os.date
local stringx = require 'pl.stringx'
local Date = class()
Date.Format = class()
--- Date constructor.
-- @param t this can be either <ul>
-- <li>nil - use current date and time</li>
-- <li>number - seconds since epoch (as returned by @{os.time})</li>
-- <li>Date - copy constructor</li>
-- <li>table - table containing year, month, etc as for os.time()
-- You may leave out year, month or day, in which case current values will be used.
-- </li>
-- <li> two to six numbers: year, month, day, hour, min, sec
-- </ul>
-- @function Date
function Date:_init(t,...)
local time
if select('#',...) > 0 then
local extra = {...}
local year = t
t = {
year = year,
month = extra[1],
day = extra[2],
hour = extra[3],
min = extra[4],
sec = extra[5]
}
end
if t == nil then
time = os_time()
elseif type(t) == 'number' then
time = t
elseif type(t) == 'table' then
if getmetatable(t) == Date then -- copy ctor
time = t.time
else
if not (t.year and t.month and t.year) then
local lt = os.date('*t')
if not t.year and not t.month and not t.day then
t.year = lt.year
t.month = lt.month
t.day = lt.day
else
t.year = t.year or lt.year
t.month = t.month or (t.day and lt.month or 1)
t.day = t.day or 1
end
end
time = os_time(t)
end
end
self:set(time)
end
local thour,tmin
--- get the time zone offset from UTC.
-- @return hours ahead of UTC
-- @return minutes ahead of UTC
function Date.tzone ()
if not thour then
local t = os.time()
local ut = os.date('!*t',t)
local lt = os.date('*t',t)
thour = lt.hour - ut.hour
tmin = lt.min - ut.min
end
return thour, tmin
end
--- convert this date to UTC.
function Date:toUTC ()
local th, tm = Date.tzone()
self:add { hour = -th }
if tm > 0 then self:add {min = -tm} end
end
--- convert this UTC date to local.
function Date:toLocal ()
local th, tm = Date.tzone()
self:add { hour = th }
if tm > 0 then self:add {min = tm} end
end
--- set the current time of this Date object.
-- @param t seconds since epoch
function Date:set(t)
self.time = t
self.tab = os_date('*t',self.time)
end
--- set the year.
-- @param y Four-digit year
-- @class function
-- @name Date:year
--- set the month.
-- @param m month
-- @class function
-- @name Date:month
--- set the day.
-- @param d day
-- @class function
-- @name Date:day
--- set the hour.
-- @param h hour
-- @class function
-- @name Date:hour
--- set the minutes.
-- @param min minutes
-- @class function
-- @name Date:min
--- set the seconds.
-- @param sec seconds
-- @class function
-- @name Date:sec
--- set the day of year.
-- @class function
-- @param yday day of year
-- @name Date:yday
--- get the year.
-- @param y Four-digit year
-- @class function
-- @name Date:year
--- get the month.
-- @class function
-- @name Date:month
--- get the day.
-- @class function
-- @name Date:day
--- get the hour.
-- @class function
-- @name Date:hour
--- get the minutes.
-- @class function
-- @name Date:min
--- get the seconds.
-- @class function
-- @name Date:sec
--- get the day of year.
-- @class function
-- @name Date:yday
for _,c in ipairs{'year','month','day','hour','min','sec','yday'} do
Date[c] = function(self,val)
if val then
self.tab[c] = val
self:set(os_time(self.tab))
return self
else
return self.tab[c]
end
end
end
--- name of day of week.
-- @param full abbreviated if true, full otherwise.
-- @return string name
function Date:weekday_name(full)
return os_date(full and '%A' or '%a',self.time)
end
--- name of month.
-- @param full abbreviated if true, full otherwise.
-- @return string name
function Date:month_name(full)
return os_date(full and '%B' or '%b',self.time)
end
--- is this day on a weekend?.
function Date:is_weekend()
return self.tab.wday == 0 or self.tab.wday == 6
end
--- add to a date object.
-- @param t a table containing one of the following keys and a value:<br>
-- year,month,day,hour,min,sec
-- @return this date
function Date:add(t)
local key,val = next(t)
self.tab[key] = self.tab[key] + val
self:set(os_time(self.tab))
return self
end
--- last day of the month.
-- @return int day
function Date:last_day()
local d = 28
local m = self.tab.month
while self.tab.month == m do
d = d + 1
self:add{day=1}
end
self:add{day=-1}
return self
end
--- difference between two Date objects.
-- Note: currently the result is a regular @{Date} object,
-- but also has `interval` field set, which means a more
-- appropriate string rep is used.
-- @param other Date object
-- @return a Date object
function Date:diff(other)
local dt = self.time - other.time
if dt < 0 then error("date difference is negative!",2) end
local date = Date(dt)
date.interval = true
return date
end
--- long numerical ISO data format version of this date.
function Date:__tostring()
if not self.interval then
return os_date('%Y-%m-%d %H:%M:%S',self.time)
else
local t, res = self.tab, ''
local y,m,d = t.year - 1970, t.month - 1, t.day - 1
if y > 0 then res = res .. y .. ' years ' end
if m > 0 then res = res .. m .. ' months ' end
if d > 0 then res = res .. d .. ' days ' end
if y == 0 and m == 0 then
local h = t.hour - Date.tzone() -- not accounting for UTC mins!
if h > 0 then res = res .. h .. ' hours ' end
if t.min > 0 then res = res .. t.min .. ' min ' end
if t.sec > 0 then res = res .. t.sec .. ' sec ' end
end
return res
end
end
--- equality between Date objects.
function Date:__eq(other)
return self.time == other.time
end
--- equality between Date objects.
function Date:__lt(other)
return self.time < other.time
end
------------ Date.Format class: parsing and renderinig dates ------------
-- short field names, explicit os.date names, and a mask for allowed field repeats
local formats = {
d = {'day',{true,true}},
y = {'year',{false,true,false,true}},
m = {'month',{true,true}},
H = {'hour',{true,true}},
M = {'min',{true,true}},
S = {'sec',{true,true}},
}
--
--- Date.Format constructor.
-- @param fmt. A string where the following fields are significant: <ul>
-- <li>d day (either d or dd)</li>
-- <li>y year (either yy or yyy)</li>
-- <li>m month (either m or mm)</li>
-- <li>H hour (either H or HH)</li>
-- <li>M minute (either M or MM)</li>
-- <li>S second (either S or SS)</li>
-- </ul>
-- Alternatively, if fmt is nil then this returns a flexible date parser
-- that tries various date/time schemes in turn:
-- <ol>
-- <li> <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>,
-- like 2010-05-10 12:35:23Z or 2008-10-03T14:30+02<li>
-- <li> times like 15:30 or 8.05pm (assumed to be today's date)</li>
-- <li> dates like 28/10/02 (European order!) or 5 Feb 2012 </li>
-- <li> month name like march or Mar (case-insensitive, first 3 letters);
-- here the day will be 1 and the year this current year </li>
-- </ol>
-- A date in format 3 can be optionally followed by a time in format 2.
-- Please see test-date.lua in the tests folder for more examples.
-- @usage df = Date.Format("yyyy-mm-dd HH:MM:SS")
-- @class function
-- @name Date.Format
function Date.Format:_init(fmt)
if not fmt then return end
local append = table.insert
local D,PLUS,OPENP,CLOSEP = '\001','\002','\003','\004'
local vars,used = {},{}
local patt,outf = {},{}
local i = 1
while i < #fmt do
local ch = fmt:sub(i,i)
local df = formats[ch]
if df then
if used[ch] then error("field appeared twice: "..ch,2) end
used[ch] = true
-- this field may be repeated
local _,inext = fmt:find(ch..'+',i+1)
local cnt = not _ and 1 or inext-i+1
if not df[2][cnt] then error("wrong number of fields: "..ch,2) end
-- single chars mean 'accept more than one digit'
local p = cnt==1 and (D..PLUS) or (D):rep(cnt)
append(patt,OPENP..p..CLOSEP)
append(vars,ch)
if ch == 'y' then
append(outf,cnt==2 and '%y' or '%Y')
else
append(outf,'%'..ch)
end
i = i + cnt
else
append(patt,ch)
append(outf,ch)
i = i + 1
end
end
-- escape any magic characters
fmt = table.concat(patt):gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')
-- replace markers with their magic equivalents
fmt = fmt:gsub(D,'%%d'):gsub(PLUS,'+'):gsub(OPENP,'('):gsub(CLOSEP,')')
self.fmt = fmt
self.outf = table.concat(outf)
self.vars = vars
end
local parse_date
--- parse a string into a Date object.
-- @param str a date string
-- @return date object
function Date.Format:parse(str)
if not self.fmt then
return parse_date(str,self.us)
end
local res = {str:match(self.fmt)}
if #res==0 then return nil, 'cannot parse '..str end
local tab = {}
for i,v in ipairs(self.vars) do
local name = formats[v][1] -- e.g. 'y' becomes 'year'
tab[name] = tonumber(res[i])
end
-- os.date() requires these fields; if not present, we assume
-- that the time set is for the current day.
if not (tab.year and tab.month and tab.year) then
local today = Date()
tab.year = tab.year or today:year()
tab.month = tab.month or today:month()
tab.day = tab.day or today:month()
end
local Y = tab.year
if Y < 100 then -- classic Y2K pivot
tab.year = Y + (Y < 35 and 2000 or 1999)
elseif not Y then
tab.year = 1970
end
--dump(tab)
return Date(tab)
end
--- convert a Date object into a string.
-- @param d a date object, or a time value as returned by @{os.time}
-- @return string
function Date.Format:tostring(d)
local tm = type(d) == 'number' and d or d.time
if self.outf then
return os.date(self.outf,tm)
else
return tostring(Date(d))
end
end
function Date.Format:US_order(yesno)
self.us = yesno
end
local months = {jan=1,feb=2,mar=3,apr=4,may=5,jun=6,jul=7,aug=8,sep=9,oct=10,nov=11,dec=12}
--[[
Allowed patterns:
- [day] [monthname] [year] [time]
- [day]/[month][/year] [time]
]]
local is_word = stringx.isalpha
local is_number = stringx.isdigit
local function tonum(s,l1,l2,kind)
kind = kind or ''
local n = tonumber(s)
if not n then error(("%snot a number: '%s'"):format(kind,s)) end
if n < l1 or n > l2 then
error(("%s out of range: %s is not between %d and %d"):format(kind,s,l1,l2))
end
return n
end
local function parse_iso_end(p,ns,sec)
-- may be fractional part of seconds
local _,nfrac,secfrac = p:find('^%.%d+',ns+1)
if secfrac then
sec = sec .. secfrac
p = p:sub(nfrac+1)
else
p = p:sub(ns+1)
end
-- ISO 8601 dates may end in Z (for UTC) or [+-][isotime]
if p:match 'z$' then return sec, {h=0,m=0} end -- we're UTC!
p = p:gsub(':','') -- turn 00:30 to 0030
local _,_,sign,offs = p:find('^([%+%-])(%d+)')
if not sign then return sec, nil end -- not UTC
if #offs == 2 then offs = offs .. '00' end -- 01 to 0100
local tz = { h = tonumber(offs:sub(1,2)), m = tonumber(offs:sub(3,4)) }
if sign == '-' then tz.h = -tz.h; tz.m = -tz.m end
return sec, tz
end
local function parse_date_unsafe (s,US)
s = s:gsub('T',' ') -- ISO 8601
local parts = stringx.split(s:lower())
local i,p = 1,parts[1]
local function nextp() i = i + 1; p = parts[i] end
local year,min,hour,sec,apm
local tz
local _,nxt,day, month = p:find '^(%d+)/(%d+)'
if day then
-- swop for US case
if US then
day, month = month, day
end
_,_,year = p:find('^/(%d+)',nxt+1)
nextp()
else -- ISO
year,month,day = p:match('^(%d+)%-(%d+)%-(%d+)')
if year then
nextp()
end
end
if p and not year and is_number(p) then -- has to be date
day = p
nextp()
end
if p and is_word(p) then
p = p:sub(1,3)
local mon = months[p]
if mon then
month = mon
else error("not a month: " .. p) end
nextp()
end
if p and not year and is_number(p) then
year = p
nextp()
end
if p then -- time is hh:mm[:ss], hhmm[ss] or H.M[am|pm]
_,nxt,hour,min = p:find '^(%d+):(%d+)'
local ns
if nxt then -- are there seconds?
_,ns,sec = p:find ('^:(%d+)',nxt+1)
--if ns then
sec,tz = parse_iso_end(p,ns or nxt,sec)
--end
else -- might be h.m
_,ns,hour,min = p:find '^(%d+)%.(%d+)'
if ns then
apm = p:match '[ap]m$'
else -- or hhmm[ss]
local hourmin
_,nxt,hourmin = p:find ('^(%d+)')
if nxt then
hour = hourmin:sub(1,2)
min = hourmin:sub(3,4)
sec = hourmin:sub(5,6)
if #sec == 0 then sec = nil end
sec,tz = parse_iso_end(p,nxt,sec)
end
end
end
end
local today
if not (year and month and day) then
today = Date()
end
day = day and tonum(day,1,31,'day') or (month and 1 or today:day())
month = month and tonum(month,1,12,'month') or today:month()
year = year and tonumber(year) or today:year()
if year < 100 then -- two-digit year pivot
year = year + (year < 35 and 2000 or 1900)
end
hour = hour and tonum(hour,1,apm and 12 or 24,'hour') or 12
if apm == 'pm' then
hour = hour + 12
end
min = min and tonum(min,1,60) or 0
sec = sec and tonum(sec,1,60) or 0
local res = Date {year = year, month = month, day = day, hour = hour, min = min, sec = sec}
if tz then -- ISO 8601 UTC time
res:toUTC()
res:add {hour = tz.h}
if tz.m ~= 0 then res:add {min = tz.m} end
end
return res
end
function parse_date (s)
local ok, d = pcall(parse_date_unsafe,s)
if not ok then -- error
d = d:gsub('.-:%d+: ','')
return nil, d
else
return d
end
end
return Date
@@ -0,0 +1,553 @@
--- Python-style list class. <p>
-- Based on original code by Nick Trout.
-- <p>
-- <b>Please Note</b>: methods that change the list will return the list.
-- This is to allow for method chaining, but please note that <tt>ls = ls:sort()</tt>
-- does not mean that a new copy of the list is made. In-place (mutable) methods
-- are marked as returning 'the list' in this documentation.
-- <p>
-- See the Guide for further <a href="../../index.html#list">discussion</a>
-- <p>
-- See <a href="http://www.python.org/doc/current/tut/tut.html">http://www.python.org/doc/current/tut/tut.html</a>, section 5.1
-- <p>
-- <b>Note</b>: The comments before some of the functions are from the Python docs
-- and contain Python code.
-- <p>
-- Written for Lua version 4.0 <br />
-- Redone for Lua 5.1, Steve Donovan.
-- @class module
-- @name pl.List
-- @pragma nostrip
local tinsert,tremove,concat,tsort = table.insert,table.remove,table.concat,table.sort
local setmetatable, getmetatable,type,tostring,assert,string,next = setmetatable,getmetatable,type,tostring,assert,string,next
local write = io.write
local tablex = require 'pl.tablex'
local filter,imap,imap2,reduce,transform,tremovevalues = tablex.filter,tablex.imap,tablex.imap2,tablex.reduce,tablex.transform,tablex.removevalues
local tablex = tablex
local tsub = tablex.sub
local utils = require 'pl.utils'
local function_arg = utils.function_arg
local is_type = utils.is_type
local split = utils.split
local assert_arg = utils.assert_arg
local normalize_slice = tablex._normalize_slice
--[[
module ('pl.List',utils._module)
]]
local Multimap = utils.stdmt.MultiMap
-- metatable for our list objects
local List = utils.stdmt.List
List.__index = List
List._class = List
local iter
-- we give the metatable its own metatable so that we can call it like a function!
setmetatable(List,{
__call = function (tbl,arg)
return List.new(arg)
end,
})
local function makelist (t,obj)
local klass = List
if obj then
klass = getmetatable(obj)
end
return setmetatable(t,klass)
end
local function is_list(t)
return getmetatable(t) == List
end
local function simple_table(t)
return type(t) == 'table' and not is_list(t) and #t > 0
end
function List:_init (src)
if src then
for v in iter(src) do
tinsert(self,v)
end
end
end
--- Create a new list. Can optionally pass a table;
-- passing another instance of List will cause a copy to be created
-- we pass anything which isn't a simple table to iterate() to work out
-- an appropriate iterator @see List.iterate
-- @param t An optional list-like table
-- @return a new List
-- @usage ls = List(); ls = List {1,2,3,4}
function List.new(t)
local ls
if not simple_table(t) then
ls = {}
List._init(ls,t)
else
ls = t
end
makelist(ls)
return ls
end
function List:clone()
local ls = makelist({},self)
List._init(ls,self)
return ls
end
function List.default_map_with(T)
return function(self,name)
local f = T[name]
if f then
return function(self,...)
return self:map(f,...)
end
else
error("method not found: "..name,2)
end
end
end
---Add an item to the end of the list.
-- @param i An item
-- @return the list
function List:append(i)
tinsert(self,i)
return self
end
List.push = tinsert
--- Extend the list by appending all the items in the given list.
-- equivalent to 'a[len(a):] = L'.
-- @param L Another List
-- @return the list
function List:extend(L)
assert_arg(1,L,'table')
for i = 1,#L do tinsert(self,L[i]) end
return self
end
--- Insert an item at a given position. i is the index of the
-- element before which to insert.
-- @param i index of element before whichh to insert
-- @param x A data item
-- @return the list
function List:insert(i, x)
assert_arg(1,i,'number')
tinsert(self,i,x)
return self
end
--- Insert an item at the begining of the list.
-- @param x a data item
-- @return the list
function List:put (x)
return self:insert(1,x)
end
--- Remove an element given its index.
-- (equivalent of Python's del s[i])
-- @param i the index
-- @return the list
function List:remove (i)
assert_arg(1,i,'number')
tremove(self,i)
return self
end
--- Remove the first item from the list whose value is given.
-- (This is called 'remove' in Python; renamed to avoid confusion
-- with table.remove)
-- Return nil if there is no such item.
-- @param x A data value
-- @return the list
function List:remove_value(x)
for i=1,#self do
if self[i]==x then tremove(self,i) return self end
end
return self
end
--- Remove the item at the given position in the list, and return it.
-- If no index is specified, a:pop() returns the last item in the list.
-- The item is also removed from the list.
-- @param i An index
-- @return the item
function List:pop(i)
if not i then i = #self end
assert_arg(1,i,'number')
return tremove(self,i)
end
List.get = List.pop
--- Return the index in the list of the first item whose value is given.
-- Return nil if there is no such item.
-- @class function
-- @name List:index
-- @param x A data value
-- @param idx where to start search (default 1)
-- @return the index, or nil if not found.
local tfind = tablex.find
List.index = tfind
--- does this list contain the value?.
-- @param x A data value
-- @return true or false
function List:contains(x)
return tfind(self,x) and true or false
end
--- Return the number of times value appears in the list.
-- @param x A data value
-- @return number of times x appears
function List:count(x)
local cnt=0
for i=1,#self do
if self[i]==x then cnt=cnt+1 end
end
return cnt
end
--- Sort the items of the list, in place.
-- @param cmp an optional comparison function; '<' is used if not given.
-- @return the list
function List:sort(cmp)
tsort(self,cmp)
return self
end
--- Reverse the elements of the list, in place.
-- @return the list
function List:reverse()
local t = self
local n = #t
local n2 = n/2
for i = 1,n2 do
local k = n-i+1
t[i],t[k] = t[k],t[i]
end
return self
end
--- Emulate list slicing. like 'list[first:last]' in Python.
-- If first or last are negative then they are relative to the end of the list
-- eg. slice(-2) gives last 2 entries in a list, and
-- slice(-4,-2) gives from -4th to -2nd
-- @param first An index
-- @param last An index
-- @return a new List
function List:slice(first,last)
return tsub(self,first,last)
end
--- empty the list.
-- @return the list
function List:clear()
for i=1,#self do tremove(self,i) end
return self
end
local eps = 1.0e-10
--- Emulate Python's range(x) function.
-- Include it in List table for tidiness
-- @param start A number
-- @param finish A number greater than start; if zero, then 0..start-1
-- @param incr an optional increment (may be less than 1)
-- @usage List.range(0,3) == List {0,1,2,3}
function List.range(start,finish,incr)
if not finish then
start = 0
finish = finish - 1
end
if incr then
if not utils.is_integer(incr) then finish = finish + eps end
else
incr = 1
end
assert_arg(1,start,'number')
assert_arg(2,finish,'number')
local t = List.new()
for i=start,finish,incr do tinsert(t,i) end
return t
end
--- list:len() is the same as #list.
function List:len()
return #self
end
-- Extended operations --
--- Remove a subrange of elements.
-- equivalent to 'del s[i1:i2]' in Python.
-- @param i1 start of range
-- @param i2 end of range
-- @return the list
function List:chop(i1,i2)
return tremovevalues(self,i1,i2)
end
--- Insert a sublist into a list
-- equivalent to 's[idx:idx] = list' in Python
-- @param idx index
-- @param list list to insert
-- @return the list
-- @usage l = List{10,20}; l:splice(2,{21,22}); assert(l == List{10,21,22,20})
function List:splice(idx,list)
assert_arg(1,idx,'number')
idx = idx - 1
local i = 1
for v in iter(list) do
tinsert(self,i+idx,v)
i = i + 1
end
return self
end
--- general slice assignment s[i1:i2] = seq.
-- @param i1 start index
-- @param i2 end index
-- @param seq a list
-- @return the list
function List:slice_assign(i1,i2,seq)
assert_arg(1,i1,'number')
assert_arg(1,i2,'number')
i1,i2 = normalize_slice(self,i1,i2)
if i2 >= i1 then self:chop(i1,i2) end
self:splice(i1,seq)
return self
end
--- concatenation operator.
-- @param L another List
-- @return a new list consisting of the list with the elements of the new list appended
function List:__concat(L)
assert_arg(1,L,'table')
local ls = self:clone()
ls:extend(L)
return ls
end
--- equality operator ==. True iff all elements of two lists are equal.
-- @param L another List
-- @return true or false
function List:__eq(L)
if #self ~= #L then return false end
for i = 1,#self do
if self[i] ~= L[i] then return false end
end
return true
end
--- join the elements of a list using a delimiter. <br>
-- This method uses tostring on all elements.
-- @param delim a delimiter string, can be empty.
-- @return a string
function List:join (delim)
delim = delim or ''
assert_arg(1,delim,'string')
return concat(imap(tostring,self),delim)
end
--- join a list of strings. <br>
-- Uses table.concat directly.
-- @class function
-- @name List:concat
-- @param delim a delimiter
-- @return a string
List.concat = concat
local function tostring_q(val)
local s = tostring(val)
if type(val) == 'string' then
s = '"'..s..'"'
end
return s
end
--- how our list should be rendered as a string. Uses join().
-- @see List:join
function List:__tostring()
return '{'..self:join(',',tostring_q)..'}'
end
--[[
-- NOTE: this works, but is unreliable. If you leave the loop before finishing,
-- then the iterator is not reset.
--- can iterate over a list directly.
-- @usage for v in ls do print(v) end
function List:__call()
if not self.key then self.key = 1 end
local value = self[self.key]
self.key = self.key + 1
if not value then self.key = nil end
return value
end
--]]
--[[
function List.__call(t,v,i)
i = (i or 0) + 1
v = t[i]
if v then return i, v end
end
--]]
--- call the function for each element of the list.
-- @param fun a function or callable object
-- @param ... optional values to pass to function
function List:foreach (fun,...)
local t = self
fun = function_arg(1,fun)
for i = 1,#t do
fun(t[i],...)
end
end
--- create a list of all elements which match a function.
-- @param fun a boolean function
-- @param arg optional argument to be passed as second argument of the predicate
-- @return a new filtered list.
function List:filter (fun,arg)
return makelist(filter(self,fun,arg),self)
end
--- split a string using a delimiter.
-- @param s the string
-- @param delim the delimiter (default spaces)
-- @return a List of strings
-- @see pl.utils.split
function List.split (s,delim)
assert_arg(1,s,'string')
return makelist(split(s,delim))
end
--- apply a function to all elements.
-- Any extra arguments will be passed to the function
-- @param fun a function of at least one argument
-- @param ... arbitrary extra arguments.
-- @return a new list: {f(x) for x in self}
-- @see pl.tablex.imap
function List:map (fun,...)
return makelist(imap(fun,self,...),self)
end
--- apply a function to all elements, in-place.
-- Any extra arguments are passed to the function.
-- @param fun A function that takes at least one argument
-- @param ... arbitrary extra arguments.
function List:transform (fun,...)
transform(fun,self,...)
end
--- apply a function to elements of two lists.
-- Any extra arguments will be passed to the function
-- @param fun a function of at least two arguments
-- @param ls another list
-- @param ... arbitrary extra arguments.
-- @return a new list: {f(x,y) for x in self, for x in arg1}
-- @see pl.tablex.imap2
function List:map2 (fun,ls,...)
return makelist(imap2(fun,self,ls,...),self)
end
--- apply a named method to all elements.
-- Any extra arguments will be passed to the method.
-- @param name name of method
-- @param ... extra arguments
-- @return a new list of the results
-- @see pl.seq.mapmethod
function List:mapm (name,...)
local res = {}
local t = self
for i = 1,#t do
local val = t[i]
local fn = val[name]
if not fn then error(type(val).." does not have method "..name,2) end
res[i] = fn(val,...)
end
return makelist(res,self)
end
--- 'reduce' a list using a binary function.
-- @param fun a function of two arguments
-- @return result of the function
-- @see pl.tablex.reduce
function List:reduce (fun)
return reduce(fun,self)
end
--- partition a list using a classifier function.
-- The function may return nil, but this will be converted to the string key '<nil>'.
-- @param fun a function of at least one argument
-- @param ... will also be passed to the function
-- @return a table where the keys are the returned values, and the values are Lists
-- of values where the function returned that key. It is given the type of Multimap.
-- @see pl.MultiMap
function List:partition (fun,...)
fun = function_arg(1,fun)
local res = {}
for i = 1,#self do
local val = self[i]
local klass = fun(val,...)
if klass == nil then klass = '<nil>' end
if not res[klass] then res[klass] = List() end
res[klass]:append(val)
end
return setmetatable(res,Multimap)
end
--- return an iterator over all values.
function List:iter ()
return iter(self)
end
--- Create an iterator over a seqence.
-- This captures the Python concept of 'sequence'.
-- For tables, iterates over all values with integer indices.
-- @param seq a sequence; a string (over characters), a table, a file object (over lines) or an iterator function
-- @usage for x in iterate {1,10,22,55} do io.write(x,',') end ==> 1,10,22,55
-- @usage for ch in iterate 'help' do do io.write(ch,' ') end ==> h e l p
function List.iterate(seq)
if type(seq) == 'string' then
local idx = 0
local n = #seq
local sub = string.sub
return function ()
idx = idx + 1
if idx > n then return nil
else
return sub(seq,idx,idx)
end
end
elseif type(seq) == 'table' then
local idx = 0
local n = #seq
return function()
idx = idx + 1
if idx > n then return nil
else
return seq[idx]
end
end
elseif type(seq) == 'function' then
return seq
elseif type(seq) == 'userdata' and io.type(seq) == 'file' then
return seq:lines()
end
end
iter = List.iterate
return List
@@ -0,0 +1,108 @@
--- A Map class.
-- @class module
-- @name pl.Map
--[[
module ('pl.Map')
]]
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local stdmt = utils.stdmt
local is_callable = utils.is_callable
local tmakeset,deepcompare,merge,keys,difference,tupdate = tablex.makeset,tablex.deepcompare,tablex.merge,tablex.keys,tablex.difference,tablex.update
local pretty_write = require 'pl.pretty' . write
local Map = stdmt.Map
local Set = stdmt.Set
local List = stdmt.List
local class = require 'pl.class'
-- the Map class ---------------------
class(nil,nil,Map)
local function makemap (m)
return setmetatable(m,Map)
end
function Map:_init (t)
local mt = getmetatable(t)
if mt == Set or mt == Map then
self:update(t)
else
return t -- otherwise assumed to be a map-like table
end
end
local function makelist(t)
return setmetatable(t,List)
end
--- list of keys.
Map.keys = tablex.keys
--- list of values.
Map.values = tablex.values
--- return an iterator over all key-value pairs.
function Map:iter ()
return pairs(self)
end
--- return a List of all key-value pairs, sorted by the keys.
function Map:items()
local ls = makelist(tablex.pairmap (function (k,v) return makelist {k,v} end, self))
ls:sort(function(t1,t2) return t1[1] < t2[1] end)
return ls
end
-- Will return the existing value, or if it doesn't exist it will set
-- a default value and return it.
function Map:setdefault(key, defaultval)
return self[key] or self:set(key,defaultval) or defaultval
end
--- size of map.
-- note: this is a relatively expensive operation!
-- @class function
-- @name Map:len
Map.len = tablex.size
--- put a value into the map.
-- @param key the key
-- @param val the value
function Map:set (key,val)
self[key] = val
end
--- get a value from the map.
-- @param key the key
-- @return the value, or nil if not found.
function Map:get (key)
return rawget(self,key)
end
local index_by = tablex.index_by
-- get a list of values indexed by a list of keys.
-- @param keys a list-like table of keys
-- @return a new list
function Map:getvalues (keys)
return makelist(index_by(self,keys))
end
Map.iter = pairs
Map.update = tablex.update
function Map:__eq (m)
-- note we explicitly ask deepcompare _not_ to use __eq!
return deepcompare(self,m,true)
end
function Map:__tostring ()
return pretty_write(self,'')
end
return Map
@@ -0,0 +1,65 @@
--- MultiMap, a Map which has multiple values per key. <br>
-- @class module
-- @name pl.MultiMap
--[[
module ('pl.MultiMap')
]]
local classes = require 'pl.class'
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local List = require 'pl.List'
local index_by,tsort,concat = tablex.index_by,table.sort,table.concat
local append,extend,slice = List.append,List.extend,List.slice
local append = table.insert
local is_type = utils.is_type
local class = require 'pl.class'
local Map = require 'pl.Map'
-- MultiMap is a standard MT
local MultiMap = utils.stdmt.MultiMap
class(Map,nil,MultiMap)
MultiMap._name = 'MultiMap'
function MultiMap:_init (t)
if not t then return end
self:update(t)
end
--- update a MultiMap using a table.
-- @param t either a Multimap or a map-like table.
-- @return the map
function MultiMap:update (t)
utils.assert_arg(1,t,'table')
if Map:class_of(t) then
for k,v in pairs(t) do
self[k] = List()
self[k]:append(v)
end
else
for k,v in pairs(t) do
self[k] = List(v)
end
end
end
--- add a new value to a key. Setting a nil value removes the key.
-- @param key the key
-- @param val the value
-- @return the map
function MultiMap:set (key,val)
if val == nil then
self[key] = nil
else
if not self[key] then
self[key] = List()
end
self[key]:append(val)
end
end
return MultiMap
@@ -0,0 +1,150 @@
--- OrderedMap, a pl.Map which preserves ordering.
-- @class module
-- @name pl.OrderedMap
--[[
module ('pl.OrderedMap')
]]
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local List = require 'pl.List'
local index_by,tsort,concat = tablex.index_by,table.sort,table.concat
local class = require 'pl.class'
local Map = require 'pl.Map'
local OrderedMap = class(Map)
OrderedMap._name = 'OrderedMap'
--- construct an OrderedMap.
-- Will throw an error if the argument is bad.
-- @param t optional initialization table, same as for @{OrderedMap:update}
function OrderedMap:_init (t)
self._keys = List()
if t then
local map,err = self:update(t)
if not map then error(err,2) end
end
end
local assert_arg,raise = utils.assert_arg,utils.raise
--- update an OrderedMap using a table. <br>
-- If the table is itself an OrderedMap, then its entries will be appended. <br>
-- if it s a table of the form <code>{{key1=val1},{key2=val2},...}</code> these will be appended. <br>
-- Otherwise, it is assumed to be a map-like table, and order of extra entries is arbitrary.
-- @param t a table.
-- @return the map, or nil in case of error
-- @return the error message
function OrderedMap:update (t)
assert_arg(1,t,'table')
if OrderedMap:class_of(t) then
for k,v in t:iter() do
self:set(k,v)
end
elseif #t > 0 then -- an array must contain {key=val} tables
if type(t[1]) == 'table' then
for _,pair in ipairs(t) do
local key,value = next(pair)
if not key then return raise 'empty pair initialization table' end
self:set(key,value)
end
else
return raise 'cannot use an array to initialize an OrderedMap'
end
else
for k,v in pairs(t) do
self:set(k,v)
end
end
return self
end
--- set the key's value. This key will be appended at the end of the map. <br>
-- If the value is nil, then the key is removed.
-- @param key the key
-- @param val the value
-- @return the map
function OrderedMap:set (key,val)
if not self[key] and val ~= nil then -- ensure that keys are unique
self._keys:append(key)
elseif val == nil then -- removing a key-value pair
self._keys:remove_value(key)
end
self[key] = val
return self
end
--- insert a key/value pair before a given position.
-- Note: if the map already contains the key, then this effectively
-- moves the item to the new position by first removing at the old position.
-- Has no effect if the key does not exist and val is nil
-- @param pos a position starting at 1
-- @param key the key
-- @param val the value; if nil use the old value
function OrderedMap:insert (pos,key,val)
local oldval = self[key]
val = val or oldval
if oldval then
self._keys:remove_value(key)
end
if val then
self._keys:insert(pos,key)
self[key] = val
end
return self
end
--- return the keys in order.
-- (Not a copy!)
-- @return List
function OrderedMap:keys ()
return self._keys
end
--- return the values in order.
-- this is relatively expensive.
-- @return List
function OrderedMap:values ()
return List(index_by(self,self._keys))
end
--- sort the keys.
-- @param cmp a comparison function as for @{table.sort}
-- @return the map
function OrderedMap:sort (cmp)
tsort(self._keys,cmp)
return self
end
--- iterate over key-value pairs in order.
function OrderedMap:iter ()
local i = 0
local keys = self._keys
local n,idx = #keys
return function()
i = i + 1
if i > #keys then return nil end
idx = keys[i]
return idx,self[idx]
end
end
function OrderedMap:__tostring ()
local res = {}
for i,v in ipairs(self._keys) do
local val = self[v]
local vs = tostring(val)
if type(val) ~= 'number' then
vs = '"'..vs..'"'
end
res[i] = tostring(v)..'='..vs
end
return '{'..concat(res,',')..'}'
end
return OrderedMap
@@ -0,0 +1,127 @@
---- A Set class.
-- @class module
-- @name pl.Set
--[[
module ('pl.Set')
]]
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local stdmt = utils.stdmt
local tmakeset,deepcompare,merge,keys,difference,tupdate = tablex.makeset,tablex.deepcompare,tablex.merge,tablex.keys,tablex.difference,tablex.update
local Map = stdmt.Map
local Set = stdmt.Set
local List = stdmt.List
local class = require 'pl.class'
-- the Set class --------------------
class(Map,nil,Set)
local function makeset (t)
return setmetatable(t,Set)
end
--- create a set. <br>
-- @param t may be a Set, Map or list-like table.
-- @class function
-- @name Set
function Set:_init (t)
local mt = getmetatable(t)
if mt == Set or mt == Map then
for k in pairs(t) do self[k] = true end
else
for _,v in ipairs(t) do self[v] = true end
end
end
function Set:__tostring ()
return '['..self:keys():join ','..']'
end
--- add a value to a set.
-- @param key a value
function Set:set (key)
self[key] = true
end
--- remove a value from a set.
-- @param key a value
function Set:unset (key)
self[key] = nil
end
--- get a list of the values in a set.
-- @class function
-- @name Set:values
Set.values = Map.keys
--- map a function over the values of a set.
-- @param fn a function
-- @param ... extra arguments to pass to the function.
-- @return a new set
function Set:map (fn,...)
fn = utils.function_arg(1,fn)
local res = {}
for k in pairs(self) do
res[fn(k,...)] = true
end
return makeset(res)
end
--- union of two sets (also +).
-- @param set another set
-- @return a new set
function Set:union (set)
return merge(self,set,true)
end
Set.__add = Set.union
--- intersection of two sets (also *).
-- @param set another set
-- @return a new set
function Set:intersection (set)
return merge(self,set,false)
end
Set.__mul = Set.intersection
--- new set with elements in the set that are not in the other (also -).
-- @param set another set
-- @return a new set
function Set:difference (set)
return difference(self,set,false)
end
Set.__sub = Set.difference
-- a new set with elements in _either_ the set _or_ other but not both (also ^).
-- @param set another set
-- @return a new set
function Set:symmetric_difference (set)
return difference(self,set,true)
end
Set.__pow = Set.symmetric_difference
--- is the first set a subset of the second?.
-- @return true or false
function Set:issubset (set)
for k in pairs(self) do
if not set[k] then return false end
end
return true
end
Set.__lt = Set.subset
--- is the set empty?.
-- @return true or false
function Set:issempty ()
return next(self) == nil
end
--- are the sets disjoint? (no elements in common).
-- Uses naive definition, i.e. that intersection is empty
-- @param set another set
-- @return true or false
function Set:isdisjoint (set)
return self:intersection(set):isempty()
end
return Set
@@ -0,0 +1,143 @@
--- Application support functions.
-- <p>See <a href="../../index.html#app">the Guide</a>
-- @class module
-- @name pl.app
local io,package,require = _G.io, _G.package, _G.require
local utils = require 'pl.utils'
local path = require 'pl.path'
local lfs = require 'lfs'
local app = {}
local function check_script_name ()
if _G.arg == nil then error('no command line args available\nWas this run from a main script?') end
return _G.arg[0]
end
--- add the current script's path to the Lua module path.
-- Applies to both the source and the binary module paths. It makes it easy for
-- the main file of a multi-file program to access its modules in the same directory.
-- `base` allows these modules to be put in a specified subdirectory, to allow for
-- cleaner deployment and resolve potential conflicts between a script name and its
-- library directory.
-- @param base optional base directory.
-- @return the current script's path with a trailing slash
function app.require_here (base)
local p = path.dirname(check_script_name())
if not path.isabs(p) then
p = path.join(lfs.currentdir(),p)
end
if p:sub(-1,-1) ~= path.sep then
p = p..path.sep
end
if base then
p = p..base..path.sep
end
local so_ext = path.is_windows and 'dll' or 'so'
local lsep = package.path:find '^;' and '' or ';'
local csep = package.cpath:find '^;' and '' or ';'
package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
return p
end
--- return a suitable path for files private to this application.
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
-- SNAME is the name of the script without .lua extension.
-- @param file a filename (w/out path)
-- @return a full pathname, or nil
-- @return 'cannot create' error
function app.appfile (file)
local sname = path.basename(check_script_name())
local name,ext = path.splitext(sname)
local dir = path.join(path.expanduser('~'),'.'..name)
if not path.isdir(dir) then
local ret = lfs.mkdir(dir)
if not ret then return utils.raise ('cannot create '..dir) end
end
return path.join(dir,file)
end
--- return string indicating operating system.
-- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux')
function app.platform()
if path.is_windows then
return 'Windows'
else
local f = io.popen('uname')
local res = f:read()
if res == 'Darwin' then res = 'OSX' end
f:close()
return res
end
end
--- parse command-line arguments into flags and parameters.
-- Understands GNU-style command-line flags; short (-f) and long (--flag).
-- These may be given a value with either '=' or ':' (-k:2,--alpha=3.2,-n2);
-- note that a number value can be given without a space.
-- Multiple short args can be combined like so: (-abcd).
-- @param args an array of strings (default is the global 'arg')
-- @param flags_with_values any flags that take values, e.g. <code>{out=true}</code>
-- @return a table of flags (flag=value pairs)
-- @return an array of parameters
-- @raise if args is nil, then the global `args` must be available!
function app.parse_args (args,flags_with_values)
if not args then
args = _G.arg
if not args then error "Not in a main program: 'arg' not found" end
end
flags_with_values = flags_with_values or {}
local _args = {}
local flags = {}
local i = 1
while i <= #args do
local a = args[i]
local v = a:match('^-(.+)')
local is_long
if v then -- we have a flag
if v:find '^-' then
is_long = true
v = v:sub(2)
end
if flags_with_values[v] then
if i == #_args or args[i+1]:find '^-' then
return utils.raise ("no value for '"..v.."'")
end
flags[v] = args[i+1]
i = i + 1
else
-- a value can be indicated with = or :
local var,val = utils.splitv (v,'[=:]')
var = var or v
val = val or true
if not is_long then
if #var > 1 then
if var:find '.%d+' then -- short flag, number value
val = var:sub(2)
var = var:sub(1,1)
else -- multiple short flags
for i = 1,#var do
flags[var:sub(i,i)] = true
end
val = nil -- prevents use of var as a flag below
end
else -- single short flag (can have value, defaults to true)
val = val or true
end
end
if val then
flags[var] = val
end
end
else
_args[#_args+1] = a
end
i = i + 1
end
return flags,_args
end
return app
@@ -0,0 +1,391 @@
--- Operations on two-dimensional arrays.
-- @class module
-- @name pl.array2d
local require, type,tonumber,assert,tostring,io,ipairs,string,table =
_G.require, _G.type,_G.tonumber,_G.assert,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table
local ops = require 'pl.operator'
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local imap,tmap,reduce,keys,tmap2,tset,index_by = tablex.imap,tablex.map,tablex.reduce,tablex.keys,tablex.map2,tablex.set,tablex.index_by
local remove = table.remove
local perm = require 'pl.permute'
local splitv,fprintf,assert_arg = utils.splitv,utils.fprintf,utils.assert_arg
local byte = string.byte
local stdout = io.stdout
--[[
module ('pl.array2d',utils._module)
]]
local array2d = {}
--- extract a column from the 2D array.
-- @param a 2d array
-- @param key an index or key
-- @return 1d array
function array2d.column (a,key)
assert_arg(1,a,'table')
return imap(ops.index,a,key)
end
local column = array2d.column
--- map a function over a 2D array
-- @param f a function of at least one argument
-- @param a 2d array
-- @param arg an optional extra argument to be passed to the function.
-- @return 2d array
function array2d.map (f,a,arg)
assert_arg(1,a,'table')
f = utils.function_arg(1,f)
return imap(function(row) return imap(f,row,arg) end, a)
end
--- reduce the rows using a function.
-- @param f a binary function
-- @param a 2d array
-- @return 1d array
-- @see pl.tablex.reduce
function array2d.reduce_rows (f,a)
assert_arg(1,a,'table')
return tmap(function(row) return reduce(f,row) end, a)
end
--- reduce the columns using a function.
-- @param f a binary function
-- @param a 2d array
-- @return 1d array
-- @see pl.tablex.reduce
function array2d.reduce_cols (f,a)
assert_arg(1,a,'table')
return tmap(function(c) return reduce(f,column(a,c)) end, keys(a[1]))
end
--- reduce a 2D array into a scalar, using two operations.
-- @param opc operation to reduce the final result
-- @param opr operation to reduce the rows
-- @param a 2D array
function array2d.reduce2 (opc,opr,a)
assert_arg(3,a,'table')
local tmp = array2d.reduce_rows(opr,a)
return reduce(opc,tmp)
end
local function dimension (t)
return type(t[1])=='table' and 2 or 1
end
--- map a function over two arrays.
-- They can be both or either 2D arrays
-- @param f function of at least two arguments
-- @param ad order of first array
-- @param bd order of second array
-- @param a 1d or 2d array
-- @param b 1d or 2d array
-- @param arg optional extra argument to pass to function
-- @return 2D array, unless both arrays are 1D
function array2d.map2 (f,ad,bd,a,b,arg)
assert_arg(1,a,'table')
assert_arg(2,b,'table')
f = utils.function_arg(1,f)
--local ad,bd = dimension(a),dimension(b)
if ad == 1 and bd == 2 then
return imap(function(row)
return tmap2(f,a,row,arg)
end, b)
elseif ad == 2 and bd == 1 then
return imap(function(row)
return tmap2(f,row,b,arg)
end, a)
elseif ad == 1 and bd == 1 then
return tmap2(f,a,b)
elseif ad == 2 and bd == 2 then
return tmap2(function(rowa,rowb)
return tmap2(f,rowa,rowb,arg)
end, a,b)
end
end
--- cartesian product of two 1d arrays.
-- @param f a function of 2 arguments
-- @param t1 a 1d table
-- @param t2 a 1d table
-- @return 2d table
-- @usage product('..',{1,2},{'a','b'}) == {{'1a','2a'},{'1b','2b'}}
function array2d.product (f,t1,t2)
f = utils.function_arg(1,f)
assert_arg(2,t1,'table')
assert_arg(3,t2,'table')
local res, map = {}, tablex.map
for i,v in ipairs(t2) do
res[i] = map(f,t1,v)
end
return res
end
--- flatten a 2D array.
-- (this goes over columns first.)
-- @param t 2d table
-- @return a 1d table
-- @usage flatten {{1,2},{3,4},{5,6}} == {1,2,3,4,5,6}
function array2d.flatten (t)
local res = {}
local k = 1
for _,a in ipairs(t) do -- for all rows
for i = 1,#a do
res[k] = a[i]
k = k + 1
end
end
return res
end
--- swap two rows of an array.
-- @param t a 2d array
-- @param i1 a row index
-- @param i2 a row index
function array2d.swap_rows (t,i1,i2)
assert_arg(1,t,'table')
t[i1],t[i2] = t[i2],t[i1]
end
--- swap two columns of an array.
-- @param t a 2d array
-- @param j1 a column index
-- @param j2 a column index
function array2d.swap_cols (t,j1,j2)
assert_arg(1,t,'table')
for i = 1,#t do
local row = t[i]
row[j1],row[j2] = row[j2],row[j1]
end
end
--- extract the specified rows.
-- @param t 2d array
-- @param ridx a table of row indices
function array2d.extract_rows (t,ridx)
return index_by(t,ridx)
end
--- extract the specified columns.
-- @param t 2d array
-- @param cidx a table of column indices
function array2d.extract_cols (t,cidx)
assert_arg(1,t,'table')
for i = 1,#t do
t[i] = index_by(t[i],cidx)
end
end
--- remove a row from an array.
-- @class function
-- @name array2d.remove_row
-- @param t a 2d array
-- @param i a row index
array2d.remove_row = remove
--- remove a column from an array.
-- @param t a 2d array
-- @param j a column index
function array2d.remove_col (t,j)
assert_arg(1,t,'table')
for i = 1,#t do
remove(t[i],j)
end
end
local Ai = byte 'A'
local function _parse (s)
local c,r
if s:sub(1,1) == 'R' then
r,c = s:match 'R(%d+)C(%d+)'
r,c = tonumber(r),tonumber(c)
else
c,r = s:match '(.)(.)'
c = byte(c) - byte 'A' + 1
r = tonumber(r)
end
assert(c ~= nil and r ~= nil,'bad cell specifier: '..s)
return r,c
end
--- parse a spreadsheet range.
-- The range can be specified either as 'A1:B2' or 'R1C1:R2C2';
-- a special case is a single element (e.g 'A1' or 'R1C1')
-- @param s a range.
-- @return start col
-- @return start row
-- @return end col
-- @return end row
function array2d.parse_range (s)
if s:find ':' then
local start,finish = splitv(s,':')
local i1,j1 = _parse(start)
local i2,j2 = _parse(finish)
return i1,j1,i2,j2
else -- single value
local i,j = _parse(s)
return i,j
end
end
--- get a slice of a 2D array using spreadsheet range notation. @see parse_range
-- @param t a 2D array
-- @param rstr range expression
-- @return a slice
-- @see array2d.parse_range
-- @see array2d.slice
function array2d.range (t,rstr)
assert_arg(1,t,'table')
local i1,j1,i2,j2 = array2d.parse_range(rstr)
if i2 then
return array2d.slice(t,i1,j1,i2,j2)
else -- single value
return t[i1][j1]
end
end
local function default_range (t,i1,j1,i2,j2)
assert(t and type(t)=='table','not a table')
i1,j1 = i1 or 1, j1 or 1
i2,j2 = i2 or #t, j2 or #t[1]
return i1,j1,i2,j2
end
--- get a slice of a 2D array. Note that if the specified range has
-- a 1D result, the rank of the result will be 1.
-- @param t a 2D array
-- @param i1 start row (default 1)
-- @param j1 start col (default 1)
-- @param i2 end row (default N)
-- @param j2 end col (default M)
-- @return an array, 2D in general but 1D in special cases.
function array2d.slice (t,i1,j1,i2,j2)
assert_arg(1,t,'table')
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
local res = {}
for i = i1,i2 do
local val
local row = t[i]
if j1 == j2 then
val = row[j1]
else
val = {}
for j = j1,j2 do
val[#val+1] = row[j]
end
end
res[#res+1] = val
end
if i1 == i2 then res = res[1] end
return res
end
--- set a specified range of an array to a value.
-- @param t a 2D array
-- @param value the value
-- @param i1 start row (default 1)
-- @param j1 start col (default 1)
-- @param i2 end row (default N)
-- @param j2 end col (default M)
function array2d.set (t,value,i1,j1,i2,j2)
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
for i = i1,i2 do
tset(t[i],value)
end
end
--- write a 2D array to a file.
-- @param t a 2D array
-- @param f a file object (default stdout)
-- @param fmt a format string (default is just to use tostring)
-- @param i1 start row (default 1)
-- @param j1 start col (default 1)
-- @param i2 end row (default N)
-- @param j2 end col (default M)
function array2d.write (t,f,fmt,i1,j1,i2,j2)
assert_arg(1,t,'table')
f = f or stdout
local rowop
if fmt then
rowop = function(row,j) fprintf(f,fmt,row[j]) end
else
rowop = function(row,j) f:write(tostring(row[j]),' ') end
end
local function newline()
f:write '\n'
end
array2d.forall(t,rowop,newline,i1,j1,i2,j2)
end
--- perform an operation for all values in a 2D array.
-- @param t 2D array
-- @param row_op function to call on each value
-- @param end_row_op function to call at end of each row
-- @param i1 start row (default 1)
-- @param j1 start col (default 1)
-- @param i2 end row (default N)
-- @param j2 end col (default M)
function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2)
assert_arg(1,t,'table')
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
for i = i1,i2 do
local row = t[i]
for j = j1,j2 do
row_op(row,j)
end
if end_row_op then end_row_op(i) end
end
end
--- iterate over all elements in a 2D array, with optional indices.
-- @param a 2D array
-- @param indices with indices (default false)
-- @param i1 start row (default 1)
-- @param j1 start col (default 1)
-- @param i2 end row (default N)
-- @param j2 end col (default M)
-- @return either value or i,j,value depending on indices
function array2d.iter (a,indices,i1,j1,i2,j2)
assert_arg(1,a,'table')
local norowset = not (i2 and j2)
i1,j1,i2,j2 = default_range(a,i1,j1,i2,j2)
local n,i,j = i2-i1+1,i1-1,j1-1
local row,nr = nil,0
local onr = j2 - j1 + 1
return function()
j = j + 1
if j > nr then
j = j1
i = i + 1
if i > i2 then return nil end
row = a[i]
nr = norowset and #row or onr
end
if indices then
return i,j,row[j]
else
return row[j]
end
end
end
function array2d.columns (a)
assert_arg(1,a,'table')
local n = a[1][1]
local i = 0
return function()
i = i + 1
if i > n then return nil end
return column(a,i)
end
end
return array2d
@@ -0,0 +1,155 @@
--- Provides a reuseable and convenient framework for creating classes in Lua.
-- Two possible notations: <br> <code> B = class(A) </code> or <code> class.B(A) </code>. <br>
-- <p>The latter form creates a named class. </p>
-- See the Guide for further <a href="../../index.html#class">discussion</a>
-- @module pl.class
local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type =
_G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type
-- this trickery is necessary to prevent the inheritance of 'super' and
-- the resulting recursive call problems.
local function call_ctor (c,obj,...)
-- nice alias for the base class ctor
local base = rawget(c,'_base')
if base then obj.super = rawget(base,'_init') end
local res = c._init(obj,...)
obj.super = nil
return res
end
local function 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 function class_of(klass,obj)
if type(klass) ~= 'table' or not rawget(klass,'is_a') then return false end
return klass.is_a(obj,klass)
end
local function _class_tostring (obj)
local mt = obj._class
local name = rawget(mt,'_name')
setmetatable(obj,nil)
local str = tostring(obj)
setmetatable(obj,mt)
if name then str = name ..str:gsub('table','') end
return str
end
local function tupdate(td,ts)
for k,v in pairs(ts) do
td[k] = v
end
end
local function _class(base,c_arg,c)
c = c or {} -- a new class instance, which is the metatable for all objects of this type
-- the class will be the metatable for all its objects,
-- and they will look up their methods in it.
local mt = {} -- a metatable for the class instance
if type(base) == 'table' then
-- our new class is a shallow copy of the base class!
tupdate(c,base)
c._base = base
-- inherit the 'not found' handler, if present
if rawget(c,'_handler') then mt.__index = c._handler end
elseif base ~= nil then
error("must derive from a table type",3)
end
c.__index = c
setmetatable(c,mt)
c._init = nil
if base and rawget(base,'_class_init') then
base._class_init(c,c_arg)
end
-- expose a ctor which can be called by <classname>(<args>)
mt.__call = function(class_tbl,...)
local obj = {}
setmetatable(obj,c)
if rawget(c,'_init') then -- explicit constructor
local res = call_ctor(c,obj,...)
if res then -- _if_ a ctor returns a value, it becomes the object...
obj = res
setmetatable(obj,c)
end
elseif base and rawget(base,'_init') then -- default constructor
-- make sure that any stuff from the base class is initialized!
call_ctor(base,obj,...)
end
if base and rawget(base,'_post_init') then
base._post_init(obj)
end
if not rawget(c,'__tostring') then
c.__tostring = _class_tostring
end
return obj
end
-- Call Class.catch to set a handler for methods/properties not found in the class!
c.catch = function(handler)
c._handler = handler
mt.__index = handler
end
c.is_a = is_a
c.class_of = class_of
c._class = c
-- any object can have a specified delegate which is called with unrecognized methods
-- if _handler exists and obj[key] is nil, then pass onto handler!
c.delegate = function(self,obj)
mt.__index = function(tbl,key)
local method = obj[key]
if method then
return function(self,...)
return method(obj,...)
end
elseif self._handler then
return self._handler(tbl,key)
end
end
end
return c
end
--- create a new class, derived from a given base class. <br>
-- Supporting two class creation syntaxes:
-- either <code>Name = class(base)</code> or <code>class.Name(base)</code>
-- @class function
-- @name class
-- @param base optional base class
-- @param c_arg optional parameter to class ctor
-- @param c optional table to be used as class
local class
class = setmetatable({},{
__call = function(fun,...)
return _class(...)
end,
__index = function(tbl,key)
if key == 'class' then
io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n')
return class
end
local env = _G
return function(...)
local c = _class(...)
c._name = key
rawset(env,key,c)
return c
end
end
})
return class
@@ -0,0 +1,288 @@
--- List comprehensions implemented in Lua. <p>
--
-- See the <a href="http://lua-users.org/wiki/ListComprehensions">wiki page</a>
-- <pre class=example>
-- local C= require 'pl.comprehension' . new()
--
-- C ('x for x=1,10') ()
-- ==> {1,2,3,4,5,6,7,8,9,10}
-- C 'x^2 for x=1,4' ()
-- ==> {1,4,9,16}
-- C '{x,x^2} for x=1,4' ()
-- ==> {{1,1},{2,4},{3,9},{4,16}}
-- C '2*x for x' {1,2,3}
-- ==> {2,4,6}
-- dbl = C '2*x for x'
-- dbl {10,20,30}
-- ==> {20,40,60}
-- C 'x for x if x % 2 == 0' {1,2,3,4,5}
-- ==> {2,4}
-- C '{x,y} for x = 1,2 for y = 1,2' ()
-- ==> {{1,1},{1,2},{2,1},{2,2}}
-- C '{x,y} for x for y' ({1,2},{10,20})
-- ==> {{1,10},{1,20},{2,10},{2,20}}
-- assert(C 'sum(x^2 for x)' {2,3,4} == 2^2+3^2+4^2)
-- </pre>
--
-- <p> (c) 2008 David Manura. Licensed under the same terms as Lua (MIT license).
-- <p> -- See <a href="../../index.html#T31">the Guide</a>
-- @class module
-- @name pl.comprehension
local utils = require 'pl.utils'
--~ local _VERSION, assert, getfenv, ipairs, load, math, pcall, require, setmetatable, table, tonumber =
--~ _G._VERSION, _G.assert, _G.getfenv, _G.ipairs, _G.load, _G.math, _G.pcall, _G.require, _G.setmetatable, _G.table, _G.tonumber
local status,lb = pcall(require, "pl.luabalanced")
if not status then
lb = require 'luabalanced'
end
local math_max = math.max
local table_concat = table.concat
-- fold operations
-- http://en.wikipedia.org/wiki/Fold_(higher-order_function)
local ops = {
list = {init=' {} ', accum=' __result[#__result+1] = (%s) '},
table = {init=' {} ', accum=' local __k, __v = %s __result[__k] = __v '},
sum = {init=' 0 ', accum=' __result = __result + (%s) '},
min = {init=' nil ', accum=' local __tmp = %s ' ..
' if __result then if __tmp < __result then ' ..
'__result = __tmp end else __result = __tmp end '},
max = {init=' nil ', accum=' local __tmp = %s ' ..
' if __result then if __tmp > __result then ' ..
'__result = __tmp end else __result = __tmp end '},
}
-- Parses comprehension string expr.
-- Returns output expression list <out> string, array of for types
-- ('=', 'in' or nil) <fortypes>, array of input variable name
-- strings <invarlists>, array of input variable value strings
-- <invallists>, array of predicate expression strings <preds>,
-- operation name string <opname>, and number of placeholder
-- parameters <max_param>.
--
-- The is equivalent to the mathematical set-builder notation:
--
-- <opname> { <out> | <invarlist> in <invallist> , <preds> }
--
-- @usage "x^2 for x" -- array values
-- @usage "x^2 for x=1,10,2" -- numeric for
-- @usage "k^v for k,v in pairs(_1)" -- iterator for
-- @usage "(x+y)^2 for x for y if x > y" -- nested
--
local function parse_comprehension(expr)
local t = {}
local pos = 1
-- extract opname (if exists)
local opname
local tok, post = expr:match('^%s*([%a_][%w_]*)%s*%(()', pos)
local pose = #expr + 1
if tok then
local tok2, posb = lb.match_bracketed(expr, post-1)
assert(tok2, 'syntax error')
if expr:match('^%s*$', posb) then
opname = tok
pose = posb - 1
pos = post
end
end
opname = opname or "list"
-- extract out expression list
local out; out, pos = lb.match_explist(expr, pos)
assert(out, "syntax error: missing expression list")
out = table_concat(out, ', ')
-- extract "for" clauses
local fortypes = {}
local invarlists = {}
local invallists = {}
while 1 do
local post = expr:match('^%s*for%s+()', pos)
if not post then break end
pos = post
-- extract input vars
local iv; iv, pos = lb.match_namelist(expr, pos)
assert(#iv > 0, 'syntax error: zero variables')
for _,ident in ipairs(iv) do
assert(not ident:match'^__',
"identifier " .. ident .. " may not contain __ prefix")
end
invarlists[#invarlists+1] = iv
-- extract '=' or 'in' (optional)
local fortype, post = expr:match('^(=)%s*()', pos)
if not fortype then fortype, post = expr:match('^(in)%s+()', pos) end
if fortype then
pos = post
-- extract input value range
local il; il, pos = lb.match_explist(expr, pos)
assert(#il > 0, 'syntax error: zero expressions')
assert(fortype ~= '=' or #il == 2 or #il == 3,
'syntax error: numeric for requires 2 or three expressions')
fortypes[#invarlists] = fortype
invallists[#invarlists] = il
else
fortypes[#invarlists] = false
invallists[#invarlists] = false
end
end
assert(#invarlists > 0, 'syntax error: missing "for" clause')
-- extract "if" clauses
local preds = {}
while 1 do
local post = expr:match('^%s*if%s+()', pos)
if not post then break end
pos = post
local pred; pred, pos = lb.match_expression(expr, pos)
assert(pred, 'syntax error: predicated expression not found')
preds[#preds+1] = pred
end
-- extract number of parameter variables (name matching "_%d+")
local stmp = ''; lb.gsub(expr, function(u, sin) -- strip comments/strings
if u == 'e' then stmp = stmp .. ' ' .. sin .. ' ' end
end)
local max_param = 0; stmp:gsub('[%a_][%w_]*', function(s)
local s = s:match('^_(%d+)$')
if s then max_param = math_max(max_param, tonumber(s)) end
end)
if pos ~= pose then
assert(false, "syntax error: unrecognized " .. expr:sub(pos))
end
--DEBUG:
--print('----\n', string.format("%q", expr), string.format("%q", out), opname)
--for k,v in ipairs(invarlists) do print(k,v, invallists[k]) end
--for k,v in ipairs(preds) do print(k,v) end
return out, fortypes, invarlists, invallists, preds, opname, max_param
end
-- Create Lua code string representing comprehension.
-- Arguments are in the form returned by parse_comprehension.
local function code_comprehension(
out, fortypes, invarlists, invallists, preds, opname, max_param
)
local op = assert(ops[opname])
local code = op.accum:gsub('%%s', out)
for i=#preds,1,-1 do local pred = preds[i]
code = ' if ' .. pred .. ' then ' .. code .. ' end '
end
for i=#invarlists,1,-1 do
if not fortypes[i] then
local arrayname = '__in' .. i
local idx = '__idx' .. i
code =
' for ' .. idx .. ' = 1, #' .. arrayname .. ' do ' ..
' local ' .. invarlists[i][1] .. ' = ' .. arrayname .. '['..idx..'] ' ..
code .. ' end '
else
code =
' for ' ..
table_concat(invarlists[i], ', ') ..
' ' .. fortypes[i] .. ' ' ..
table_concat(invallists[i], ', ') ..
' do ' .. code .. ' end '
end
end
code = ' local __result = ( ' .. op.init .. ' ) ' .. code
return code
end
-- Convert code string represented by code_comprehension
-- into Lua function. Also must pass ninputs = #invarlists,
-- max_param, and invallists (from parse_comprehension).
-- Uses environment env.
local function wrap_comprehension(code, ninputs, max_param, invallists, env)
assert(ninputs > 0)
local ts = {}
for i=1,max_param do
ts[#ts+1] = '_' .. i
end
for i=1,ninputs do
if not invallists[i] then
local name = '__in' .. i
ts[#ts+1] = name
end
end
if #ts > 0 then
code = ' local ' .. table_concat(ts, ', ') .. ' = ... ' .. code
end
code = code .. ' return __result '
--print('DEBUG:', code)
local f, err = utils.load(code,'tmp','t',env)
if not f then assert(false, err .. ' with generated code ' .. code) end
return f
end
-- Build Lua function from comprehension string.
-- Uses environment env.
local function build_comprehension(expr, env)
local out, fortypes, invarlists, invallists, preds, opname, max_param
= parse_comprehension(expr)
local code = code_comprehension(
out, fortypes, invarlists, invallists, preds, opname, max_param)
local f = wrap_comprehension(code, #invarlists, max_param, invallists, env)
return f
end
-- Creates new comprehension cache.
-- Any list comprehension function created are set to the environment
-- env (defaults to caller of new).
local function new(env)
-- Note: using a single global comprehension cache would have had
-- security implications (e.g. retrieving cached functions created
-- in other environments).
-- The cache lookup function could have instead been written to retrieve
-- the caller's environment, lookup up the cache private to that
-- environment, and then looked up the function in that cache.
-- That would avoid the need for this <new> call to
-- explicitly manage caches; however, that might also have an undue
-- performance penalty.
if not env then
env = getfenv(2)
end
local mt = {}
local cache = setmetatable({}, mt)
-- Index operator builds, caches, and returns Lua function
-- corresponding to comprehension expression string.
--
-- Example: f = comprehension['x^2 for x']
--
function mt:__index(expr)
local f = build_comprehension(expr, env)
self[expr] = f -- cache
return f
end
-- Convenience syntax.
-- Allows comprehension 'x^2 for x' instead of comprehension['x^2 for x'].
mt.__call = mt.__index
cache.new = new
return cache
end
local comprehension = {}
comprehension.new = new
return comprehension
@@ -0,0 +1,169 @@
--- Reads configuration files into a Lua table. <p>
-- Understands INI files, classic Unix config files, and simple
-- delimited columns of values. <p>
-- <pre class=example>
-- # test.config
-- # Read timeout in seconds
-- read.timeout=10
-- # Write timeout in seconds
-- write.timeout=5
-- #acceptable ports
-- ports = 1002,1003,1004
--
-- -- readconfig.lua
-- require 'pl'
-- local t = config.read 'test.config'
-- print(pretty.write(t))
--
-- ### output #####
-- {
-- ports = {
-- 1002,
-- 1003,
-- 1004
-- },
-- write_timeout = 5,
-- read_timeout = 10
-- }
-- </pre>
-- See the Guide for further <a href="../../index.html#config">discussion</a>
-- @class module
-- @name pl.config
local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table
local function split(s,re)
local res = {}
local t_insert = table.insert
re = '[^'..re..']+'
for k in s:gmatch(re) do t_insert(res,k) end
return res
end
local function strip(s)
return s:gsub('^%s+',''):gsub('%s+$','')
end
local function strip_quotes (s)
return s:gsub("['\"](.*)['\"]",'%1')
end
local config = {}
--- like io.lines(), but allows for lines to be continued with '\'.
-- @param file a file-like object (anything where read() returns the next line) or a filename.
-- Defaults to stardard input.
-- @return an iterator over the lines, or nil
-- @return error 'not a file-like object' or 'file is nil'
function config.lines(file)
local f,openf,err
local line = ''
if type(file) == 'string' then
f,err = io.open(file,'r')
if not f then return nil,err end
openf = true
else
f = file or io.stdin
if not file.read then return nil, 'not a file-like object' end
end
if not f then return nil, 'file is nil' end
return function()
local l = f:read()
while l do
-- does the line end with '\'?
local i = l:find '\\%s*$'
if i then -- if so,
line = line..l:sub(1,i-1)
elseif line == '' then
return l
else
l = line..l
line = ''
return l
end
l = f:read()
end
if openf then f:close() end
end
end
--- read a configuration file into a table
-- @param file either a file-like object or a string, which must be a filename
-- @param cnfg a configuration table that may contain these fields:
-- <ul>
-- <li> variablilize make names into valid Lua identifiers (default true)</li>
-- <li> convert_numbers try to convert values into numbers (default true)</li>
-- <li> trim_space ensure that there is no starting or trailing whitespace with values (default true)</li>
-- <li> trim_quotes remove quotes from strings (default false)</li>
-- <li> list_delim delimiter to use when separating columns (default ',')</li>
-- </ul>
-- @return a table containing items, or nil
-- @return error message (same as @{config.lines}
function config.read(file,cnfg)
local f,openf,err
cnfg = cnfg or {}
local function check_cnfg (var,def)
local val = cnfg[var]
if val == nil then return def else return val end
end
local t = {}
local top_t = t
local variablilize = check_cnfg ('variabilize',true)
local list_delim = check_cnfg('list_delim',',')
local convert_numbers = check_cnfg('convert_numbers',true)
local trim_space = check_cnfg('trim_space',true)
local trim_quotes = check_cnfg('trim_quotes',false)
local ignore_assign = check_cnfg('ignore_assign',false)
local function process_name(key)
if variablilize then
key = key:gsub('[^%w]','_')
end
return key
end
local function process_value(value)
if list_delim and value:find(list_delim) then
value = split(value,list_delim)
for i,v in ipairs(value) do
value[i] = process_value(v)
end
elseif convert_numbers and value:find('^[%d%+%-]') then
local val = tonumber(value)
if val then value = val end
end
if type(value) == 'string' then
if trim_space then value = strip(value) end
if trim_quotes then value = strip_quotes(value) end
end
return value
end
local iter,err = config.lines(file)
if not iter then return nil,err end
for line in iter do
-- strips comments
local ci = line:find('%s*[#;]')
if ci then line = line:sub(1,ci-1) end
-- and ignore blank lines
if line:find('^%s*$') then
elseif line:find('^%[') then -- section!
local section = process_name(line:match('%[([^%]]+)%]'))
t = top_t
t[section] = {}
t = t[section]
else
local i1,i2 = line:find('%s*=%s*')
if i1 and not ignore_assign then -- key,value assignment
local key = process_name(line:sub(1,i1-1))
local value = process_value(line:sub(i2+1))
t[key] = value
else -- a plain list of values...
t[#t+1] = process_value(line)
end
end
end
return top_t
end
return config
@@ -0,0 +1,588 @@
--- Reading and querying simple tabular data.
-- <pre class=example>
-- data.read 'test.txt'
-- ==> {{10,20},{2,5},{40,50},fieldnames={'x','y'},delim=','}
-- </pre>
-- Provides a way of creating basic SQL-like queries.
-- <pre class=example>
-- require 'pl'
-- local d = data.read('xyz.txt')
-- local q = d:select('x,y,z where x > 3 and z < 2 sort by y')
-- for x,y,z in q do
-- print(x,y,z)
-- end
-- </pre>
-- <p>See <a href="../../index.html#data">the Guide</a>
-- @class module
-- @name pl.data
local utils = require 'pl.utils'
local _DEBUG = rawget(_G,'_DEBUG')
local patterns,function_arg,usplit = utils.patterns,utils.function_arg,utils.split
local append,concat = table.insert,table.concat
local gsub = string.gsub
local io = io
local _G,print,loadstring,type,tonumber,ipairs,setmetatable,pcall,error,setfenv = _G,print,loadstring,type,tonumber,ipairs,setmetatable,pcall,error,setfenv
--[[
module ('pl.data',utils._module)
]]
local data = {}
local parse_select
local function count(s,chr)
chr = utils.escape(chr)
local _,cnt = s:gsub(chr,' ')
return cnt
end
local function rstrip(s)
return s:gsub('%s+$','')
end
local function make_list(l)
return setmetatable(l,utils.stdmt.List)
end
local function split(s,delim)
return make_list(usplit(s,delim))
end
local function map(fun,t)
local res = {}
for i = 1,#t do
append(res,fun(t[i]))
end
return res
end
local function find(t,v)
for i = 1,#t do
if v == t[i] then return i end
end
end
local DataMT = {
column_by_name = function(self,name)
if type(name) == 'number' then
name = '$'..name
end
local arr = {}
for res in data.query(self,name) do
append(arr,res)
end
return make_list(arr)
end,
copy_select = function(self,condn)
condn = parse_select(condn,self)
local iter = data.query(self,condn)
local res = {}
local row = make_list{iter()}
while #row > 0 do
append(res,row)
row = make_list{iter()}
end
res.delim = self.delim
return data.new(res,split(condn.fields,','))
end,
column_names = function(self)
return self.fieldnames
end,
}
DataMT.__index = DataMT
--- return a particular column as a list of values (Method). <br>
-- @param name either name of column, or numerical index.
-- @class function
-- @name Data.column_by_name
--- return a query iterator on this data object (Method). <br>
-- @param condn the query expression
-- @class function
-- @name Data.select
-- @see data.query
--- return a new data object based on this query (Method). <br>
-- @param condn the query expression
-- @class function
-- @name Data.copy_select
--- return the field names of this data object (Method). <br>
-- @class function
-- @name Data.column_names
--- write out a row (Method). <br>
-- @param f file-like object
-- @class function
-- @name Data.write_row
--- write data out to file(Method). <br>
-- @param f file-like object
-- @class function
-- @name Data.write
-- [guessing delimiter] We check for comma, tab and spaces in that order.
-- [issue] any other delimiters to be checked?
local delims = {',','\t',' ',';'}
local function guess_delim (line)
for _,delim in ipairs(delims) do
if count(line,delim) > 0 then
return delim == ' ' and '%s+' or delim
end
end
return ' '
end
-- [file parameter] If it's a string, we try open as a filename. If nil, then
-- either stdin or stdout depending on the mode. Otherwise, check if this is
-- a file-like object (implements read or write depending)
local function open_file (f,mode)
local opened, err
local reading = mode == 'r'
if type(f) == 'string' then
if f == 'stdin' then
f = io.stdin
elseif f == 'stdout' then
f = io.stdout
else
f,err = io.open(f,mode)
if not f then return nil,err end
opened = true
end
end
if f and ((reading and not f.read) or (not reading and not f.write)) then
return nil, "not a file-like object"
end
return f,nil,opened
end
local function all_n ()
end
--- read a delimited file in a Lua table.
-- By default, attempts to treat first line as separated list of fieldnames.
-- @param file a filename or a file-like object (default stdin)
-- @param cnfg options table: can override delim (a string pattern), fieldnames (a list),
-- specify no_convert (default is to convert), numfields (indices of columns known
-- to be numbers) and thousands_dot (thousands separator in Excel CSV is '.')
function data.read(file,cnfg)
local convert,err,opened
local D = {}
if not cnfg then cnfg = {} end
local f,err,opened = open_file(file,'r')
if not f then return nil, err end
local thousands_dot = cnfg.thousands_dot
local function try_tonumber(x)
if thousands_dot then x = x:gsub('%.(...)','%1') end
return tonumber(x)
end
local line = f:read()
if not line then return nil, "empty file" end
-- first question: what is the delimiter?
D.delim = cnfg.delim and cnfg.delim or guess_delim(line)
local delim = D.delim
local collect_end = cnfg.last_field_collect
local numfields = cnfg.numfields
-- some space-delimited data starts with a space. This should not be a column,
-- although it certainly would be for comma-separated, etc.
local strip
if delim == '%s+' and line:find(delim) == 1 then
strip = function(s) return s:gsub('^%s+','') end
line = strip(line)
end
-- first line will usually be field names. Unless fieldnames are specified,
-- we check if it contains purely numerical values for the case of reading
-- plain data files.
if not cnfg.fieldnames then
local fields = split(line,delim)
local nums = map(tonumber,fields)
if #nums == #fields then
convert = tonumber
append(D,nums)
numfields = {}
for i = 1,#nums do numfields[i] = i end
else
cnfg.fieldnames = fields
end
line = f:read()
if strip then line = strip(line) end
elseif type(cnfg.fieldnames) == 'string' then
cnfg.fieldnames = split(cnfg.fieldnames,delim)
end
-- at this point, the column headers have been read in. If the first
-- row consisted of numbers, it has already been added to the dataset.
if cnfg.fieldnames then
D.fieldnames = cnfg.fieldnames
-- [conversion] unless @no_convert, we need the numerical field indices
-- of the first data row. Can also be specified by @numfields.
if not cnfg.no_convert then
if not numfields then
numfields = {}
local fields = split(line,D.delim)
for i = 1,#fields do
if tonumber(fields[i]) then
append(numfields,i)
end
end
end
if #numfields > 0 then -- there are numerical fields
-- note that using dot as the thousands separator (@thousands_dot)
-- requires a special conversion function!
convert = thousands_dot and try_tonumber or tonumber
end
end
end
-- keep going until finished
while line do
if not line:find ('^%s*$') then
if strip then line = strip(line) end
local fields = split(line,delim)
if convert then
for k = 1,#numfields do
local i = numfields[k]
local val = convert(fields[i])
if val == nil then
return nil, "not a number: "..fields[i]
else
fields[i] = val
end
end
end
-- [collecting end field] If @last_field_collect then we will collect
-- all extra space-delimited fields into a single last field.
if collect_end and #fields > #D.fieldnames then
local ends,N = {},#D.fieldnames
for i = N+1,#fields do
append(ends,fields[i])
end
ends = concat(ends,' ')
local cfields = {}
for i = 1,N do cfields[i] = fields[i] end
cfields[N] = cfields[N]..' '..ends
fields = cfields
end
append(D,fields)
end
line = f:read()
end
if opened then f:close() end
if delim == '%s+' then D.delim = ' ' end
if not D.fieldnames then D.fieldnames = {} end
return data.new(D)
end
local function write_row (data,f,row)
f:write(concat(row,data.delim),'\n')
end
DataMT.write_row = write_row
local function write (data,file)
local f,err,opened = open_file(file,'w')
if not f then return nil, err end
if #data.fieldnames > 0 then
f:write(concat(data.fieldnames,data.delim),'\n')
end
for i = 1,#data do
write_row(data,f,data[i])
end
if opened then f:close() end
end
DataMT.write = write
local function massage_fieldnames (fields)
-- [fieldnames must be valid Lua identifiers] fix 0.8 was %A
for i = 1,#fields do
fields[i] = fields[i]:gsub('%W','_')
end
end
--- create a new dataset from a table of rows. <br>
-- Can specify the fieldnames, else the table must have a field called
-- 'fieldnames', which is either a string of delimiter-separated names,
-- or a table of names. <br>
-- If the table does not have a field called 'delim', then an attempt will be
-- made to guess it from the fieldnames string, defaults otherwise to tab.
-- @param d the table.
-- @param fieldnames optional fieldnames
-- @return the table.
function data.new (d,fieldnames)
d.fieldnames = d.fieldnames or fieldnames
if not d.delim and type(d.fieldnames) == 'string' then
d.delim = guess_delim(d.fieldnames)
d.fieldnames = split(d.fieldnames,d.delim)
end
d.fieldnames = make_list(d.fieldnames)
massage_fieldnames(d.fieldnames)
setmetatable(d,DataMT)
-- a query with just the fieldname will return a sequence
-- of values, which seq.copy turns into a table.
return d
end
local sorted_query = [[
return function (t)
local i = 0
local v
local ls = {}
for i,v in ipairs(t) do
if CONDITION then
ls[#ls+1] = v
end
end
table.sort(ls,function(v1,v2)
return SORT_EXPR
end)
local n = #ls
return function()
i = i + 1
v = ls[i]
if i > n then return end
return FIELDLIST
end
end
]]
-- question: is this optimized case actually worth the extra code?
local simple_query = [[
return function (t)
local n = #t
local i = 0
local v
return function()
repeat
i = i + 1
v = t[i]
until i > n or CONDITION
if i > n then return end
return FIELDLIST
end
end
]]
local function is_string (s)
return type(s) == 'string'
end
local field_error
local function fieldnames_as_string (data)
return concat(data.fieldnames,',')
end
local function massage_fields(data,f)
local idx
if f:find '^%d+$' then
idx = tonumber(f)
else
idx = find(data.fieldnames,f)
end
if idx then
return 'v['..idx..']'
else
field_error = f..' not found in '..fieldnames_as_string(data)
return f
end
end
local List = require 'pl.List'
local function process_select (data,parms)
--- preparing fields ----
local res,ret
field_error = nil
local fields = parms.fields
local numfields = fields:find '%$' or #data.fieldnames == 0
if fields:find '^%s*%*%s*' then
if not numfields then
fields = fieldnames_as_string(data)
else
local ncol = #data[1]
fields = {}
for i = 1,ncol do append(fields,'$'..i) end
fields = concat(fields,',')
end
end
local idpat = patterns.IDEN
if numfields then
idpat = '%$(%d+)'
else
-- massage field names to replace non-identifier chars
fields = rstrip(fields):gsub('[^,%w]','_')
end
local massage_fields = utils.bind1(massage_fields,data)
ret = gsub(fields,idpat,massage_fields)
if field_error then return nil,field_error end
parms.fields = fields
parms.proc_fields = ret
parms.where = parms.where or 'true'
if is_string(parms.where) then
parms.where = gsub(parms.where,idpat,massage_fields)
field_error = nil
end
return true
end
parse_select = function(s,data)
local endp
local parms = {}
local w1,w2 = s:find('where ')
local s1,s2 = s:find('sort by ')
if w1 then -- where clause!
endp = (s1 or 0)-1
parms.where = s:sub(w2+1,endp)
end
if s1 then -- sort by clause (must be last!)
parms.sort_by = s:sub(s2+1)
end
endp = (w1 or s1 or 0)-1
parms.fields = s:sub(1,endp)
local status,err = process_select(data,parms)
if not status then return nil,err
else return parms end
end
--- create a query iterator from a select string.
-- Select string has this format: <br>
-- FIELDLIST [ where LUA-CONDN [ sort by FIELD] ]<br>
-- FIELDLIST is a comma-separated list of valid fields, or '*'. <br> <br>
-- The condition can also be a table, with fields 'fields' (comma-sep string or
-- table), 'sort_by' (string) and 'where' (Lua expression string or function)
-- @param data table produced by read
-- @param condn select string or table
-- @param context a list of tables to be searched when resolving functions
-- @param return_row if true, wrap the results in a row table
-- @return an iterator over the specified fields, or nil
-- @return an error message
function data.query(data,condn,context,return_row)
local err
if is_string(condn) then
condn,err = parse_select(condn,data)
if not condn then return nil,err end
elseif type(condn) == 'table' then
if type(condn.fields) == 'table' then
condn.fields = concat(condn.fields,',')
end
if not condn.proc_fields then
local status,err = process_select(data,condn)
if not status then return nil,err end
end
else
return nil, "condition must be a string or a table"
end
local query, k
if condn.sort_by then -- use sorted_query
query = sorted_query
else
query = simple_query
end
local fields = condn.proc_fields or condn.fields
if return_row then
fields = '{'..fields..'}'
end
query,k = query:gsub('FIELDLIST',fields)
if is_string(condn.where) then
query = query:gsub('CONDITION',condn.where)
condn.where = nil
else
query = query:gsub('CONDITION','_condn(v)')
condn.where = function_arg(0,condn.where,'condition.where must be callable')
end
if condn.sort_by then
local expr,sort_var,sort_dir
local sort_by = condn.sort_by
local i1,i2 = sort_by:find('%s+')
if i1 then
sort_var,sort_dir = sort_by:sub(1,i1-1),sort_by:sub(i2+1)
else
sort_var = sort_by
sort_dir = 'asc'
end
if sort_var:match '^%$' then sort_var = sort_var:sub(2) end
sort_var = massage_fields(data,sort_var)
if field_error then return nil,field_error end
if sort_dir == 'asc' then
sort_dir = '<'
else
sort_dir = '>'
end
expr = ('%s %s %s'):format(sort_var:gsub('v','v1'),sort_dir,sort_var:gsub('v','v2'))
query = query:gsub('SORT_EXPR',expr)
end
if condn.where then
query = 'return function(_condn) '..query..' end'
end
if _DEBUG then print(query) end
local fn,err = loadstring(query,'tmp')
if not fn then return nil,err end
fn = fn() -- get the function
if condn.where then
fn = fn(condn.where)
end
local qfun = fn(data)
if context then
-- [specifying context for condition] @context is a list of tables which are
-- 'injected'into the condition's custom context
append(context,_G)
local lookup = {}
setfenv(qfun,lookup)
setmetatable(lookup,{
__index = function(tbl,key)
-- _G.print(tbl,key)
for k,t in ipairs(context) do
if t[key] then return t[key] end
end
end
})
end
return qfun
end
DataMT.select = data.query
DataMT.select_row = function(d,condn,context)
return data.query(d,condn,context,true)
end
--- Filter input using a query.
-- @param Q a query string
-- @param infile filename or file-like object
-- @param outfile filename or file-like object
-- @param dont_fail true if you want to return an error, not just fail
function data.filter (Q,infile,outfile,dont_fail)
local err
local d = data.read(infile or 'stdin')
local out = open_file(outfile or 'stdout')
local iter,err = d:select(Q)
local delim = d.delim
if not iter then
err = 'error: '..err
if dont_fail then
return nil,err
else
utils.quit(1,err)
end
end
while true do
local res = {iter()}
if #res == 0 then break end
out:write(concat(res,delim),'\n')
end
end
return data
@@ -0,0 +1,478 @@
--- Useful functions for getting directory contents and matching them against wildcards.
-- @class module
-- @name pl.dir
local utils = require 'pl.utils'
local path = require 'pl.path'
local is_windows = path.is_windows
local tablex = require 'pl.tablex'
local ldir = path.dir
local chdir = path.chdir
local mkdir = path.mkdir
local rmdir = path.rmdir
local sub = string.sub
local os,pcall,ipairs,pairs,require,setmetatable,_G = os,pcall,ipairs,pairs,require,setmetatable,_G
local remove = os.remove
local append = table.insert
local wrap = coroutine.wrap
local yield = coroutine.yield
local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise
local List = utils.stdmt.List
--[[
module ('pl.dir',utils._module)
]]
local dir = {}
local function assert_dir (n,val)
assert_arg(n,val,'string',path.isdir,'not a directory')
end
local function assert_file (n,val)
assert_arg(n,val,'string',path.isfile,'not a file')
end
local function filemask(mask)
mask = utils.escape(mask)
return mask:gsub('%%%*','.+'):gsub('%%%?','.')..'$'
end
--- does the filename match the shell pattern?.
-- (cf. fnmatch.fnmatch in Python, 11.8)
-- @param file A file name
-- @param pattern A shell pattern
-- @return true or false
-- @raise file and pattern must be strings
function dir.fnmatch(file,pattern)
assert_string(1,file)
assert_string(2,pattern)
return path.normcase(file):find(filemask(pattern)) ~= nil
end
--- return a list of all files which match the pattern.
-- (cf. fnmatch.filter in Python, 11.8)
-- @param files A table containing file names
-- @param pattern A shell pattern.
-- @return list of files
-- @raise file and pattern must be strings
function dir.filter(files,pattern)
assert_arg(1,files,'table')
assert_string(2,pattern)
local res = {}
local mask = filemask(pattern)
for i,f in ipairs(files) do
if f:find(mask) then append(res,f) end
end
return setmetatable(res,List)
end
local function _listfiles(dir,filemode,match)
local res = {}
local check = utils.choose(filemode,path.isfile,path.isdir)
if not dir then dir = '.' end
for f in ldir(dir) do
if f ~= '.' and f ~= '..' then
local p = path.join(dir,f)
if check(p) and (not match or match(p)) then
append(res,p)
end
end
end
return setmetatable(res,List)
end
--- return a list of all files in a directory which match the a shell pattern.
-- @param dir A directory. If not given, all files in current directory are returned.
-- @param mask A shell pattern. If not given, all files are returned.
-- @return lsit of files
-- @raise dir and mask must be strings
function dir.getfiles(dir,mask)
assert_dir(1,dir)
assert_string(2,mask)
local match
if mask then
mask = filemask(mask)
match = function(f)
return f:find(mask)
end
end
return _listfiles(dir,true,match)
end
--- return a list of all subdirectories of the directory.
-- @param dir A directory
-- @return a list of directories
-- @raise dir must be a string
function dir.getdirectories(dir)
assert_dir(1,dir)
return _listfiles(dir,false)
end
local function quote_argument (f)
f = path.normcase(f)
if f:find '%s' then
return '"'..f..'"'
else
return f
end
end
local alien,ffi,ffi_checked,CopyFile,MoveFile,GetLastError,win32_errors,cmd_tmpfile
local function execute_command(cmd,parms)
if not cmd_tmpfile then cmd_tmpfile = path.tmpname () end
local err = path.is_windows and ' > ' or ' 2> '
cmd = cmd..' '..parms..err..cmd_tmpfile
local ret = utils.execute(cmd)
if not ret then
return false,(utils.readfile(cmd_tmpfile):gsub('\n(.*)',''))
else
return true
end
end
local function find_ffi_copyfile ()
if not ffi_checked then
ffi_checked = true
local res
res,alien = pcall(require,'alien')
if not res then
alien = nil
res, ffi = pcall(require,'ffi')
end
if not res then
ffi = nil
return
end
else
return
end
if alien then
-- register the Win32 CopyFile and MoveFile functions
local kernel = alien.load('kernel32.dll')
CopyFile = kernel.CopyFileA
CopyFile:types{'string','string','int',ret='int',abi='stdcall'}
MoveFile = kernel.MoveFileA
MoveFile:types{'string','string',ret='int',abi='stdcall'}
GetLastError = kernel.GetLastError
GetLastError:types{ret ='int', abi='stdcall'}
elseif ffi then
ffi.cdef [[
int CopyFileA(const char *src, const char *dest, int iovr);
int MoveFileA(const char *src, const char *dest);
int GetLastError();
]]
CopyFile = ffi.C.CopyFileA
MoveFile = ffi.C.MoveFileA
GetLastError = ffi.C.GetLastError
end
win32_errors = {
ERROR_FILE_NOT_FOUND = 2,
ERROR_PATH_NOT_FOUND = 3,
ERROR_ACCESS_DENIED = 5,
ERROR_WRITE_PROTECT = 19,
ERROR_BAD_UNIT = 20,
ERROR_NOT_READY = 21,
ERROR_WRITE_FAULT = 29,
ERROR_READ_FAULT = 30,
ERROR_SHARING_VIOLATION = 32,
ERROR_LOCK_VIOLATION = 33,
ERROR_HANDLE_DISK_FULL = 39,
ERROR_BAD_NETPATH = 53,
ERROR_NETWORK_BUSY = 54,
ERROR_DEV_NOT_EXIST = 55,
ERROR_FILE_EXISTS = 80,
ERROR_OPEN_FAILED = 110,
ERROR_INVALID_NAME = 123,
ERROR_BAD_PATHNAME = 161,
ERROR_ALREADY_EXISTS = 183,
}
end
local function two_arguments (f1,f2)
return quote_argument(f1)..' '..quote_argument(f2)
end
local function file_op (is_copy,src,dest,flag)
if flag == 1 and path.exists(dest) then
return false,"cannot overwrite destination"
end
if is_windows then
-- if we haven't tried to load Alien/LuaJIT FFI before, then do so
find_ffi_copyfile()
-- fallback if there's no Alien, just use DOS commands *shudder*
-- 'rename' involves a copy and then deleting the source.
if not CopyFile then
src = path.normcase(src)
dest = path.normcase(dest)
local cmd = is_copy and 'copy' or 'rename'
local res, err = execute_command('copy',two_arguments(src,dest))
if not res then return nil,err end
if not is_copy then
return execute_command('del',quote_argument(src))
end
else
if path.isdir(dest) then
dest = path.join(dest,path.basename(src))
end
local ret
if is_copy then ret = CopyFile(src,dest,flag)
else ret = MoveFile(src,dest) end
if ret == 0 then
local err = GetLastError()
for name,value in pairs(win32_errors) do
if value == err then return false,name end
end
return false,"Error #"..err
else return true
end
end
else -- for Unix, just use cp for now
return execute_command(is_copy and 'cp' or 'mv',
two_arguments(src,dest))
end
end
--- copy a file.
-- @param src source file
-- @param dest destination file or directory
-- @param flag true if you want to force the copy (default)
-- @return true if operation succeeded
-- @raise src and dest must be strings
function dir.copyfile (src,dest,flag)
assert_string(1,src)
assert_string(2,dest)
flag = flag==nil or flag
return file_op(true,src,dest,flag and 0 or 1)
end
--- move a file.
-- @param src source file
-- @param dest destination file or directory
-- @return true if operation succeeded
-- @raise src and dest must be strings
function dir.movefile (src,dest)
assert_string(1,src)
assert_string(2,dest)
return file_op(false,src,dest,0)
end
local function _dirfiles(dir,attrib)
local dirs = {}
local files = {}
for f in ldir(dir) do
if f ~= '.' and f ~= '..' then
local p = path.join(dir,f)
local mode = attrib(p,'mode')
if mode=='directory' then
append(dirs,f)
else
append(files,f)
end
end
end
return setmetatable(dirs,List),setmetatable(files,List)
end
local function _walker(root,bottom_up,attrib)
local dirs,files = _dirfiles(root,attrib)
if not bottom_up then yield(root,dirs,files) end
for i,d in ipairs(dirs) do
_walker(root..path.sep..d,bottom_up,attrib)
end
if bottom_up then yield(root,dirs,files) end
end
--- return an iterator which walks through a directory tree starting at root.
-- The iterator returns (root,dirs,files)
-- Note that dirs and files are lists of names (i.e. you must say path.join(root,d)
-- to get the actual full path)
-- If bottom_up is false (or not present), then the entries at the current level are returned
-- before we go deeper. This means that you can modify the returned list of directories before
-- continuing.
-- This is a clone of os.walk from the Python libraries.
-- @param root A starting directory
-- @param bottom_up False if we start listing entries immediately.
-- @param follow_links follow symbolic links
-- @return an iterator returning root,dirs,files
-- @raise root must be a string
function dir.walk(root,bottom_up,follow_links)
assert_string(1,root)
if not path.isdir(root) then return raise 'not a directory' end
local attrib
if path.is_windows or not follow_links then
attrib = path.attrib
else
attrib = path.link_attrib
end
return wrap(function () _walker(root,bottom_up,attrib) end)
end
--- remove a whole directory tree.
-- @param fullpath A directory path
-- @return true or nil
-- @return error if failed
-- @raise fullpath must be a string
function dir.rmtree(fullpath)
assert_string(1,fullpath)
if not path.isdir(fullpath) then return raise 'not a directory' end
if path.islink(fullpath) then return false,'will not follow symlink' end
for root,dirs,files in dir.walk(fullpath,true) do
for i,f in ipairs(files) do
remove(path.join(root,f))
end
rmdir(root)
end
return true
end
local dirpat
if path.is_windows then
dirpat = '(.+)\\[^\\]+$'
else
dirpat = '(.+)/[^/]+$'
end
local _makepath
function _makepath(p)
-- windows root drive case
if p:find '^%a:[\\]*$' then
return true
end
if not path.isdir(p) then
local subp = p:match(dirpat)
if not _makepath(subp) then return raise ('cannot create '..subp) end
return mkdir(p)
else
return true
end
end
--- create a directory path.
-- This will create subdirectories as necessary!
-- @param p A directory path
-- @return a valid created path
-- @raise p must be a string
function dir.makepath (p)
assert_string(1,p)
return _makepath(path.normcase(path.abspath(p)))
end
--- clone a directory tree. Will always try to create a new directory structure
-- if necessary.
-- @param path1 the base path of the source tree
-- @param path2 the new base path for the destination
-- @param file_fun an optional function to apply on all files
-- @param verbose an optional boolean to control the verbosity of the output.
-- It can also be a logging function that behaves like print()
-- @return true, or nil
-- @return error message, or list of failed directory creations
-- @return list of failed file operations
-- @raise path1 and path2 must be strings
-- @usage clonetree('.','../backup',copyfile)
function dir.clonetree (path1,path2,file_fun,verbose)
assert_string(1,path1)
assert_string(2,path2)
if verbose == true then verbose = print end
local abspath,normcase,isdir,join = path.abspath,path.normcase,path.isdir,path.join
local faildirs,failfiles = {},{}
if not isdir(path1) then return raise 'source is not a valid directory' end
path1 = abspath(normcase(path1))
path2 = abspath(normcase(path2))
if verbose then verbose('normalized:',path1,path2) end
-- particularly NB that the new path isn't fully contained in the old path
if path1 == path2 then return raise "paths are the same" end
local i1,i2 = path2:find(path1,1,true)
if i2 == #path1 and path2:sub(i2+1,i2+1) == path.sep then
return raise 'destination is a subdirectory of the source'
end
local cp = path.common_prefix (path1,path2)
local idx = #cp
if idx == 0 then -- no common path, but watch out for Windows paths!
if path1:sub(2,2) == ':' then idx = 3 end
end
for root,dirs,files in dir.walk(path1) do
local opath = path2..root:sub(idx)
if verbose then verbose('paths:',opath,root) end
if not isdir(opath) then
local ret = dir.makepath(opath)
if not ret then append(faildirs,opath) end
if verbose then verbose('creating:',opath,ret) end
end
if file_fun then
for i,f in ipairs(files) do
local p1 = join(root,f)
local p2 = join(opath,f)
local ret = file_fun(p1,p2)
if not ret then append(failfiles,p2) end
if verbose then
verbose('files:',p1,p2,ret)
end
end
end
end
return true,faildirs,failfiles
end
--- return an iterator over all entries in a directory tree
-- @param d a directory
-- @return an iterator giving pathname and mode (true for dir, false otherwise)
-- @raise d must be a non-empty string
function dir.dirtree( d )
assert( d and d ~= "", "directory parameter is missing or empty" )
local exists, isdir = path.exists, path.isdir
local sep = path.sep
local last = sub ( d, -1 )
if last == sep or last == '/' then
d = sub( d, 1, -2 )
end
local function yieldtree( dir )
for entry in ldir( dir ) do
if entry ~= "." and entry ~= ".." then
entry = dir .. sep .. entry
if exists(entry) then -- Just in case a symlink is broken.
local is_dir = isdir(entry)
yield( entry, is_dir )
if is_dir then
yieldtree( entry )
end
end
end
end
end
return wrap( function() yieldtree( d ) end )
end
--- Recursively returns all the file starting at <i>path</i>. It can optionally take a shell pattern and
-- only returns files that match <i>pattern</i>. If a pattern is given it will do a case insensitive search.
-- @param start_path {string} A directory. If not given, all files in current directory are returned.
-- @param pattern {string} A shell pattern. If not given, all files are returned.
-- @return Table containing all the files found recursively starting at <i>path</i> and filtered by <i>pattern</i>.
-- @raise start_path must be a string
function dir.getallfiles( start_path, pattern )
assert( type( start_path ) == "string", "bad argument #1 to 'GetAllFiles' (Expected string but recieved " .. type( start_path ) .. ")" )
pattern = pattern or ""
local files = {}
local normcase = path.normcase
for filename, mode in dir.dirtree( start_path ) do
if not mode then
local mask = filemask( pattern )
if normcase(filename):find( mask ) then
files[#files + 1] = filename
end
end
end
return files
end
return dir
@@ -0,0 +1,69 @@
--- File manipulation functions: reading, writing, moving and copying.
-- @class module
-- @name pl.file
local os = os
local utils = require 'pl.utils'
local dir = require 'pl.dir'
local path = require 'pl.path'
--[[
module ('pl.file',utils._module)
]]
local file = {}
--- return the contents of a file as a string
-- @class function
-- @name file.read
-- @param filename The file path
-- @return file contents
file.read = utils.readfile
--- write a string to a file
-- @class function
-- @name file.write
-- @param filename The file path
-- @param str The string
file.write = utils.writefile
--- copy a file.
-- @class function
-- @name file.copy
-- @param src source file
-- @param dest destination file
-- @param flag true if you want to force the copy (default)
-- @return true if operation succeeded
file.copy = dir.copyfile
--- move a file.
-- @class function
-- @name file.move
-- @param src source file
-- @param dest destination file
-- @return true if operation succeeded, else false and the reason for the error.
file.move = dir.movefile
--- Return the time of last access as the number of seconds since the epoch.
-- @class function
-- @name file.access_time
-- @param path A file path
file.access_time = path.getatime
---Return when the file was created.
-- @class function
-- @name file.creation_time
-- @param path A file path
file.creation_time = path.getctime
--- Return the time of last modification
-- @class function
-- @name file.modified_time
-- @param path A file path
file.modified_time = path.getmtime
--- Delete a file
-- @class function
-- @name file.delete
-- @param path A file path
file.delete = os.remove
return file
@@ -0,0 +1,379 @@
--- Functional helpers like composition, binding and placeholder expressions.
-- Placeholder expressions are useful for short anonymous functions, and were
-- inspired by the Boost Lambda library.
-- <pre class=example>
-- utils.import 'pl.func'
-- ls = List{10,20,30}
-- = ls:map(_1+1)
-- {11,21,31}
-- </pre>
-- They can also be used to <em>bind</em> particular arguments of a function.
-- <pre class = example>
-- p = bind(print,'start>',_0)
-- p(10,20,30)
-- start> 10 20 30
-- </pre>
-- See <a href="../../index.html#func">the Guide</a>
-- @class module
-- @name pl.func
local type,select,setmetatable,getmetatable,rawset = type,select,setmetatable,getmetatable,rawset
local concat,append = table.concat,table.insert
local max = math.max
local print,tostring = print,tostring
local pairs,ipairs,loadstring,rawget,unpack = pairs,ipairs,loadstring,rawget,unpack
local _G = _G
local utils = require 'pl.utils'
local tablex = require 'pl.tablex'
local map = tablex.map
local _DEBUG = rawget(_G,'_DEBUG')
local assert_arg = utils.assert_arg
--[[
module ('pl.func',utils._module)
]]
local func = {}
-- metatable for Placeholder Expressions (PE)
local _PEMT = {}
local function P (t)
setmetatable(t,_PEMT)
return t
end
func.PE = P
local function isPE (obj)
return getmetatable(obj) == _PEMT
end
func.isPE = isPE
-- construct a placeholder variable (e.g _1 and _2)
local function PH (idx)
return P {op='X',repr='_'..idx, index=idx}
end
-- construct a constant placeholder variable (e.g _C1 and _C2)
local function CPH (idx)
return P {op='X',repr='_C'..idx, index=idx}
end
func._1,func._2,func._3,func._4,func._5 = PH(1),PH(2),PH(3),PH(4),PH(5)
func._0 = P{op='X',repr='...',index=0}
function func.Var (name)
local ls = utils.split(name,'[%s,]+')
local res = {}
for _,n in ipairs(ls) do
append(res,P{op='X',repr=n,index=0})
end
return unpack(res)
end
function func._ (value)
return P{op='X',repr=value,index='wrap'}
end
local repr
func.Nil = func.Var 'nil'
function _PEMT.__index(obj,key)
return P{op='[]',obj,key}
end
function _PEMT.__call(fun,...)
return P{op='()',fun,...}
end
function _PEMT.__tostring (e)
return repr(e)
end
function _PEMT.__unm(arg)
return P{op='-',arg}
end
function func.Not (arg)
return P{op='not',arg}
end
function func.Len (arg)
return P{op='#',arg}
end
local function binreg(context,t)
for name,op in pairs(t) do
rawset(context,name,function(x,y)
return P{op=op,x,y}
end)
end
end
local function import_name (name,fun,context)
rawset(context,name,function(...)
return P{op='()',fun,...}
end)
end
local imported_functions = {}
local function is_global_table (n)
return type(_G[n]) == 'table'
end
--- wrap a table of functions. This makes them available for use in
-- placeholder expressions.
-- @param tname a table name
-- @param context context to put results, defaults to environment of caller
function func.import(tname,context)
assert_arg(1,tname,'string',is_global_table,'arg# 1: not a name of a global table')
local t = _G[tname]
context = context or _G
for name,fun in pairs(t) do
import_name(name,fun,context)
imported_functions[fun] = name
end
end
--- register a function for use in placeholder expressions.
-- @param fun a function
-- @param name an optional name
-- @return a placeholder functiond
function func.register (fun,name)
assert_arg(1,fun,'function')
if name then
assert_arg(2,name,'string')
imported_functions[fun] = name
end
return function(...)
return P{op='()',fun,...}
end
end
function func.lookup_imported_name (fun)
return imported_functions[fun]
end
local function _arg(...) return ... end
function func.Args (...)
return P{op='()',_arg,...}
end
-- binary and unary operators, with their precedences (see 2.5.6)
local operators = {
['or'] = 0,
['and'] = 1,
['=='] = 2, ['~='] = 2, ['<'] = 2, ['>'] = 2, ['<='] = 2, ['>='] = 2,
['..'] = 3,
['+'] = 4, ['-'] = 4,
['*'] = 5, ['/'] = 5, ['%'] = 5,
['not'] = 6, ['#'] = 6, ['-'] = 6,
['^'] = 7
}
-- comparisons (as prefix functions)
binreg (func,{And='and',Or='or',Eq='==',Lt='<',Gt='>',Le='<=',Ge='>='})
-- standard binary operators (as metamethods)
binreg (_PEMT,{__add='+',__sub='-',__mul='*',__div='/',__mod='%',__pow='^',__concat='..'})
binreg (_PEMT,{__eq='=='})
--- all elements of a table except the first.
-- @param ls a list-like table.
function func.tail (ls)
assert_arg(1,ls,'table')
local res = {}
for i = 2,#ls do
append(res,ls[i])
end
return res
end
--- create a string representation of a placeholder expression.
-- @param e a placeholder expression
-- @param lastpred not used
function repr (e,lastpred)
local tail = func.tail
if isPE(e) then
local pred = operators[e.op]
local ls = map(repr,e,pred)
if pred then --unary or binary operator
if #ls ~= 1 then
local s = concat(ls,' '..e.op..' ')
if lastpred and lastpred > pred then
s = '('..s..')'
end
return s
else
return e.op..' '..ls[1]
end
else -- either postfix, or a placeholder
if e.op == '[]' then
return ls[1]..'['..ls[2]..']'
elseif e.op == '()' then
local fn
if ls[1] ~= nil then -- was _args, undeclared!
fn = ls[1]
else
fn = ''
end
return fn..'('..concat(tail(ls),',')..')'
else
return e.repr
end
end
elseif type(e) == 'string' then
return '"'..e..'"'
elseif type(e) == 'function' then
local name = func.lookup_imported_name(e)
if name then return name else return tostring(e) end
else
return tostring(e) --should not really get here!
end
end
func.repr = repr
-- collect all the non-PE values in this PE into vlist, and replace each occurence
-- with a constant PH (_C1, etc). Return the maximum placeholder index found.
local collect_values
function collect_values (e,vlist)
if isPE(e) then
if e.op ~= 'X' then
local m = 0
for i,subx in ipairs(e) do
local pe = isPE(subx)
if pe then
if subx.op == 'X' and subx.index == 'wrap' then
subx = subx.repr
pe = false
else
m = max(m,collect_values(subx,vlist))
end
end
if not pe then
append(vlist,subx)
e[i] = CPH(#vlist)
end
end
return m
else -- was a placeholder, it has an index...
return e.index
end
else -- plain value has no placeholder dependence
return 0
end
end
func.collect_values = collect_values
--- instantiate a PE into an actual function. First we find the largest placeholder used,
-- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace
-- any non-PE values from the PE, and build up a constant binding list.
-- Finally, the expression can be compiled, and e.__PE_function is set.
-- @param e a placeholder expression
-- @return a function
function func.instantiate (e)
local consts,values,parms = {},{},{}
local rep, err, fun
local n = func.collect_values(e,values)
for i = 1,#values do
append(consts,'_C'..i)
if _DEBUG then print(i,values[i]) end
end
for i =1,n do
append(parms,'_'..i)
end
consts = concat(consts,',')
parms = concat(parms,',')
rep = repr(e)
local fstr = ('return function(%s) return function(%s) return %s end end'):format(consts,parms,rep)
if _DEBUG then print(fstr) end
fun,err = loadstring(fstr,'fun')
if not fun then return nil,err end
fun = fun() -- get wrapper
fun = fun(unpack(values)) -- call wrapper (values could be empty)
e.__PE_function = fun
return fun
end
--- instantiate a PE unless it has already been done.
-- @param e a placeholder expression
-- @return the function
function func.I(e)
if rawget(e,'__PE_function') then
return e.__PE_function
else return func.instantiate(e)
end
end
utils.add_function_factory(_PEMT,func.I)
--- bind the first parameter of the function to a value.
-- @class function
-- @name func.curry
-- @param fn a function of one or more arguments
-- @param p a value
-- @return a function of one less argument
-- @usage (curry(math.max,10))(20) == math.max(10,20)
func.curry = utils.bind1
--- create a function which chains two functions.
-- @param f a function of at least one argument
-- @param g a function of at least one argument
-- @return a function
-- @usage printf = compose(io.write,string.format)
function func.compose (f,g)
return function(...) return f(g(...)) end
end
--- bind the arguments of a function to given values.
-- bind(fn,v,_2) is equivalent to curry(fn,v).
-- @param fn a function of at least one argument
-- @param ... values or placeholder variables
-- @return a function
-- @usage (bind(f,_1,a))(b) == f(a,b)
-- @usage (bind(f,_2,_1))(a,b) == f(b,a)
function func.bind(fn,...)
local args = table.pack(...)
local holders,parms,bvalues,values = {},{},{'fn'},{}
local nv,maxplace,varargs = 1,0,false
for i = 1,args.n do
local a = args[i]
if isPE(a) and a.op == 'X' then
append(holders,a.repr)
maxplace = max(maxplace,a.index)
if a.index == 0 then varargs = true end
else
local v = '_v'..nv
append(bvalues,v)
append(holders,v)
append(values,a)
nv = nv + 1
end
end
for np = 1,maxplace do
append(parms,'_'..np)
end
if varargs then append(parms,'...') end
bvalues = concat(bvalues,',')
parms = concat(parms,',')
holders = concat(holders,',')
local fstr = ([[
return function (%s)
return function(%s) return fn(%s) end
end
]]):format(bvalues,parms,holders)
if _DEBUG then print(fstr) end
local res,err = loadstring(fstr)
res = res()
return res(fn,unpack(values))
end
return func
@@ -0,0 +1,47 @@
--------------
-- entry point for loading all PL libraries only on demand.
-- Requiring 'pl' means that whenever a module is accesssed (e.g. utils.split)
-- then that module is dynamically loaded. The submodules are all brought into
-- the global space.
-- @class module
-- @name pl
local modules = {
utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true,
input=true,seq=true,lexer=true,stringx=true,
config=true,pretty=true,data=true,func=true,text=true,
operator=true,lapp=true,array2d=true,
comprehension=true,xml=true,
test = true, app = true, file = true, class = true, List = true,
Map = true, Set = true, OrderedMap = true, MultiMap = true,
Date = true,
-- classes --
}
_G.utils = require 'pl.utils'
for name,klass in pairs(_G.utils.stdmt) do
klass.__index = function(t,key)
return require ('pl.'..name)[key]
end;
end
local _hook
setmetatable(_G,{
hook = function(handler)
_hook = handler
end,
__index = function(t,name)
local found = modules[name]
-- either true, or the name of the module containing this class.
-- either way, we load the required module and make it globally available.
if found then
-- e..g pretty.dump causes pl.pretty to become available as 'pretty'
rawset(_G,name,require('pl.'..name))
return _G[name]
elseif _hook then
return _hook(t,name)
end
end
})
if _G.PENLIGHT_STRICT then require 'pl.strict' end
@@ -0,0 +1,172 @@
--- Iterators for extracting words or numbers from an input source.
-- <pre class=example>
-- require 'pl'
-- local total,n = <a href="pl.seq.html#sum">seq.sum</a>(input.numbers())
-- print('average',total/n)
-- </pre>
-- <p> See <a href="../../index.html#lexer">here</a>
-- @class module
-- @name pl.input
local strfind = string.find
local strsub = string.sub
local strmatch = string.match
local pairs,type,unpack,tonumber = pairs,type,unpack,tonumber
local utils = require 'pl.utils'
local patterns = utils.patterns
local io = io
local assert_arg = utils.assert_arg
--[[
module ('pl.input',utils._module)
]]
local input = {}
--- create an iterator over all tokens.
-- based on allwords from PiL, 7.1
-- @param getter any function that returns a line of text
-- @param pattern
-- @param fn Optionally can pass a function to process each token as it/s found.
-- @return an iterator
function input.alltokens (getter,pattern,fn)
local line = getter() -- current line
local pos = 1 -- current position in the line
return function () -- iterator function
while line do -- repeat while there are lines
local s, e = strfind(line, pattern, pos)
if s then -- found a word?
pos = e + 1 -- next position is after this token
local res = strsub(line, s, e) -- return the token
if fn then res = fn(res) end
return res
else
line = getter() -- token not found; try next line
pos = 1 -- restart from first position
end
end
return nil -- no more lines: end of traversal
end
end
local alltokens = input.alltokens
-- question: shd this _split_ a string containing line feeds?
--- create a function which grabs the next value from a source. If the source is a string, then the getter
-- will return the string and thereafter return nil. If not specified then the source is assumed to be stdin.
-- @param f a string or a file-like object (i.e. has a read() method which returns the next line)
-- @return a getter function
function input.create_getter(f)
if f then
if type(f) == 'string' then
local ls = utils.split(f,'\n')
local i,n = 0,#ls
return function()
i = i + 1
if i > n then return nil end
return ls[i]
end
else
-- anything that supports the read() method!
if not f.read then error('not a file-like object') end
return function() return f:read() end
end
else
return io.read -- i.e. just read from stdin
end
end
--- generate a sequence of numbers from a source.
-- @param f A source
-- @return An iterator
function input.numbers(f)
return alltokens(input.create_getter(f),
'('..patterns.FLOAT..')',tonumber)
end
--- generate a sequence of words from a source.
-- @param f A source
-- @return An iterator
function input.words(f)
return alltokens(input.create_getter(f),"%w+")
end
local function apply_tonumber (no_fail,...)
local args = {...}
for i = 1,#args do
local n = tonumber(args[i])
if n == nil then
if not no_fail then return nil,args[i] end
else
args[i] = n
end
end
return args
end
--- parse an input source into fields.
-- By default, will fail if it cannot convert a field to a number.
-- @param ids a list of field indices, or a maximum field index
-- @param delim delimiter to parse fields (default space)
-- @param f a source @see create_getter
-- @param opts option table, {no_fail=true}
-- @return an iterator with the field values
-- @usage for x,y in fields {2,3} do print(x,y) end -- 2nd and 3rd fields from stdin
function input.fields (ids,delim,f,opts)
local sep
local s
local getter = input.create_getter(f)
local no_fail = opts and opts.no_fail
local no_convert = opts and opts.no_convert
if not delim or delim == ' ' then
delim = '%s'
sep = '%s+'
s = '%s*'
else
sep = delim
s = ''
end
local max_id = 0
if type(ids) == 'table' then
for i,id in pairs(ids) do
if id > max_id then max_id = id end
end
else
max_id = ids
ids = {}
for i = 1,max_id do ids[#ids+1] = i end
end
local pat = '[^'..delim..']*'
local k = 1
for i = 1,max_id do
if ids[k] == i then
k = k + 1
s = s..'('..pat..')'
else
s = s..pat
end
if i < max_id then
s = s..sep
end
end
local linecount = 1
return function()
local line,results,err
repeat
line = getter()
linecount = linecount + 1
if not line then return nil end
if no_convert then
results = {strmatch(line,s)}
else
results,err = apply_tonumber(no_fail,strmatch(line,s))
if not results then
utils.quit("line "..(linecount-1)..": cannot convert '"..err.."' to number")
end
end
until #results > 0
return unpack(results)
end
end
return input
@@ -0,0 +1,350 @@
--- Simple command-line parsing using human-readable specification.
-- Supports GNU-style parameters.
-- <pre class=example>
-- lapp = require 'pl.lapp'
-- local args = lapp [[
-- Does some calculations
-- -o,--offset (default 0.0) Offset to add to scaled number
-- -s,--scale (number) Scaling factor
-- &lt;number&gt; (number ) Number to be scaled
-- ]]
--
-- print(args.offset + args.scale * args.number)
-- </pre>
-- Lines begining with '-' are flags; there may be a short and a long name;
-- lines begining wih '&lt;var&gt;' are arguments. Anything in parens after
-- the flag/argument is either a default, a type name or a range constraint.
-- <p>See <a href="../../index.html#lapp">the Guide</a>
-- @class module
-- @name pl.lapp
local status,sip = pcall(require,'pl.sip')
if not status then
sip = require 'sip'
end
local match = sip.match_at_start
local append,tinsert = table.insert,table.insert
--[[
module('pl.lapp')
]]
local function lines(s) return s:gmatch('([^\n]*)\n') end
local function lstrip(str) return str:gsub('^%s+','') end
local function strip(str) return lstrip(str):gsub('%s+$','') end
local function at(s,k) return s:sub(k,k) end
local function isdigit(s) return s:find('^%d+$') == 1 end
local lapp = {}
local open_files,parms,aliases,parmlist,usage,windows,script
lapp.callback = false -- keep Strict happy
local filetypes = {
stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'},
stderr = {io.stderr,'file-out'}
}
--- controls whether to dump usage on error.
-- Defaults to true
lapp.show_usage_error = true
--- quit this script immediately.
-- @param msg optional message
-- @param no_usage suppress 'usage' display
function lapp.quit(msg,no_usage)
if msg then
io.stderr:write(msg..'\n\n')
end
if not no_usage then
io.stderr:write(usage)
end
os.exit(1);
end
--- print an error to stderr and quit.
-- @param msg a message
-- @param no_usage suppress 'usage' display
function lapp.error(msg,no_usage)
if not lapp.show_usage_error then
no_usage = true
end
lapp.quit(script..':'..msg,no_usage)
end
--- open a file.
-- This will quit on error, and keep a list of file objects for later cleanup.
-- @param file filename
-- @param opt same as second parameter of <code>io.open</code>
function lapp.open (file,opt)
local val,err = io.open(file,opt)
if not val then lapp.error(err,true) end
append(open_files,val)
return val
end
--- quit if the condition is false.
-- @param condn a condition
-- @param msg an optional message
function lapp.assert(condn,msg)
if not condn then
lapp.error(msg)
end
end
local function range_check(x,min,max,parm)
lapp.assert(min <= x and max >= x,parm..' out of range')
end
local function xtonumber(s)
local val = tonumber(s)
if not val then lapp.error("unable to convert to number: "..s) end
return val
end
local function is_filetype(type)
return type == 'file-in' or type == 'file-out'
end
local types
local function convert_parameter(ps,val)
if ps.converter then
val = ps.converter(val)
end
if ps.type == 'number' then
val = xtonumber(val)
elseif is_filetype(ps.type) then
val = lapp.open(val,(ps.type == 'file-in' and 'r') or 'w' )
elseif ps.type == 'boolean' then
val = true
end
if ps.constraint then
ps.constraint(val)
end
return val
end
--- add a new type to Lapp. These appear in parens after the value like
-- a range constraint, e.g. '<ival> (integer) Process PID'
-- @param name name of type
-- @param converter either a function to convert values, or a Lua type name.
-- @param constraint optional function to verify values, should use lapp.error
-- if failed.
function lapp.add_type (name,converter,constraint)
types[name] = {converter=converter,constraint=constraint}
end
local function force_short(short)
lapp.assert(#short==1,short..": short parameters should be one character")
end
local function process_default (sval)
local val = tonumber(sval)
if val then -- we have a number!
return val,'number'
elseif filetypes[sval] then
local ft = filetypes[sval]
return ft[1],ft[2]
else
if sval:match '^["\']' then sval = sval:sub(2,-2) end
return sval,'string'
end
end
--- process a Lapp options string.
-- Usually called as lapp().
-- @param str the options text
-- @return a table with parameter-value pairs
function lapp.process_options_string(str)
local results = {}
local opts = {at_start=true}
local varargs
open_files = {}
parms = {}
aliases = {}
parmlist = {}
types = {}
local function check_varargs(s)
local res,cnt = s:gsub('^%.%.%.%s*','')
return res, (cnt > 0)
end
local function set_result(ps,parm,val)
if not ps.varargs then
results[parm] = val
else
if not results[parm] then
results[parm] = { val }
else
append(results[parm],val)
end
end
end
usage = str
for line in lines(str) do
local res = {}
local optspec,optparm,i1,i2,defval,vtype,constraint,rest
line = lstrip(line)
local function check(str)
return match(str,line,res)
end
-- flags: either '-<short>', '-<short>,--<long>' or '--<long>'
if check '-$v{short}, --$v{long} $' or check '-$v{short} $' or check '--$v{long} $' then
if res.long then
optparm = res.long
if res.short then aliases[res.short] = optparm end
else
optparm = res.short
end
if res.short then force_short(res.short) end
res.rest, varargs = check_varargs(res.rest)
elseif check '$<{name} $' then -- is it <parameter_name>?
-- so <input file...> becomes input_file ...
optparm,rest = res.name:match '([^%.]+)(.*)'
optparm = optparm:gsub('%A','_')
varargs = rest == '...'
append(parmlist,optparm)
end
if res.rest then -- this is not a pure doc line
line = res.rest
res = {}
-- do we have (default <val>) or (<type>)?
if match('$({def} $',line,res) or match('$({def}',line,res) then
local typespec = strip(res.def)
if match('default $',typespec,res) then
defval,vtype = process_default(res[1])
elseif match('$f{min}..$f{max}',typespec,res) then
local min,max = res.min,res.max
vtype = 'number'
constraint = function(x)
range_check(x,min,max,optparm)
end
else -- () just contains type of required parameter
vtype = typespec
end
else -- must be a plain flag, no extra parameter required
defval = false
vtype = 'boolean'
end
local ps = {
type = vtype,
defval = defval,
required = defval == nil,
comment = res.rest or optparm,
constraint = constraint,
varargs = varargs
}
varargs = nil
if types[vtype] then
local converter = types[vtype].converter
if type(converter) == 'string' then
ps.type = converter
else
ps.converter = converter
end
ps.constraint = types[vtype].constraint
end
parms[optparm] = ps
end
end
-- cool, we have our parms, let's parse the command line args
local iparm = 1
local iextra = 1
local i = 1
local parm,ps,val
while i <= #arg do
local theArg = arg[i]
local res = {}
-- look for a flag, -<short flags> or --<long flag>
if match('--$v{long}',theArg,res) or match('-$v{short}',theArg,res) then
if res.long then -- long option
parm = res.long
elseif #res.short == 1 then
parm = res.short
else
local parmstr = res.short
parm = at(parmstr,1)
if isdigit(at(parmstr,2)) then
-- a short option followed by a digit is an exception (for AW;))
-- push ahead into the arg array
tinsert(arg,i+1,parmstr:sub(2))
else
-- push multiple flags into the arg array!
for k = 2,#parmstr do
tinsert(arg,i+k-1,'-'..at(parmstr,k))
end
end
end
if parm == 'h' or parm == 'help' then
lapp.quit()
end
if aliases[parm] then parm = aliases[parm] end
else -- a parameter
parm = parmlist[iparm]
if not parm then
-- extra unnamed parameters are indexed starting at 1
parm = iextra
ps = { type = 'string' }
parms[parm] = ps
iextra = iextra + 1
else
ps = parms[parm]
end
if not ps.varargs then
iparm = iparm + 1
end
val = theArg
end
ps = parms[parm]
if not ps then lapp.error("unrecognized parameter: "..parm) end
if ps.type ~= 'boolean' then -- we need a value! This should follow
if not val then
i = i + 1
val = arg[i]
end
lapp.assert(val,parm.." was expecting a value")
end
ps.used = true
val = convert_parameter(ps,val)
set_result(ps,parm,val)
if is_filetype(ps.type) then
set_result(ps,parm..'_name',theArg)
end
if lapp.callback then
lapp.callback(parm,theArg,res)
end
i = i + 1
val = nil
end
-- check unused parms, set defaults and check if any required parameters were missed
for parm,ps in pairs(parms) do
if not ps.used then
if ps.required then lapp.error("missing required parameter: "..parm) end
set_result(ps,parm,ps.defval)
end
end
return results
end
if arg then
script = arg[0]:gsub('.+[\\/]',''):gsub('%.%a+$','')
else
script = "inter"
end
setmetatable(lapp, {
__call = function(tbl,str) return lapp.process_options_string(str) end,
})
return lapp
@@ -0,0 +1,461 @@
--- Lexical scanner for creating a sequence of tokens from text. <br>
-- <p><code>lexer.scan(s)</code> returns an iterator over all tokens found in the
-- string <code>s</code>. This iterator returns two values, a token type string
-- (such as 'string' for quoted string, 'iden' for identifier) and the value of the
-- token.
-- <p>
-- Versions specialized for Lua and C are available; these also handle block comments
-- and classify keywords as 'keyword' tokens. For example:
-- <pre class=example>
-- > s = 'for i=1,n do'
-- > for t,v in lexer.lua(s) do print(t,v) end
-- keyword for
-- iden i
-- = =
-- number 1
-- , ,
-- iden n
-- keyword do
-- </pre>
-- See the Guide for further <a href="../../index.html#lexer">discussion</a> <br>
-- @class module
-- @name pl.lexer
local yield,wrap = coroutine.yield,coroutine.wrap
local strfind = string.find
local strsub = string.sub
local append = table.insert
--[[
module ('pl.lexer',utils._module)
]]
local function assert_arg(idx,val,tp)
if type(val) ~= tp then
error("argument "..idx.." must be "..tp, 2)
end
end
local lexer = {}
local NUMBER1 = '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+'
local NUMBER2 = '^[%+%-]?%d+%.?%d*'
local NUMBER3 = '^0x[%da-fA-F]+'
local NUMBER4 = '^%d+%.?%d*[eE][%+%-]?%d+'
local NUMBER5 = '^%d+%.?%d*'
local IDEN = '^[%a_][%w_]*'
local WSPACE = '^%s+'
local STRING1 = [[^'.-[^\\]']]
local STRING2 = [[^".-[^\\]"]]
local STRING3 = "^((['\"])%2)" -- empty string
local PREPRO = '^#.-[^\\]\n'
local plain_matches,lua_matches,cpp_matches,lua_keyword,cpp_keyword
local function tdump(tok)
return yield(tok,tok)
end
local function ndump(tok,options)
if options and options.number then
tok = tonumber(tok)
end
return yield("number",tok)
end
-- regular strings, single or double quotes; usually we want them
-- without the quotes
local function sdump(tok,options)
if options and options.string then
tok = tok:sub(2,-2)
end
return yield("string",tok)
end
-- long Lua strings need extra work to get rid of the quotes
local function sdump_l(tok,options)
if options and options.string then
tok = tok:sub(3,-3)
end
return yield("string",tok)
end
local function chdump(tok,options)
if options and options.string then
tok = tok:sub(2,-2)
end
return yield("char",tok)
end
local function cdump(tok)
return yield('comment',tok)
end
local function wsdump (tok)
return yield("space",tok)
end
local function pdump (tok)
return yield('prepro',tok)
end
local function plain_vdump(tok)
return yield("iden",tok)
end
local function lua_vdump(tok)
if lua_keyword[tok] then
return yield("keyword",tok)
else
return yield("iden",tok)
end
end
local function cpp_vdump(tok)
if cpp_keyword[tok] then
return yield("keyword",tok)
else
return yield("iden",tok)
end
end
--- create a plain token iterator from a string or file-like object.
-- @param s the string
-- @param matches an optional match table (set of pattern-action pairs)
-- @param filter a table of token types to exclude, by default {space=true}
-- @param options a table of options; by default, {number=true,string=true},
-- which means convert numbers and strip string quotes.
function lexer.scan (s,matches,filter,options)
--assert_arg(1,s,'string')
local file = type(s) ~= 'string' and s
filter = filter or {space=true}
options = options or {number=true,string=true}
if filter then
if filter.space then filter[wsdump] = true end
if filter.comments then
filter[cdump] = true
end
end
if not matches then
if not plain_matches then
plain_matches = {
{WSPACE,wsdump},
{NUMBER3,ndump},
{IDEN,plain_vdump},
{NUMBER1,ndump},
{NUMBER2,ndump},
{STRING3,sdump},
{STRING1,sdump},
{STRING2,sdump},
{'^.',tdump}
}
end
matches = plain_matches
end
local function lex ()
local i1,i2,idx,res1,res2,tok,pat,fun,capt
local line = 1
if file then s = file:read()..'\n' end
local sz = #s
local idx = 1
--print('sz',sz)
while true do
for _,m in ipairs(matches) do
pat = m[1]
fun = m[2]
i1,i2 = strfind(s,pat,idx)
if i1 then
tok = strsub(s,i1,i2)
idx = i2 + 1
if not (filter and filter[fun]) then
lexer.finished = idx > sz
res1,res2 = fun(tok,options)
end
if res1 then
local tp = type(res1)
-- insert a token list
if tp=='table' then
yield('','')
for _,t in ipairs(res1) do
yield(t[1],t[2])
end
elseif tp == 'string' then -- or search up to some special pattern
i1,i2 = strfind(s,res1,idx)
if i1 then
tok = strsub(s,i1,i2)
idx = i2 + 1
yield('',tok)
else
yield('','')
idx = sz + 1
end
--if idx > sz then return end
else
yield(line,idx)
end
end
if idx > sz then
if file then
--repeat -- next non-empty line
line = line + 1
s = file:read()
if not s then return end
--until not s:match '^%s*$'
s = s .. '\n'
idx ,sz = 1,#s
break
else
return
end
else break end
end
end
end
end
return wrap(lex)
end
local function isstring (s)
return type(s) == 'string'
end
--- insert tokens into a stream.
-- @param tok a token stream
-- @param a1 a string is the type, a table is a token list and
-- a function is assumed to be a token-like iterator (returns type & value)
-- @param a2 a string is the value
function lexer.insert (tok,a1,a2)
if not a1 then return end
local ts
if isstring(a1) and isstring(a2) then
ts = {{a1,a2}}
elseif type(a1) == 'function' then
ts = {}
for t,v in a1() do
append(ts,{t,v})
end
else
ts = a1
end
tok(ts)
end
--- get everything in a stream upto a newline.
-- @param tok a token stream
-- @return a string
function lexer.getline (tok)
local t,v = tok('.-\n')
return v
end
--- get current line number. <br>
-- Only available if the input source is a file-like object.
-- @param tok a token stream
-- @return the line number and current column
function lexer.lineno (tok)
return tok(0)
end
--- get the rest of the stream.
-- @param tok a token stream
-- @return a string
function lexer.getrest (tok)
local t,v = tok('.+')
return v
end
--- get the Lua keywords as a set-like table.
-- So <code>res["and"]</code> etc would be <code>true</code>.
-- @return a table
function lexer.get_keywords ()
if not lua_keyword then
lua_keyword = {
["and"] = true, ["break"] = true, ["do"] = true,
["else"] = true, ["elseif"] = true, ["end"] = true,
["false"] = true, ["for"] = true, ["function"] = true,
["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true,
["not"] = true, ["or"] = true, ["repeat"] = true,
["return"] = true, ["then"] = true, ["true"] = true,
["until"] = true, ["while"] = true
}
end
return lua_keyword
end
--- create a Lua token iterator from a string or file-like object.
-- Will return the token type and value.
-- @param s the string
-- @param filter a table of token types to exclude, by default {space=true,comments=true}
-- @param options a table of options; by default, {number=true,string=true},
-- which means convert numbers and strip string quotes.
function lexer.lua(s,filter,options)
filter = filter or {space=true,comments=true}
lexer.get_keywords()
if not lua_matches then
lua_matches = {
{WSPACE,wsdump},
{NUMBER3,ndump},
{IDEN,lua_vdump},
{NUMBER4,ndump},
{NUMBER5,ndump},
{STRING3,sdump},
{STRING1,sdump},
{STRING2,sdump},
{'^%-%-%[%[.-%]%]',cdump},
{'^%-%-.-\n',cdump},
{'^%[%[.-%]%]',sdump_l},
{'^==',tdump},
{'^~=',tdump},
{'^<=',tdump},
{'^>=',tdump},
{'^%.%.%.',tdump},
{'^%.%.',tdump},
{'^.',tdump}
}
end
return lexer.scan(s,lua_matches,filter,options)
end
--- create a C/C++ token iterator from a string or file-like object.
-- Will return the token type type and value.
-- @param s the string
-- @param filter a table of token types to exclude, by default {space=true,comments=true}
-- @param options a table of options; by default, {number=true,string=true},
-- which means convert numbers and strip string quotes.
function lexer.cpp(s,filter,options)
filter = filter or {comments=true}
if not cpp_keyword then
cpp_keyword = {
["class"] = true, ["break"] = true, ["do"] = true, ["sizeof"] = true,
["else"] = true, ["continue"] = true, ["struct"] = true,
["false"] = true, ["for"] = true, ["public"] = true, ["void"] = true,
["private"] = true, ["protected"] = true, ["goto"] = true,
["if"] = true, ["static"] = true, ["const"] = true, ["typedef"] = true,
["enum"] = true, ["char"] = true, ["int"] = true, ["bool"] = true,
["long"] = true, ["float"] = true, ["true"] = true, ["delete"] = true,
["double"] = true, ["while"] = true, ["new"] = true,
["namespace"] = true, ["try"] = true, ["catch"] = true,
["switch"] = true, ["case"] = true, ["extern"] = true,
["return"] = true,["default"] = true,['unsigned'] = true,['signed'] = true,
["union"] = true, ["volatile"] = true, ["register"] = true,["short"] = true,
}
end
if not cpp_matches then
cpp_matches = {
{WSPACE,wsdump},
{PREPRO,pdump},
{NUMBER3,ndump},
{IDEN,cpp_vdump},
{NUMBER4,ndump},
{NUMBER5,ndump},
{STRING3,sdump},
{STRING1,chdump},
{STRING2,sdump},
{'^//.-\n',cdump},
{'^/%*.-%*/',cdump},
{'^==',tdump},
{'^!=',tdump},
{'^<=',tdump},
{'^>=',tdump},
{'^->',tdump},
{'^&&',tdump},
{'^||',tdump},
{'^%+%+',tdump},
{'^%-%-',tdump},
{'^%+=',tdump},
{'^%-=',tdump},
{'^%*=',tdump},
{'^/=',tdump},
{'^|=',tdump},
{'^%^=',tdump},
{'^::',tdump},
{'^.',tdump}
}
end
return lexer.scan(s,cpp_matches,filter,options)
end
--- get a list of parameters separated by a delimiter from a stream.
-- @param tok the token stream
-- @param endtoken end of list (default ')'). Can be '\n'
-- @param delim separator (default ',')
-- @return a list of token lists.
function lexer.get_separated_list(tok,endtoken,delim)
endtoken = endtoken or ')'
delim = delim or ','
local parm_values = {}
local level = 1 -- used to count ( and )
local tl = {}
local function tappend (tl,t,val)
val = val or t
append(tl,{t,val})
end
local is_end
if endtoken == '\n' then
is_end = function(t,val)
return t == 'space' and val:find '\n'
end
else
is_end = function (t)
return t == endtoken
end
end
local token,value
while true do
token,value=tok()
if not token then return nil,'EOS' end -- end of stream is an error!
if is_end(token,value) and level == 1 then
append(parm_values,tl)
break
elseif token == '(' then
level = level + 1
tappend(tl,'(')
elseif token == ')' then
level = level - 1
if level == 0 then -- finished with parm list
append(parm_values,tl)
break
else
tappend(tl,')')
end
elseif token == delim and level == 1 then
append(parm_values,tl) -- a new parm
tl = {}
else
tappend(tl,token,value)
end
end
return parm_values,{token,value}
end
--- get the next non-space token from the stream.
-- @param tok the token stream.
function lexer.skipws (tok)
local t,v = tok()
while t == 'space' do
t,v = tok()
end
return t,v
end
local skipws = lexer.skipws
--- get the next token, which must be of the expected type.
-- Throws an error if this type does not match!
-- @param tok the token stream
-- @param expected_type the token type
-- @param no_skip_ws whether we should skip whitespace
function lexer.expecting (tok,expected_type,no_skip_ws)
assert_arg(1,tok,'function')
assert_arg(2,expected_type,'string')
local t,v
if no_skip_ws then
t,v = tok()
else
t,v = skipws(tok)
end
if t ~= expected_type then error ("expecting "..expected_type,2) end
return v
end
return lexer
@@ -0,0 +1,264 @@
--- Extract delimited Lua sequences from strings.
-- Inspired by Damian Conway's Text::Balanced in Perl. <br/>
-- <ul>
-- <li>[1] <a href="http://lua-users.org/wiki/LuaBalanced">Lua Wiki Page</a></li>
-- <li>[2] http://search.cpan.org/dist/Text-Balanced/lib/Text/Balanced.pm</li>
-- </ul> <br/>
-- <pre class=example>
-- local lb = require "pl.luabalanced"
-- --Extract Lua expression starting at position 4.
-- print(lb.match_expression("if x^2 + x > 5 then print(x) end", 4))
-- --> x^2 + x > 5 16
-- --Extract Lua string starting at (default) position 1.
-- print(lb.match_string([["test\"123" .. "more"]]))
-- --> "test\"123" 12
-- </pre>
-- (c) 2008, David Manura, Licensed under the same terms as Lua (MIT license).
-- @class module
-- @name pl.luabalanced
local M = {}
local assert = assert
local table_concat = table.concat
-- map opening brace <-> closing brace.
local ends = { ['('] = ')', ['{'] = '}', ['['] = ']' }
local begins = {}; for k,v in pairs(ends) do begins[v] = k end
-- Match Lua string in string <s> starting at position <pos>.
-- Returns <string>, <posnew>, where <string> is the matched
-- string (or nil on no match) and <posnew> is the character
-- following the match (or <pos> on no match).
-- Supports all Lua string syntax: "...", '...', [[...]], [=[...]=], etc.
local function match_string(s, pos)
pos = pos or 1
local posa = pos
local c = s:sub(pos,pos)
if c == '"' or c == "'" then
pos = pos + 1
while 1 do
pos = assert(s:find("[" .. c .. "\\]", pos), 'syntax error')
if s:sub(pos,pos) == c then
local part = s:sub(posa, pos)
return part, pos + 1
else
pos = pos + 2
end
end
else
local sc = s:match("^%[(=*)%[", pos)
if sc then
local _; _, pos = s:find("%]" .. sc .. "%]", pos)
assert(pos)
local part = s:sub(posa, pos)
return part, pos + 1
else
return nil, pos
end
end
end
M.match_string = match_string
-- Match bracketed Lua expression, e.g. "(...)", "{...}", "[...]", "[[...]]",
-- [=[...]=], etc.
-- Function interface is similar to match_string.
local function match_bracketed(s, pos)
pos = pos or 1
local posa = pos
local ca = s:sub(pos,pos)
if not ends[ca] then
return nil, pos
end
local stack = {}
while 1 do
pos = s:find('[%(%{%[%)%}%]\"\']', pos)
assert(pos, 'syntax error: unbalanced')
local c = s:sub(pos,pos)
if c == '"' or c == "'" then
local part; part, pos = match_string(s, pos)
assert(part)
elseif ends[c] then -- open
local mid, posb
if c == '[' then mid, posb = s:match('^%[(=*)%[()', pos) end
if mid then
pos = s:match('%]' .. mid .. '%]()', posb)
assert(pos, 'syntax error: long string not terminated')
if #stack == 0 then
local part = s:sub(posa, pos-1)
return part, pos
end
else
stack[#stack+1] = c
pos = pos + 1
end
else -- close
assert(stack[#stack] == assert(begins[c]), 'syntax error: unbalanced')
stack[#stack] = nil
if #stack == 0 then
local part = s:sub(posa, pos)
return part, pos+1
end
pos = pos + 1
end
end
end
M.match_bracketed = match_bracketed
-- Match Lua comment, e.g. "--...\n", "--[[...]]", "--[=[...]=]", etc.
-- Function interface is similar to match_string.
local function match_comment(s, pos)
pos = pos or 1
if s:sub(pos, pos+1) ~= '--' then
return nil, pos
end
pos = pos + 2
local partt, post = match_string(s, pos)
if partt then
return '--' .. partt, post
end
local part; part, pos = s:match('^([^\n]*\n?)()', pos)
return '--' .. part, pos
end
-- Match Lua expression, e.g. "a + b * c[e]".
-- Function interface is similar to match_string.
local wordop = {['and']=true, ['or']=true, ['not']=true}
local is_compare = {['>']=true, ['<']=true, ['~']=true}
local function match_expression(s, pos)
pos = pos or 1
local posa = pos
local lastident
local poscs, posce
while pos do
local c = s:sub(pos,pos)
if c == '"' or c == "'" or c == '[' and s:find('^[=%[]', pos+1) then
local part; part, pos = match_string(s, pos)
assert(part, 'syntax error')
elseif c == '-' and s:sub(pos+1,pos+1) == '-' then
-- note: handle adjacent comments in loop to properly support
-- backtracing (poscs/posce).
poscs = pos
while s:sub(pos,pos+1) == '--' do
local part; part, pos = match_comment(s, pos)
assert(part)
pos = s:match('^%s*()', pos)
posce = pos
end
elseif c == '(' or c == '{' or c == '[' then
local part; part, pos = match_bracketed(s, pos)
elseif c == '=' and s:sub(pos+1,pos+1) == '=' then
pos = pos + 2 -- skip over two-char op containing '='
elseif c == '=' and is_compare[s:sub(pos-1,pos-1)] then
pos = pos + 1 -- skip over two-char op containing '='
elseif c:match'^[%)%}%];,=]' then
local part = s:sub(posa, pos-1)
return part, pos
elseif c:match'^[%w_]' then
local newident,newpos = s:match('^([%w_]+)()', pos)
if pos ~= posa and not wordop[newident] then -- non-first ident
local pose = ((posce == pos) and poscs or pos) - 1
while s:match('^%s', pose) do pose = pose - 1 end
local ce = s:sub(pose,pose)
if ce:match'[%)%}\'\"%]]' or
ce:match'[%w_]' and not wordop[lastident]
then
local part = s:sub(posa, pos-1)
return part, pos
end
end
lastident, pos = newident, newpos
else
pos = pos + 1
end
pos = s:find('[%(%{%[%)%}%]\"\';,=%w_%-]', pos)
end
local part = s:sub(posa, #s)
return part, #s+1
end
M.match_expression = match_expression
-- Match name list (zero or more names). E.g. "a,b,c"
-- Function interface is similar to match_string,
-- but returns array as match.
local function match_namelist(s, pos)
pos = pos or 1
local list = {}
while 1 do
local c = #list == 0 and '^' or '^%s*,%s*'
local item, post = s:match(c .. '([%a_][%w_]*)%s*()', pos)
if item then pos = post else break end
list[#list+1] = item
end
return list, pos
end
M.match_namelist = match_namelist
-- Match expression list (zero or more expressions). E.g. "a+b,b*c".
-- Function interface is similar to match_string,
-- but returns array as match.
local function match_explist(s, pos)
pos = pos or 1
local list = {}
while 1 do
if #list ~= 0 then
local post = s:match('^%s*,%s*()', pos)
if post then pos = post else break end
end
local item; item, pos = match_expression(s, pos)
assert(item, 'syntax error')
list[#list+1] = item
end
return list, pos
end
M.match_explist = match_explist
-- Replace snippets of code in Lua code string <s>
-- using replacement function f(u,sin) --> sout.
-- <u> is the type of snippet ('c' = comment, 's' = string,
-- 'e' = any other code).
-- Snippet is replaced with <sout> (unless <sout> is nil or false, in
-- which case the original snippet is kept)
-- This is somewhat analogous to string.gsub .
local function gsub(s, f)
local pos = 1
local posa = 1
local sret = ''
while 1 do
pos = s:find('[%-\'\"%[]', pos)
if not pos then break end
if s:match('^%-%-', pos) then
local exp = s:sub(posa, pos-1)
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
local comment; comment, pos = match_comment(s, pos)
sret = sret .. (f('c', assert(comment)) or comment)
posa = pos
else
local posb = s:find('^[\'\"%[]', pos)
local str
if posb then str, pos = match_string(s, posb) end
if str then
local exp = s:sub(posa, posb-1)
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
sret = sret .. (f('s', str) or str)
posa = pos
else
pos = pos + 1
end
end
end
local exp = s:sub(posa)
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
return sret
end
M.gsub = gsub
return M
@@ -0,0 +1,197 @@
--- Lua operators available as functions.
-- (similar to the Python module of the same name)<br>
-- There is a module field <code>optable</code> which maps the operator strings
-- onto these functions, e.g. <pre class=example>operator.optable['()']==operator.call</pre>
-- <p>Operator strings like '>' and '{}' can be passed to most Penlight functions
-- expecting a function argument.</p>
-- @class module
-- @name pl.operator
local strfind = string.find
local utils = require 'pl.utils'
local operator = {}
--- apply function to some arguments ()
-- @param fn a function or callable object
-- @param ... arguments
function operator.call(fn,...)
return fn(...)
end
--- get the indexed value from a table []
-- @param t a table or any indexable object
-- @param k the key
function operator.index(t,k)
return t[k]
end
--- returns true if arguments are equal ==
-- @param a value
-- @param b value
function operator.eq(a,b)
return a==b
end
--- returns true if arguments are not equal ~=
-- @param a value
-- @param b value
function operator.neq(a,b)
return a~=b
end
--- returns true if a is less than b <
-- @param a value
-- @param b value
function operator.lt(a,b)
return a < b
end
--- returns true if a is less or equal to b <=
-- @param a value
-- @param b value
function operator.le(a,b)
return a <= b
end
--- returns true if a is greater than b >
-- @param a value
-- @param b value
function operator.gt(a,b)
return a > b
end
--- returns true if a is greater or equal to b >=
-- @param a value
-- @param b value
function operator.ge(a,b)
return a >= b
end
--- returns length of string or table #
-- @param a a string or a table
function operator.len(a)
return #a
end
--- add two values +
-- @param a value
-- @param b value
function operator.add(a,b)
return a+b
end
--- subtract b from a -
-- @param a value
-- @param b value
function operator.sub(a,b)
return a-b
end
--- multiply two values *
-- @param a value
-- @param b value
function operator.mul(a,b)
return a*b
end
--- divide first value by second /
-- @param a value
-- @param b value
function operator.div(a,b)
return a/b
end
--- raise first to the power of second ^
-- @param a value
-- @param b value
function operator.pow(a,b)
return a^b
end
--- modulo; remainder of a divided by b %
-- @param a value
-- @param b value
function operator.mod(a,b)
return a%b
end
--- concatenate two values (either strings or __concat defined) ..
-- @param a value
-- @param b value
function operator.concat(a,b)
return a..b
end
--- return the negative of a value -
-- @param a value
-- @param b value
function operator.unm(a)
return -a
end
--- false if value evaluates as true not
-- @param a value
function operator.lnot(a)
return not a
end
--- true if both values evaluate as true and
-- @param a value
-- @param b value
function operator.land(a,b)
return a and b
end
--- true if either value evaluate as true or
-- @param a value
-- @param b value
function operator.lor(a,b)
return a or b
end
--- make a table from the arguments {}
-- @param ... non-nil arguments
-- @return a table
function operator.table (...)
return {...}
end
--- match two strings ~
-- uses @{string.find}
function operator.match (a,b)
return strfind(a,b)~=nil
end
--- the null operation.
-- @param ... arguments
-- @return the arguments
function operator.nop (...)
return ...
end
operator.optable = {
['+']=operator.add,
['-']=operator.sub,
['*']=operator.mul,
['/']=operator.div,
['%']=operator.mod,
['^']=operator.pow,
['..']=operator.concat,
['()']=operator.call,
['[]']=operator.index,
['<']=operator.lt,
['<=']=operator.le,
['>']=operator.gt,
['>=']=operator.ge,
['==']=operator.eq,
['~=']=operator.neq,
['#']=operator.len,
['and']=operator.land,
['or']=operator.lor,
['{}']=operator.table,
['~']=operator.match,
['']=operator.nop,
}
return operator
@@ -0,0 +1,335 @@
--- Path manipulation and file queries. <br>
-- This is modelled after Python's os.path library (11.1)
-- @class module
-- @name pl.path
-- imports and locals
local _G = _G
local sub = string.sub
local getenv = os.getenv
local tmpnam = os.tmpname
local attributes, currentdir, link_attrib
local package = package
local io = io
local append = table.insert
local ipairs = ipairs
local utils = require 'pl.utils'
local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise
--[[
module ('pl.path',utils._module)
]]
local path, attrib
if _G.luajava then
path = require 'pl.platf.luajava'
else
path = {}
local res,lfs = _G.pcall(_G.require,'lfs')
if res then
attributes = lfs.attributes
currentdir = lfs.currentdir
link_attrib = lfs.symlinkattributes
else
error("pl.path requires LuaFileSystem")
end
attrib = attributes
path.attrib = attrib
path.link_attrib = link_attrib
path.dir = lfs.dir
path.mkdir = lfs.mkdir
path.rmdir = lfs.rmdir
path.chdir = lfs.chdir
--- is this a directory?
-- @param P A file path
function path.isdir(P)
if P:match("\\$") then
P = P:sub(1,-2)
end
return attrib(P,'mode') == 'directory'
end
--- is this a file?.
-- @param P A file path
function path.isfile(P)
return attrib(P,'mode') == 'file'
end
-- is this a symbolic link?
-- @param P A file path
function path.islink(P)
if link_attrib then
return link_attrib(P,'mode')=='link'
else
return false
end
end
--- return size of a file.
-- @param P A file path
function path.getsize(P)
return attrib(P,'size')
end
--- does a path exist?.
-- @param P A file path
-- @return the file path if it exists, nil otherwise
function path.exists(P)
return attrib(P,'mode') ~= nil and P
end
--- Return the time of last access as the number of seconds since the epoch.
-- @param P A file path
function path.getatime(P)
return attrib(P,'access')
end
--- Return the time of last modification
-- @param P A file path
function path.getmtime(P)
return attrib(P,'modification')
end
---Return the system's ctime.
-- @param P A file path
function path.getctime(P)
return path.attrib(P,'change')
end
end
local function at(s,i)
return sub(s,i,i)
end
path.is_windows = utils.dir_separator == '\\'
local other_sep
-- !constant sep is the directory separator for this platform.
if path.is_windows then
path.sep = '\\'; other_sep = '/'
path.dirsep = ';'
else
path.sep = '/'
path.dirsep = ':'
end
local sep,dirsep = path.sep,path.dirsep
--- are we running Windows?
-- @class field
-- @name path.is_windows
--- path separator for this platform.
-- @class field
-- @name path.sep
--- separator for PATH for this platform
-- @class field
-- @name path.dirsep
--- given a path, return the directory part and a file part.
-- if there's no directory part, the first value will be empty
-- @param P A file path
function path.splitpath(P)
assert_string(1,P)
local i = #P
local ch = at(P,i)
while i > 0 and ch ~= sep and ch ~= other_sep do
i = i - 1
ch = at(P,i)
end
if i == 0 then
return '',P
else
return sub(P,1,i-1), sub(P,i+1)
end
end
--- return an absolute path.
-- @param P A file path
function path.abspath(P)
assert_string(1,P)
if not currentdir then return P end
P = P:gsub('[\\/]$','')
local pwd = currentdir()
if not path.isabs(P) then
return path.join(pwd,P)
elseif path.is_windows and at(P,2) ~= ':' and at(P,2) ~= '\\' then
return pwd:sub(1,2)..P
else
return P
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
function path.splitext(P)
assert_string(1,P)
local i = #P
local ch = at(P,i)
while i > 0 and ch ~= '.' do
if ch == sep or ch == other_sep then
return P,''
end
i = i - 1
ch = at(P,i)
end
if i == 0 then
return P,''
else
return sub(P,1,i-1),sub(P,i)
end
end
--- return the directory part of a path
-- @param P A file path
function path.dirname(P)
assert_string(1,P)
local p1,p2 = path.splitpath(P)
return p1
end
--- return the file part of a path
-- @param P A file path
function path.basename(P)
assert_string(1,P)
local p1,p2 = path.splitpath(P)
return p2
end
--- get the extension part of a path.
-- @param P A file path
function path.extension(P)
assert_string(1,P)
local p1,p2 = path.splitext(P)
return p2
end
--- is this an absolute path?.
-- @param P A file path
function path.isabs(P)
assert_string(1,P)
if path.is_windows then
return at(P,1) == '/' or at(P,1)=='\\' or at(P,2)==':'
else
return at(P,1) == '/'
end
end
--- return the P resulting from combining the two paths.
-- if the second is already an absolute path, then it returns it.
-- @param p1 A file path
-- @param p2 A file path
function path.join(p1,p2)
assert_string(1,p1)
assert_string(2,p2)
if path.isabs(p2) then return p2 end
local endc = at(p1,#p1)
if endc ~= path.sep and endc ~= other_sep then
p1 = p1..path.sep
end
return p1..p2
end
--- normalize the case of a pathname. On Unix, this returns the path unchanged;
-- for Windows, it converts the path to lowercase, and it also converts forward slashes
-- to backward slashes.
-- @param P A file path
function path.normcase(P)
assert_string(1,P)
if path.is_windows then
return (P:lower():gsub('/','\\'))
else
return P
end
end
--- normalize a path name.
-- A//B, A/./B and A/foo/../B all become A/B.
-- @param P a file path
function path.normpath (P)
assert_string(1,P)
if path.is_windows then
P = P:gsub('/','\\')
return (P:gsub('[^\\]+\\%.%.\\',''):gsub('\\%.?\\','\\'))
else
return (P:gsub('[^/]+/%.%./',''):gsub('/%.?/','/'))
end
end
--- Replace a starting '~' with the user's home directory.
-- In windows, if HOME isn't set, then USERPROFILE is used in preference to
-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows.
-- @param P A file path
function path.expanduser(P)
assert_string(1,P)
if at(P,1) == '~' then
local home = getenv('HOME')
if not home then -- has to be Windows
home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH')
end
return home..sub(P,2)
else
return P
end
end
---Return a suitable full path to a new temporary file name.
-- unlike os.tmpnam(), it always gives you a writeable path (uses %TMP% on Windows)
function path.tmpname ()
local res = tmpnam()
if path.is_windows then res = getenv('TMP')..res end
return res
end
--- return the largest common prefix path of two paths.
-- @param path1 a file path
-- @param path2 a file path
function path.common_prefix (path1,path2)
assert_string(1,path1)
assert_string(2,path2)
-- get them in order!
if #path1 > #path2 then path2,path1 = path1,path2 end
for i = 1,#path1 do
local c1 = at(path1,i)
if c1 ~= at(path2,i) then
local cp = path1:sub(1,i-1)
if at(path1,i-1) ~= sep then
cp = path.dirname(cp)
end
return cp
end
end
if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end
return path1
--return ''
end
--- return the full path where a particular Lua module would be found.
-- Both package.path and package.cpath is searched, so the result may
-- either be a Lua file or a shared libarary.
-- @param mod name of the module
-- @return on success: path of module, lua or binary
-- @return on error: nil,error string
function path.package_path(mod)
assert_string(1,mod)
local res
mod = mod:gsub('%.',sep)
res = package.searchpath(mod,package.path)
if res then return res,true end
res = package.searchpath(mod,package.cpath)
if res then return res,false end
return raise 'cannot find module on path'
end
---- finis -----
return path
@@ -0,0 +1,65 @@
--- Permutation operations.
-- @class module
-- @name pl.permute
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local copy = tablex.deepcopy
local append = table.insert
local coroutine = coroutine
local resume = coroutine.resume
local assert_arg = utils.assert_arg
--[[
module ('pl.permute',utils._module)
]]
local permute = {}
-- PiL, 9.3
local permgen
permgen = function (a, n, fn)
if n == 0 then
fn(a)
else
for i=1,n do
-- put i-th element as the last one
a[n], a[i] = a[i], a[n]
-- generate all permutations of the other elements
permgen(a, n - 1, fn)
-- restore i-th element
a[n], a[i] = a[i], a[n]
end
end
end
--- an iterator over all permutations of the elements of a list.
-- Please note that the same list is returned each time, so do not keep references!
-- @param a list-like table
-- @return an iterator which provides the next permutation as a list
function permute.iter (a)
assert_arg(1,a,'table')
local n = #a
local co = coroutine.create(function () permgen(a, n, coroutine.yield) end)
return function () -- iterator
local code, res = resume(co)
return res
end
end
--- construct a table containing all the permutations of a list.
-- @param a list-like table
-- @return a table of tables
-- @usage permute.table {1,2,3} --> {{2,3,1},{3,2,1},{3,1,2},{1,3,2},{2,1,3},{1,2,3}}
function permute.table (a)
assert_arg(1,a,'table')
local res = {}
local n = #a
permgen(a,n,function(t) append(res,copy(t)) end)
return res
end
return permute
@@ -0,0 +1,101 @@
-- experimental support for LuaJava
--
local path = {}
path.link_attrib = nil
local File = luajava.bindClass("java.io.File")
local Array = luajava.bindClass('java.lang.reflect.Array')
local function file(s)
return luajava.new(File,s)
end
function path.dir(P)
local ls = file(P):list()
print(ls)
local idx,n = -1,Array:getLength(ls)
return function ()
idx = idx + 1
if idx == n then return nil
else
return Array:get(ls,idx)
end
end
end
function path.mkdir(P)
return file(P):mkdir()
end
function path.rmdir(P)
return file(P):delete()
end
--- is this a directory?
-- @param P A file path
function path.isdir(P)
if P:match("\\$") then
P = P:sub(1,-2)
end
return file(P):isDirectory()
end
--- is this a file?.
-- @param P A file path
function path.isfile(P)
return file(P):isFile()
end
-- is this a symbolic link?
-- Direct support for symbolic links is not provided.
-- see http://stackoverflow.com/questions/813710/java-1-6-determine-symbolic-links
-- and the caveats therein.
-- @param P A file path
function path.islink(P)
local f = file(P)
local canon
local parent = f:getParent()
if not parent then
canon = f
else
parent = f.getParentFile():getCanonicalFile()
canon = luajava.new(File,parent,f:getName())
end
return canon:getCanonicalFile() ~= canon:getAbsoluteFile()
end
--- return size of a file.
-- @param P A file path
function path.getsize(P)
return file(P):length()
end
--- does a path exist?.
-- @param P A file path
-- @return the file path if it exists, nil otherwise
function path.exists(P)
return file(P):exists() and P
end
--- Return the time of last access as the number of seconds since the epoch.
-- @param P A file path
function path.getatime(P)
return path.getmtime(P)
end
--- Return the time of last modification
-- @param P A file path
function path.getmtime(P)
-- Java time is no. of millisec since the epoch
return file(P):lastModified()/1000
end
---Return the system's ctime.
-- @param P A file path
function path.getctime(P)
return path.getmtime(P)
end
return path
@@ -0,0 +1,224 @@
--- Pretty-printing Lua tables.
-- Also provides a sandboxed Lua table reader and
-- a function to present large numbers in human-friendly format.
-- @class module
-- @name pl.pretty
local append = table.insert
local concat = table.concat
local utils = require 'pl.utils'
local lexer = require 'pl.lexer'
local assert_arg = utils.assert_arg
local pretty = {}
--- read a string representation of a Lua table.
-- Uses load(), but tries to be cautious about loading arbitrary code!
-- It is expecting a string of the form '{...}', with perhaps some whitespace
-- before or after the curly braces. An empty environment is used, and
-- any occurance of the keyword 'function' will be considered a problem.
-- @param s {string} string of the form '{...}', with perhaps some whitespace
-- before or after the curly braces.
function pretty.read(s)
assert_arg(1,s,'string')
if not s:find '^%s*%b{}%s*$' then return nil,"not a Lua table" end
if s:find '[^\'"%w_]function[^\'"%w_]' then
local tok = lexer.lua(s)
for t,v in tok do
if t == 'keyword' then
return nil,"cannot have Lua keywords in table definition"
end
end
end
local chunk,err = utils.load('return '..s,'tbl','t',{})
if not chunk then return nil,err end
return chunk()
end
local function quote_if_necessary (v)
if not v then return ''
else
if v:find ' ' then v = '"'..v..'"' end
end
return v
end
local keywords
--- Create a string representation of a Lua table.
-- This function never fails, but may complain by returning an
-- extra value. Normally puts out one item per line, using
-- the provided indent; set the second parameter to '' if
-- you want output on one line.
-- @param tbl {table} Table to serialize to a string.
-- @param space {string} (optional) The indent to use.
-- Defaults to two spaces.
-- @param not_clever {bool} (optional) Use for plain output, e.g {['key']=1}.
-- Defaults to false.
-- @return a string
-- @return a possible error message
function pretty.write (tbl,space,not_clever)
if type(tbl) ~= 'table' then
local res = tostring(tbl)
if type(tbl) == 'string' then res = '"'..res..'"' end
return res, 'not a table'
end
if not keywords then
keywords = lexer.get_keywords()
end
local set = ' = '
if space == '' then set = '=' end
space = space or ' '
local lines = {}
local line = ''
local tables = {}
local function is_identifier (s)
return (s:find('^[%a_][%w_]*$')) and not keywords[s]
end
local function put(s)
if #s > 0 then
line = line..s
end
end
local function putln (s)
if #line > 0 then
line = line..s
append(lines,line)
line = ''
else
append(lines,s)
end
end
local function eat_last_comma ()
local n,lastch = #lines
local lastch = lines[n]:sub(-1,-1)
if lastch == ',' then
lines[n] = lines[n]:sub(1,-2)
end
end
local function quote (s)
return ('%q'):format(tostring(s))
end
local function index (numkey,key)
if not numkey then key = quote(key) end
return '['..key..']'
end
local writeit
writeit = function (t,oldindent,indent)
local tp = type(t)
if tp ~= 'string' and tp ~= 'table' then
putln(quote_if_necessary(tostring(t))..',')
elseif tp == 'string' then
if t:find('\n') then
putln('[[\n'..t..']],')
else
putln(quote(t)..',')
end
elseif tp == 'table' then
if tables[t] then
putln('<cycle>,')
return
end
tables[t] = true
local newindent = indent..space
putln('{')
local used = {}
if not not_clever then
for i,val in ipairs(t) do
put(indent)
writeit(val,indent,newindent)
used[i] = true
end
end
for key,val in pairs(t) do
local numkey = type(key) == 'number'
if not_clever then
key = tostring(key)
put(indent..index(numkey,key)..set)
writeit(val,indent,newindent)
else
if not numkey or not used[key] then -- non-array indices
if numkey or not is_identifier(key) then
key = index(numkey,key)
end
put(indent..key..set)
writeit(val,indent,newindent)
end
end
end
eat_last_comma()
putln(oldindent..'},')
else
putln(tostring(t)..',')
end
end
writeit(tbl,'',space)
eat_last_comma()
return concat(lines,#space > 0 and '\n' or '')
end
--- Dump a Lua table out to a file or stdout.
-- @param t {table} The table to write to a file or stdout.
-- @param ... {string} (optional) File name to write too. Defaults to writing
-- to stdout.
function pretty.dump (t,...)
if select('#',...)==0 then
print(pretty.write(t))
return true
else
return utils.writefile(...,pretty.write(t))
end
end
local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
local comma
function comma (val)
local thou = math.floor(val/1000)
if thou > 0 then return comma(thou)..','..(val % 1000)
else return tostring(val) end
end
--- format large numbers nicely for human consumption.
-- @param num a number
-- @param kind one of 'M' (memory in KiB etc), 'N' (postfixes are 'K','M' and 'B')
-- and 'T' (use commas as thousands separator)
-- @param prec number of digits to use for 'M' and 'N' (default 1)
function pretty.number (num,kind,prec)
local fmt = '%.'..(prec or 1)..'f%s'
if kind == 'T' then
return comma(num)
else
local postfixes, fact
if kind == 'M' then
fact = 1024
postfixes = memp
else
fact = 1000
postfixes = nump
end
local div = fact
local k = 1
while num >= div and k <= #postfixes do
div = div * fact
k = k + 1
end
div = div / fact
if k > #postfixes then k = k - 1; div = div/fact end
if k > 1 then
return fmt:format(num/div,postfixes[k] or 'duh')
else
return num..postfixes[1]
end
end
end
return pretty
@@ -0,0 +1,527 @@
--- Manipulating sequences as iterators.
-- @class module
-- @name pl.seq
local next,assert,type,pairs,tonumber,type,setmetatable,getmetatable,_G = next,assert,type,pairs,tonumber,type,setmetatable,getmetatable,_G
local strfind = string.find
local strmatch = string.match
local format = string.format
local mrandom = math.random
local remove,tsort,tappend = table.remove,table.sort,table.insert
local io = io
local utils = require 'pl.utils'
local function_arg = utils.function_arg
local _List = utils.stdmt.List
local _Map = utils.stdmt.Map
local assert_arg = utils.assert_arg
require 'debug'
--[[
module("pl.seq",utils._module)
]]
local seq = {}
-- given a number, return a function(y) which returns true if y > x
-- @param x a number
function seq.greater_than(x)
return function(v)
return tonumber(v) > x
end
end
-- given a number, returns a function(y) which returns true if y < x
-- @param x a number
function seq.less_than(x)
return function(v)
return tonumber(v) < x
end
end
-- given any value, return a function(y) which returns true if y == x
-- @param x a value
function seq.equal_to(x)
if type(x) == "number" then
return function(v)
return tonumber(v) == x
end
else
return function(v)
return v == x
end
end
end
--- given a string, return a function(y) which matches y against the string.
-- @param s a string
function seq.matching(s)
return function(v)
return strfind(v,s)
end
end
--- sequence adaptor for a table. Note that if any generic function is
-- passed a table, it will automatically use seq.list()
-- @param t a list-like table
-- @usage sum(list(t)) is the sum of all elements of t
-- @usage for x in list(t) do...end
function seq.list(t)
assert_arg(1,t,'table')
local key,value
return function()
key,value = next(t,key)
return value
end
end
--- return the keys of the table.
-- @param t a list-like table
-- @return iterator over keys
function seq.keys(t)
assert_arg(1,t,'table')
local key,value
return function()
key,value = next(t,key)
return key
end
end
local list = seq.list
local function default_iter(iter)
if type(iter) == 'table' then return list(iter)
else return iter end
end
seq.iter = default_iter
--- create an iterator over a numerical range. Like the standard Python function xrange.
-- @param start a number
-- @param finish a number greater than start
function seq.range(start,finish)
local i = start - 1
return function()
i = i + 1
if i > finish then return nil
else return i end
end
end
-- count the number of elements in the sequence which satisfy the predicate
-- @param iter a sequence
-- @param condn a predicate function (must return either true or false)
-- @param optional argument to be passed to predicate as second argument.
function seq.count(iter,condn,arg)
local i = 0
seq.foreach(iter,function(val)
if condn(val,arg) then i = i + 1 end
end)
return i
end
--- return the minimum and the maximum value of the sequence.
-- @param iter a sequence
function seq.minmax(iter)
local vmin,vmax = 1e70,-1e70
for v in default_iter(iter) do
v = tonumber(v)
if v < vmin then vmin = v end
if v > vmax then vmax = v end
end
return vmin,vmax
end
--- return the sum and element count of the sequence.
-- @param iter a sequence
-- @param fn an optional function to apply to the values
function seq.sum(iter,fn)
local s = 0
local i = 0
for v in default_iter(iter) do
if fn then v = fn(v) end
s = s + v
i = i + 1
end
return s,i
end
--- create a table from the sequence. (This will make the result a List.)
-- @param iter a sequence
-- @return a List
-- @usage copy(list(ls)) is equal to ls
-- @usage copy(list {1,2,3}) == List{1,2,3}
function seq.copy(iter)
local res = {}
for v in default_iter(iter) do
tappend(res,v)
end
setmetatable(res,_List)
return res
end
--- create a table of pairs from the double-valued sequence.
-- @param iter a double-valued sequence
-- @param i1 used to capture extra iterator values
-- @param i2 as with pairs & ipairs
-- @usage copy2(ipairs{10,20,30}) == {{1,10},{2,20},{3,30}}
-- @return a list-like table
function seq.copy2 (iter,i1,i2)
local res = {}
for v1,v2 in iter,i1,i2 do
tappend(res,{v1,v2})
end
return res
end
--- create a table of 'tuples' from a multi-valued sequence.
-- A generalization of copy2 above
-- @param iter a multiple-valued sequence
-- @return a list-like table
function seq.copy_tuples (iter)
iter = default_iter(iter)
local res = {}
local row = {iter()}
while #row > 0 do
tappend(res,row)
row = {iter()}
end
return res
end
--- return an iterator of random numbers.
-- @param n the length of the sequence
-- @param l same as the first optional argument to math.random
-- @param u same as the second optional argument to math.random
-- @return a sequnce
function seq.random(n,l,u)
local rand
assert(type(n) == 'number')
if u then
rand = function() return mrandom(l,u) end
elseif l then
rand = function() return mrandom(l) end
else
rand = mrandom
end
return function()
if n == 0 then return nil
else
n = n - 1
return rand()
end
end
end
--- return an iterator to the sorted elements of a sequence.
-- @param iter a sequence
-- @param comp an optional comparison function (comp(x,y) is true if x < y)
function seq.sort(iter,comp)
local t = seq.copy(iter)
tsort(t,comp)
return list(t)
end
--- return an iterator which returns elements of two sequences.
-- @param iter1 a sequence
-- @param iter2 a sequence
-- @usage for x,y in seq.zip(ls1,ls2) do....end
function seq.zip(iter1,iter2)
iter1 = default_iter(iter1)
iter2 = default_iter(iter2)
return function()
return iter1(),iter2()
end
end
--- A table where the key/values are the values and value counts of the sequence.
-- This version works with 'hashable' values like strings and numbers. <br>
-- pl.tablex.count_map is more general.
-- @param iter a sequence
-- @return a map-like table
-- @return a table
-- @see pl.tablex.count_map
function seq.count_map(iter)
local t = {}
local v
for s in default_iter(iter) do
v = t[s]
if v then t[s] = v + 1
else t[s] = 1 end
end
return setmetatable(t,_Map)
end
-- given a sequence, return all the unique values in that sequence.
-- @param iter a sequence
-- @param returns_table true if we return a table, not a sequence
-- @return a sequence or a table; defaults to a sequence.
function seq.unique(iter,returns_table)
local t = seq.count_map(iter)
local res = {}
for k in pairs(t) do tappend(res,k) end
table.sort(res)
if returns_table then
return res
else
return list(res)
end
end
-- print out a sequence @iter, with a separator @sep (default space)
-- and maximum number of values per line @nfields (default 7)
-- @fmt is an optional format function to create a representation of each value.
function seq.printall(iter,sep,nfields,fmt)
local write = io.write
if not sep then sep = ' ' end
if not nfields then
if sep == '\n' then nfields = 1e30
else nfields = 7 end
end
if fmt then
local fstr = fmt
fmt = function(v) return format(fstr,v) end
end
local k = 1
for v in default_iter(iter) do
if fmt then v = fmt(v) end
if k < nfields then
write(v,sep)
k = k + 1
else
write(v,'\n')
k = 1
end
end
write '\n'
end
-- return an iterator running over every element of two sequences (concatenation).
-- @param iter1 a sequence
-- @param iter2 a sequence
function seq.splice(iter1,iter2)
iter1 = default_iter(iter1)
iter2 = default_iter(iter2)
local iter = iter1
return function()
local ret = iter()
if ret == nil then
if iter == iter1 then
iter = iter2
return iter()
else return nil end
else
return ret
end
end
end
--- return a sequence where every element of a sequence has been transformed
-- by a function. If you don't supply an argument, then the function will
-- receive both values of a double-valued sequence, otherwise behaves rather like
-- tablex.map.
-- @param fn a function to apply to elements; may take two arguments
-- @param iter a sequence of one or two values
-- @param arg optional argument to pass to function.
function seq.map(fn,iter,arg)
fn = function_arg(1,fn)
iter = default_iter(iter)
return function()
local v1,v2 = iter()
if v1 == nil then return nil end
if arg then return fn(v1,arg) or false
else return fn(v1,v2) or false
end
end
end
--- filter a sequence using a predicate function
-- @param iter a sequence of one or two values
-- @param pred a boolean function; may take two arguments
-- @param arg optional argument to pass to function.
function seq.filter (iter,pred,arg)
pred = function_arg(2,pred)
return function ()
local v1,v2
while true do
v1,v2 = iter()
if v1 == nil then return nil end
if arg then
if pred(v1,arg) then return v1,v2 end
else
if pred(v1,v2) then return v1,v2 end
end
end
end
end
--- 'reduce' a sequence using a binary function.
-- @param fun a function of two arguments
-- @param iter a sequence
-- @param oldval optional initial value
-- @usage seq.reduce(operator.add,seq.list{1,2,3,4}) == 10
-- @usage seq.reduce('-',{1,2,3,4,5}) == -13
function seq.reduce (fun,iter,oldval)
fun = function_arg(1,fun)
iter = default_iter(iter)
if not oldval then
oldval = iter()
end
local val = oldval
for v in iter do
val = fun(val,v)
end
return val
end
--- take the first n values from the sequence.
-- @param iter a sequence of one or two values
-- @param n number of items to take
-- @return a sequence of at most n items
function seq.take (iter,n)
local i = 1
iter = default_iter(iter)
return function()
if i > n then return end
local val1,val2 = iter()
if not val1 then return end
i = i + 1
return val1,val2
end
end
--- skip the first n values of a sequence
-- @param iter a sequence of one or more values
-- @param n number of items to skip
function seq.skip (iter,n)
n = n or 1
for i = 1,n do iter() end
return iter
end
--- a sequence with a sequence count and the original value. <br>
-- enum(copy(ls)) is a roundabout way of saying ipairs(ls).
-- @param iter a single or double valued sequence
-- @return sequence of (i,v), i = 1..n and v is from iter.
function seq.enum (iter)
local i = 0
iter = default_iter(iter)
return function ()
local val1,val2 = iter()
if not val1 then return end
i = i + 1
return i,val1,val2
end
end
--- map using a named method over a sequence.
-- @param iter a sequence
-- @param name the method name
-- @param arg1 optional first extra argument
-- @param arg2 optional second extra argument
function seq.mapmethod (iter,name,arg1,arg2)
iter = default_iter(iter)
return function()
local val = iter()
if not val then return end
local fn = val[name]
if not fn then error(type(val).." does not have method "..name) end
return fn(val,arg1,arg2)
end
end
--- a sequence of (last,current) values from another sequence.
-- This will return S(i-1),S(i) if given S(i)
-- @param iter a sequence
function seq.last (iter)
iter = default_iter(iter)
local l = iter()
if l == nil then return nil end
return function ()
local val,ll
val = iter()
if val == nil then return nil end
ll = l
l = val
return val,ll
end
end
--- call the function on each element of the sequence.
-- @param iter a sequence with up to 3 values
-- @param fn a function
function seq.foreach(iter,fn)
fn = function_arg(2,fn)
for i1,i2,i3 in default_iter(iter) do fn(i1,i2,i3) end
end
---------------------- Sequence Adapters ---------------------
local SMT
local callable = utils.is_callable
local function SW (iter,...)
if callable(iter) then
return setmetatable({iter=iter},SMT)
else
return iter,...
end
end
-- can't directly look these up in seq because of the wrong argument order...
local map,reduce,mapmethod = seq.map, seq.reduce, seq.mapmethod
local overrides = {
map = function(self,fun,arg)
return map(fun,self,arg)
end,
reduce = function(self,fun)
return reduce(fun,self)
end
}
SMT = {
__index = function (tbl,key)
local s = overrides[key] or seq[key]
if s then
return function(sw,...) return SW(s(sw.iter,...)) end
else
return function(sw,...) return SW(mapmethod(sw.iter,key,...)) end
end
end,
__call = function (sw)
return sw.iter()
end,
}
setmetatable(seq,{
__call = function(tbl,iter)
if not callable(iter) then
if type(iter) == 'table' then iter = seq.list(iter)
else return iter
end
end
return setmetatable({iter=iter},SMT)
end
})
--- create a wrapped iterator over all lines in the file.
-- @param f either a filename or nil (for standard input)
-- @return a sequence wrapper
function seq.lines (f)
local iter = f and io.lines(f) or io.lines()
return SW(iter)
end
function seq.import ()
_G.debug.setmetatable(function() end,{
__index = function(tbl,key)
local s = overrides[key] or seq[key]
if s then return s
else
return function(s,...) return seq.mapmethod(s,key,...) end
end
end
})
end
return seq
@@ -0,0 +1,335 @@
--- Simple Input Patterns (SIP). <p>
-- SIP patterns start with '$', then a
-- one-letter type, and then an optional variable in curly braces. <p>
-- Example:
-- <pre class=example>
-- sip.match('$v=$q','name="dolly"',res)
-- ==> res=={'name','dolly'}
-- sip.match('($q{first},$q{second})','("john","smith")',res)
-- ==> res=={second='smith',first='john'}
-- </pre>
-- <pre>
-- <b>Type names</b>
-- v identifier
-- i integer
-- f floating-point
-- q quoted string
-- ([{&lt; match up to closing bracket
-- </pre>
-- <p>
-- See <a href="../../index.html#sip">the Guide</a>
-- @class module
-- @name pl.sip
local append,concat = table.insert,table.concat
local concat = table.concat
local ipairs,loadstring,type,unpack = ipairs,loadstring,type,unpack
local io,_G = io,_G
local print,rawget = print,rawget
local patterns = {
FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
INTEGER = '[+%-%d]%d*',
IDEN = '[%a_][%w_]*',
FILE = '[%a%.\\][:%][%w%._%-\\]*'
}
local function assert_arg(idx,val,tp)
if type(val) ~= tp then
error("argument "..idx.." must be "..tp, 2)
end
end
--[[
module ('pl.sip',utils._module)
]]
local sip = {}
local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' }
local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0}
local _patterns = {}
local function group(s)
return '('..s..')'
end
-- escape all magic characters except $, which has special meaning
-- Also, un-escape any characters after $, so $( passes through as is.
local function escape (spec)
--_G.print('spec',spec)
local res = spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%1'):gsub('%$%%(%S)','$%1')
--_G.print('res',res)
return res
end
local function imcompressible (s)
return s:gsub('%s+','\001')
end
-- [handling of spaces in patterns]
-- spaces may be 'compressed' (i.e will match zero or more spaces)
-- unless this occurs within a number or an identifier. So we mark
-- the four possible imcompressible patterns first and then replace.
-- The possible alnum patterns are v,f,a,d,x,l and u.
local function compress_spaces (s)
s = s:gsub('%$[vifadxlu]%s+%$[vfadxlu]',imcompressible)
s = s:gsub('[%w_]%s+[%w_]',imcompressible)
s = s:gsub('[%w_]%s+%$[vfadxlu]',imcompressible)
s = s:gsub('%$[vfadxlu]%s+[%w_]',imcompressible)
s = s:gsub('%s+','%%s*')
s = s:gsub('\001',' ')
return s
end
--- convert a SIP pattern into the equivalent Lua string pattern.
-- @param spec a SIP pattern
-- @param options a table; only the <code>at_start</code> field is
-- currently meaningful and esures that the pattern is anchored
-- at the start of the string.
-- @return a Lua string pattern.
function sip.create_pattern (spec,options)
assert_arg(1,spec,'string')
local fieldnames,fieldtypes = {},{}
if type(spec) == 'string' then
spec = escape(spec)
else
local res = {}
for i,s in ipairs(spec) do
res[i] = escape(s)
end
spec = concat(res,'.-')
end
local kount = 1
local function addfield (name,type)
if not name then name = kount end
if fieldnames then append(fieldnames,name) end
if fieldtypes then fieldtypes[name] = type end
kount = kount + 1
end
local named_vars, pattern
named_vars = spec:find('{%a+}')
pattern = '%$%S'
if options and options.at_start then
spec = '^'..spec
end
if spec:sub(-1,-1) == '$' then
spec = spec:sub(1,-2)..'$r'
if named_vars then spec = spec..'{rest}' end
end
local names
if named_vars then
names = {}
spec = spec:gsub('{(%a+)}',function(name)
append(names,name)
return ''
end)
end
spec = compress_spaces(spec)
local k = 1
local err
local r = (spec:gsub(pattern,function(s)
local type,name
type = s:sub(2,2)
if names then name = names[k]; k=k+1 end
-- this kludge is necessary because %q generates two matches, and
-- we want to ignore the first. Not a problem for named captures.
if not names and type == 'q' then
addfield(nil,'Q')
else
addfield(name,type)
end
local res
if type == 'v' then
res = group(patterns.IDEN)
elseif type == 'i' then
res = group(patterns.INTEGER)
elseif type == 'f' then
res = group(patterns.FLOAT)
elseif type == 'r' then
res = '(%S.*)'
elseif type == 'q' then
-- some Lua pattern matching voodoo; we want to match '...' as
-- well as "...", and can use the fact that %n will match a
-- previous capture. Adding the extra field above comes from needing
-- to accomodate the extra spurious match (which is either ' or ")
addfield(name,type)
res = '(["\'])(.-)%'..(kount-2)
elseif type == 'p' then
res = '([%a]?[:]?[\\/%.%w_]+)'
else
local endbracket = brackets[type]
if endbracket then
res = '(%b'..type..endbracket..')'
elseif stdclasses[type] or stdclasses[type:lower()] then
res = '(%'..type..'+)'
else
err = "unknown format type or character class"
end
end
return res
end))
--print(r,err)
if err then
return nil,err
else
return r,fieldnames,fieldtypes
end
end
local function tnumber (s)
return s == 'd' or s == 'i' or s == 'f'
end
function sip.create_spec_fun(spec,options)
local fieldtypes,fieldnames
local ls = {}
spec,fieldnames,fieldtypes = sip.create_pattern(spec,options)
if not spec then return spec,fieldnames end
local named_vars = type(fieldnames[1]) == 'string'
for i = 1,#fieldnames do
append(ls,'mm'..i)
end
local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec)
fun = fun..'\tif not mm1 then return false end\n'
local k=1
for i,f in ipairs(fieldnames) do
if f ~= '_' then
local var = 'mm'..i
if tnumber(fieldtypes[f]) then
var = 'tonumber('..var..')'
elseif brackets[fieldtypes[f]] then
var = var..':sub(2,-2)'
end
if named_vars then
fun = ('%s\tres.%s = %s\n'):format(fun,f,var)
else
if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture
fun = ('%s\tres[%d] = %s\n'):format(fun,k,var)
k = k + 1
end
end
end
end
return fun..'\treturn true\nend)\n', named_vars
end
--- convert a SIP pattern into a matching function.
-- The returned function takes two arguments, the line and an empty table.
-- If the line matched the pattern, then this function return true
-- and the table is filled with field-value pairs.
-- @param spec a SIP pattern
-- @param options optional table; {anywhere=true} will stop pattern anchoring at start
-- @return a function if successful, or nil,<error>
function sip.compile(spec,options)
assert_arg(1,spec,'string')
local fun,names = sip.create_spec_fun(spec,options)
if not fun then return nil,names end
if rawget(_G,'_DEBUG') then print(fun) end
local chunk,err = loadstring(fun,'tmp')
if err then return nil,err end
return chunk(),names
end
local cache = {}
--- match a SIP pattern against a string.
-- @param spec a SIP pattern
-- @param line a string
-- @param res a table to receive values
-- @param options (optional) option table
-- @return true or false
function sip.match (spec,line,res,options)
assert_arg(1,spec,'string')
assert_arg(2,line,'string')
assert_arg(3,res,'table')
if not cache[spec] then
cache[spec] = sip.compile(spec,options)
end
return cache[spec](line,res)
end
--- match a SIP pattern against the start of a string.
-- @param spec a SIP pattern
-- @param line a string
-- @param res a table to receive values
-- @return true or false
function sip.match_at_start (spec,line,res)
return sip.match(spec,line,res,{at_start=true})
end
--- given a pattern and a file object, return an iterator over the results
-- @param spec a SIP pattern
-- @param f a file - use standard input if not specified.
function sip.fields (spec,f)
assert_arg(1,spec,'string')
f = f or io.stdin
local fun,err = sip.compile(spec)
if not fun then return nil,err end
local res = {}
return function()
while true do
local line = f:read()
if not line then return end
if fun(line,res) then
local values = res
res = {}
return unpack(values)
end
end
end
end
--- register a match which will be used in the read function.
-- @param spec a SIP pattern
-- @param fun a function to be called with the results of the match
-- @see read
function sip.pattern (spec,fun)
assert_arg(1,spec,'string')
local pat,named = sip.compile(spec)
append(_patterns,{pat=pat,named=named,callback=fun or false})
end
--- enter a loop which applies all registered matches to the input file.
-- @param f a file object; if nil, then io.stdin is assumed.
function sip.read (f)
local owned,err
f = f or io.stdin
if type(f) == 'string' then
f,err = io.open(f)
if not f then return nil,err end
owned = true
end
local res = {}
for line in f:lines() do
for _,item in ipairs(_patterns) do
if item.pat(line,res) then
if item.callback then
if item.named then
item.callback(res)
else
item.callback(unpack(res))
end
end
res = {}
break
end
end
end
if owned then f:close() end
end
return sip
@@ -0,0 +1,71 @@
--- Checks uses of undeclared global variables.
-- All global variables must be 'declared' through a regular assignment
-- (even assigning nil will do) in a main chunk before being used
-- anywhere or assigned to inside a function.
-- @class module
-- @name pl.strict
require 'debug'
local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget
local handler,hooked
local mt = getmetatable(_G)
if mt == nil then
mt = {}
setmetatable(_G, mt)
elseif mt.hook then
hooked = true
end
-- predeclaring _PROMPT keeps the Lua Interpreter happy
mt.__declared = {_PROMPT=true}
local function what ()
local d = getinfo(3, "S")
return d and d.what or "C"
end
mt.__newindex = function (t, n, v)
if not mt.__declared[n] then
local w = what()
if w ~= "main" and w ~= "C" then
error("assign to undeclared variable '"..n.."'", 2)
end
mt.__declared[n] = true
end
rawset(t, n, v)
end
handler = function(t,n)
if not mt.__declared[n] and what() ~= "C" then
error("variable '"..n.."' is not declared", 2)
end
return rawget(t, n)
end
function package.strict (mod)
local mt = getmetatable(mod)
if mt == nil then
mt = {}
setmetatable(mod, mt)
end
mt.__declared = {}
mt.__newindex = function(t, n, v)
mt.__declared[n] = true
rawset(t, n, v)
end
mt.__index = function(t,n)
if not mt.__declared[n] then
error("variable '"..n.."' is not declared", 2)
end
return rawget(t, n)
end
end
if not hooked then
mt.__index = handler
else
mt.hook(handler)
end
@@ -0,0 +1,144 @@
--- Reading and writing strings using file-like objects. <br>
-- <pre class=example>
-- f = stringio.open(text)
-- l1 = f:read() -- read first line
-- n,m = f:read ('*n','*n') -- read two numbers
-- for line in f:lines() do print(line) end -- iterate over all lines
-- f = stringio.create()
-- f:write('hello')
-- f:write('dolly')
-- assert(f:value(),'hellodolly')
-- </pre>
-- See <a href="../../index.html#stringio">the Guide</a>.
-- @class module
-- @name pl.stringio
local getmetatable,tostring,unpack,tonumber = getmetatable,tostring,unpack,tonumber
local concat,append = table.concat,table.insert
local stringio = {}
--- Writer class
local SW = {}
SW.__index = SW
local function xwrite(self,...)
local args = {...} --arguments may not be nil!
for i = 1, #args do
append(self.tbl,args[i])
end
end
function SW:write(arg1,arg2,...)
if arg2 then
xwrite(self,arg1,arg2,...)
else
append(self.tbl,arg1)
end
end
function SW:writef(fmt,...)
self:write(fmt:format(...))
end
function SW:value()
return concat(self.tbl)
end
function SW:close() -- for compatibility only
end
function SW:seek()
end
--- Reader class
local SR = {}
SR.__index = SR
function SR:_read(fmt)
local i,str = self.i,self.str
local sz = #str
if i >= sz then return nil end
local res
if fmt == nil or fmt == '*l' then
local idx = str:find('\n',i) or (sz+1)
res = str:sub(i,idx-1)
self.i = idx+1
elseif fmt == '*a' then
res = str:sub(i)
self.i = sz
elseif fmt == '*n' then
local _,i2,i2,idx
_,idx = str:find ('%s*%d+',i)
_,i2 = str:find ('%.%d+',idx+1)
if i2 then idx = i2 end
_,i2 = str:find ('[eE][%+%-]*%d+',idx+1)
if i2 then idx = i2 end
local val = str:sub(i,idx)
res = tonumber(val)
self.i = idx+1
elseif type(fmt) == 'number' then
res = str:sub(i,i+fmt-1)
self.i = i + fmt
else
error("bad read format",2)
end
return res
end
function SR:read(...)
local fmts = {...}
if #fmts <= 1 then
return self:_read(fmts[1])
else
local res = {}
for i = 1, #fmts do
res[i] = self:_read(fmts[i])
end
return unpack(res)
end
end
function SR:seek(whence,offset)
local base
whence = whence or 'cur'
offset = offset or 0
if whence == 'set' then
base = 1
elseif whence == 'cur' then
base = self.i
elseif whence == 'end' then
base = #self.str
end
self.i = base + offset
return self.i
end
function SR:lines()
return function()
return self:read()
end
end
function SR:close() -- for compatibility only
end
--- create a file-like object which can be used to construct a string.
-- The resulting object has an extra <code>value()</code> method for
-- retrieving the string value.
-- @usage f = create(); f:write('hello, dolly\n'); print(f:value())
function stringio.create()
return setmetatable({tbl={}},SW)
end
--- create a file-like object for reading from a given string.
-- @param s The input string.
function stringio.open(s)
return setmetatable({str=s,i=1},SR)
end
function stringio.lines(s)
return stringio.open(s):lines()
end
return stringio
@@ -0,0 +1,441 @@
--- Python-style string library. <p>
-- see 3.6.1 of the Python reference. <p>
-- If you want to make these available as string methods, then say
-- <code>stringx.import()</code> to bring them into the standard <code>string</code>
-- table.
-- @class module
-- @name pl.stringx
local string = string
local find = string.find
local type,setmetatable,getmetatable,ipairs,unpack = type,setmetatable,getmetatable,ipairs,unpack
local error,tostring = error,tostring
local gsub = string.gsub
local rep = string.rep
local sub = string.sub
local concat = table.concat
local utils = require 'pl.utils'
local escape = utils.escape
local ceil = math.ceil
local _G = _G
local assert_arg,usplit,list_MT = utils.assert_arg,utils.split,utils.stdmt.List
local lstrip
local function assert_string (n,s)
assert_arg(n,s,'string')
end
local function non_empty(s)
return #s > 0
end
local function assert_nonempty_string(n,s)
assert_arg(n,s,'string',non_empty,'must be a non-empty string')
end
--[[
module ('pl.stringx',utils._module)
]]
local stringx = {}
--- does s only contain alphabetic characters?.
-- @param s a string
function stringx.isalpha(s)
assert_string(1,s)
return find(s,'^%a+$') == 1
end
--- does s only contain digits?.
-- @param s a string
function stringx.isdigit(s)
assert_string(1,s)
return find(s,'^%d+$') == 1
end
--- does s only contain alphanumeric characters?.
-- @param s a string
function stringx.isalnum(s)
assert_string(1,s)
return find(s,'^%w+$') == 1
end
--- does s only contain spaces?.
-- @param s a string
function stringx.isspace(s)
assert_string(1,s)
return find(s,'^%s+$') == 1
end
--- does s only contain lower case characters?.
-- @param s a string
function stringx.islower(s)
assert_string(1,s)
return find(s,'^[%l%s]+$') == 1
end
--- does s only contain upper case characters?.
-- @param s a string
function stringx.isupper(s)
assert_string(1,s)
return find(s,'^[%u%s]+$') == 1
end
--- concatenate the strings using this string as a delimiter.
-- @param self the string
-- @param seq a table of strings or numbers
-- @usage (' '):join {1,2,3} == '1 2 3'
function stringx.join (self,seq)
assert_string(1,self)
return concat(seq,self)
end
--- does string start with the substring?.
-- @param self the string
-- @param s2 a string
function stringx.startswith(self,s2)
assert_string(1,self)
assert_string(2,s2)
return find(self,s2,1,true) == 1
end
local function _find_all(s,sub,first,last)
if sub == '' then return #s+1,#s end
local i1,i2 = find(s,sub,first,true)
local res
local k = 0
while i1 do
res = i1
k = k + 1
i1,i2 = find(s,sub,i2+1,true)
if last and i1 > last then break end
end
return res,k
end
--- does string end with the given substring?.
-- @param s a string
-- @param send a substring or a table of suffixes
function stringx.endswith(s,send)
assert_string(1,s)
if type(send) == 'string' then
return #s >= #send and s:find(send, #s-#send+1, true) and true or false
elseif type(send) == 'table' then
local endswith = stringx.endswith
for _,suffix in ipairs(send) do
if endswith(s,suffix) then return true end
end
return false
else
error('argument #2: either a substring or a table of suffixes expected')
end
end
-- break string into a list of lines
-- @param self the string
-- @param keepends (currently not used)
function stringx.splitlines (self,keepends)
assert_string(1,self)
local res = usplit(self,'[\r\n]')
-- we are currently hacking around a problem with utils.split (see stringx.split)
if #res == 0 then res = {''} end
return setmetatable(res,list_MT)
end
local function tab_expand (self,n)
return (gsub(self,'([^\t]*)\t', function(s)
return s..(' '):rep(n - #s % n)
end))
end
--- replace all tabs in s with n spaces. If not specified, n defaults to 8.
-- with 0.9.5 this now correctly expands to the next tab stop (if you really
-- want to just replace tabs, use :gsub('\t',' ') etc)
-- @param self the string
-- @param n number of spaces to expand each tab, (default 8)
function stringx.expandtabs(self,n)
assert_string(1,self)
n = n or 8
if not self:find '\n' then return tab_expand(self,n) end
local res,i = {},1
for line in stringx.lines(self) do
res[i] = tab_expand(line,n)
i = i + 1
end
return table.concat(res,'\n')
end
--- find index of first instance of sub in s from the left.
-- @param self the string
-- @param sub substring
-- @param i1 start index
function stringx.lfind(self,sub,i1)
assert_string(1,self)
assert_string(2,sub)
local idx = find(self,sub,i1,true)
if idx then return idx else return nil end
end
--- find index of first instance of sub in s from the right.
-- @param self the string
-- @param sub substring
-- @param first first index
-- @param last last index
function stringx.rfind(self,sub,first,last)
assert_string(1,self)
assert_string(2,sub)
local idx = _find_all(self,sub,first,last)
if idx then return idx else return nil end
end
--- replace up to n instances of old by new in the string s.
-- if n is not present, replace all instances.
-- @param s the string
-- @param old the target substring
-- @param new the substitution
-- @param n optional maximum number of substitutions
-- @return result string
-- @return the number of substitutions
function stringx.replace(s,old,new,n)
assert_string(1,s)
assert_string(1,old)
return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
end
--- split a string into a list of strings using a delimiter.
-- @class function
-- @name split
-- @param self the string
-- @param re a delimiter (defaults to whitespace)
-- @param n maximum number of results
-- @usage #(('one two'):split()) == 2
-- @usage ('one,two,three'):split(',') == List{'one','two','three'}
-- @usage ('one,two,three'):split(',',2) == List{'one','two,three'}
function stringx.split(self,re,n)
local s = self
local plain = true
if not re then -- default spaces
s = lstrip(s)
plain = false
end
local res = usplit(s,re,plain,n)
if re and re ~= '' and find(s,re,-#re,true) then
res[#res+1] = ""
end
return setmetatable(res,list_MT)
end
--- split a string using a pattern. Note that at least one value will be returned!
-- @param self the string
-- @param re a Lua string pattern (defaults to whitespace)
-- @return the parts of the string
-- @usage a,b = line:splitv('=')
function stringx.splitv (self,re)
assert_string(1,self)
return utils.splitv(self,re)
end
local function copy(self)
return self..''
end
--- count all instances of substring in string.
-- @param self the string
-- @param sub substring
function stringx.count(self,sub)
assert_string(1,self)
local i,k = _find_all(self,sub,1)
return k
end
local function _just(s,w,ch,left,right)
local n = #s
if w > n then
if not ch then ch = ' ' end
local f1,f2
if left and right then
local ln = ceil((w-n)/2)
local rn = w - n - ln
f1 = rep(ch,ln)
f2 = rep(ch,rn)
elseif right then
f1 = rep(ch,w-n)
f2 = ''
else
f2 = rep(ch,w-n)
f1 = ''
end
return f1..s..f2
else
return copy(s)
end
end
--- left-justify s with width w.
-- @param self the string
-- @param w width of justification
-- @param ch padding character, default ' '
function stringx.ljust(self,w,ch)
assert_string(1,self)
assert_arg(2,w,'number')
return _just(self,w,ch,true,false)
end
--- right-justify s with width w.
-- @param s the string
-- @param w width of justification
-- @param ch padding character, default ' '
function stringx.rjust(s,w,ch)
assert_string(1,s)
assert_arg(2,w,'number')
return _just(s,w,ch,false,true)
end
--- center-justify s with width w.
-- @param s the string
-- @param w width of justification
-- @param ch padding character, default ' '
function stringx.center(s,w,ch)
assert_string(1,s)
assert_arg(2,w,'number')
return _just(s,w,ch,true,true)
end
local function _strip(s,left,right,chrs)
if not chrs then
chrs = '%s'
else
chrs = '['..escape(chrs)..']'
end
if left then
local i1,i2 = find(s,'^'..chrs..'*')
if i2 >= i1 then
s = sub(s,i2+1)
end
end
if right then
local i1,i2 = find(s,chrs..'*$')
if i2 >= i1 then
s = sub(s,1,i1-1)
end
end
return s
end
--- trim any whitespace on the left of s.
-- @param self the string
-- @param chrs default space, can be a string of characters to be trimmed
function stringx.lstrip(self,chrs)
assert_string(1,self)
return _strip(self,true,false,chrs)
end
lstrip = stringx.lstrip
--- trim any whitespace on the right of s.
-- @param s the string
-- @param chrs default space, can be a string of characters to be trimmed
function stringx.rstrip(s,chrs)
assert_string(1,s)
return _strip(s,false,true,chrs)
end
--- trim any whitespace on both left and right of s.
-- @param self the string
-- @param chrs default space, can be a string of characters to be trimmed
function stringx.strip(self,chrs)
assert_string(1,self)
return _strip(self,true,true,chrs)
end
-- The partition functions split a string using a delimiter into three parts:
-- the part before, the delimiter itself, and the part afterwards
local function _partition(p,delim,fn)
local i1,i2 = fn(p,delim)
if not i1 or i1 == -1 then
return p,'',''
else
if not i2 then i2 = i1 end
return sub(p,1,i1-1),sub(p,i1,i2),sub(p,i2+1)
end
end
--- partition the string using first occurance of a delimiter
-- @param self the string
-- @param ch delimiter
-- @return part before ch
-- @return ch
-- @return part after ch
function stringx.partition(self,ch)
assert_string(1,self)
assert_nonempty_string(2,ch)
return _partition(self,ch,stringx.lfind)
end
--- partition the string p using last occurance of a delimiter
-- @param self the string
-- @param ch delimiter
-- @return part before ch
-- @return ch
-- @return part after ch
function stringx.rpartition(self,ch)
assert_string(1,self)
assert_nonempty_string(2,ch)
return _partition(self,ch,stringx.rfind)
end
--- return the 'character' at the index.
-- @param self the string
-- @param idx an index (can be negative)
-- @return a substring of length 1 if successful, empty string otherwise.
function stringx.at(self,idx)
assert_string(1,self)
assert_arg(2,idx,'number')
return sub(self,idx,idx)
end
--- return an interator over all lines in a string
-- @param self the string
-- @return an iterator
function stringx.lines (self)
assert_string(1,self)
local s = self
if not s:find '\n$' then s = s..'\n' end
return s:gmatch('([^\n]*)\n')
end
--- iniital word letters uppercase ('title case').
-- Here 'words' mean chunks of non-space characters.
-- @param self the string
-- @return a string with each word's first letter uppercase
function stringx.title(self)
return (self:gsub('(%S)(%S*)',function(f,r)
return f:upper()..r:lower()
end))
end
stringx.capitalize = stringx.title
local elipsis = '...'
local n_elipsis = #elipsis
--- return a shorted version of a string.
-- @param self the string
-- @param sz the maxinum size allowed
-- @param tail true if we want to show the end of the string (head otherwise)
function stringx.shorten(self,sz,tail)
if #self > sz then
if sz < n_elipsis then return elipsis:sub(1,sz) end
if tail then
local i = #self - sz + 1 + n_elipsis
return elipsis .. self:sub(i)
else
return self:sub(1,sz-n_elipsis) .. elipsis
end
end
return self
end
function stringx.import(dont_overload)
utils.import(stringx,string)
end
return stringx
@@ -0,0 +1,766 @@
--- Extended operations on Lua tables.
-- @class module
-- @name pl.tablex
local getmetatable,setmetatable,require = getmetatable,setmetatable,require
local append,remove = table.insert,table.remove
local min,max = math.min,math.max
local pairs,type,unpack,next,ipairs,select,tostring = pairs,type,unpack,next,ipairs,select,tostring
local utils = require ('pl.utils')
local function_arg = utils.function_arg
local Set = utils.stdmt.Set
local List = utils.stdmt.List
local Map = utils.stdmt.Map
local assert_arg = utils.assert_arg
--[[
module ('pl.tablex',utils._module)
]]
local tablex = {}
-- generally, functions that make copies of tables try to preserve the metatable.
-- However, when the source has no obvious type, then we attach appropriate metatables
-- like List, Map, etc to the result.
local function setmeta (res,tbl,def)
return setmetatable(res,getmetatable(tbl) or def)
end
local function makelist (res)
return setmetatable(res,List)
end
--- copy a table into another, in-place.
-- @param t1 destination table
-- @param t2 source table
-- @return first table
function tablex.update (t1,t2)
assert_arg(1,t1,'table')
assert_arg(2,t2,'table')
for k,v in pairs(t2) do
t1[k] = v
end
return t1
end
--- total number of elements in this table. <br>
-- Note that this is distinct from #t, which is the number
-- of values in the array part; this value will always
-- be greater or equal. The difference gives the size of
-- the hash part, for practical purposes.
-- @param t a table
-- @return the size
function tablex.size (t)
assert_arg(1,t,'table')
local i = 0
for k in pairs(t) do i = i + 1 end
return i
end
--- make a shallow copy of a table
-- @param t source table
-- @return new table
function tablex.copy (t)
assert_arg(1,t,'table')
local res = {}
for k,v in pairs(t) do
res[k] = v
end
return res
end
--- make a deep copy of a table, recursively copying all the keys and fields.
-- This will also set the copied table's metatable to that of the original.
-- @param t A table
-- @return new table
function tablex.deepcopy(t)
assert_arg(1,t,'table')
if type(t) ~= 'table' then return t end
local mt = getmetatable(t)
local res = {}
for k,v in pairs(t) do
if type(v) == 'table' then
v = tablex.deepcopy(v)
end
res[k] = v
end
setmetatable(res,mt)
return res
end
local abs = math.abs
--- compare two values.
-- if they are tables, then compare their keys and fields recursively.
-- @param t1 A value
-- @param t2 A value
-- @param ignore_mt if true, ignore __eq metamethod (default false)
-- @param eps if defined, then used for any number comparisons
-- @return true or false
function tablex.deepcompare(t1,t2,ignore_mt,eps)
local ty1 = type(t1)
local ty2 = type(t2)
if ty1 ~= ty2 then return false end
-- non-table types can be directly compared
if ty1 ~= 'table' then
if ty1 == 'number' and eps then return abs(t1-t2) < eps end
return t1 == t2
end
-- as well as tables which have the metamethod __eq
local mt = getmetatable(t1)
if not ignore_mt and mt and mt.__eq then return t1 == t2 end
for k1,v1 in pairs(t1) do
local v2 = t2[k1]
if v2 == nil or not tablex.deepcompare(v1,v2,ignore_mt,eps) then return false end
end
for k2,v2 in pairs(t2) do
local v1 = t1[k2]
if v1 == nil or not tablex.deepcompare(v1,v2,ignore_mt,eps) then return false end
end
return true
end
--- compare two list-like tables using a predicate.
-- @param t1 a table
-- @param t2 a table
-- @param cmp A comparison function
function tablex.compare (t1,t2,cmp)
assert_arg(1,t1,'table')
assert_arg(2,t2,'table')
if #t1 ~= #t2 then return false end
cmp = function_arg(3,cmp)
for k in ipairs(t1) do
if not cmp(t1[k],t2[k]) then return false end
end
return true
end
--- compare two list-like tables using an optional predicate, without regard for element order.
-- @param t1 a list-like table
-- @param t2 a list-like table
-- @param cmp A comparison function (may be nil)
function tablex.compare_no_order (t1,t2,cmp)
assert_arg(1,t1,'table')
assert_arg(2,t2,'table')
if cmp then cmp = function_arg(3,cmp) end
if #t1 ~= #t2 then return false end
local visited = {}
for i = 1,#t1 do
local val = t1[i]
local gotcha
for j = 1,#t2 do if not visited[j] then
local match
if cmp then match = cmp(val,t2[j]) else match = val == t2[j] end
if match then
gotcha = j
break
end
end end
if not gotcha then return false end
visited[gotcha] = true
end
return true
end
--- return the index of a value in a list.
-- Like string.find, there is an optional index to start searching,
-- which can be negative.
-- @param t A list-like table (i.e. with numerical indices)
-- @param val A value
-- @param idx index to start; -1 means last element,etc (default 1)
-- @return index of value or nil if not found
-- @usage find({10,20,30},20) == 2
-- @usage find({'a','b','a','c'},'a',2) == 3
function tablex.find(t,val,idx)
assert_arg(1,t,'table')
idx = idx or 1
if idx < 0 then idx = #t + idx + 1 end
for i = idx,#t do
if t[i] == val then return i end
end
return nil
end
--- return the index of a value in a list, searching from the end.
-- Like string.find, there is an optional index to start searching,
-- which can be negative.
-- @param t A list-like table (i.e. with numerical indices)
-- @param val A value
-- @param idx index to start; -1 means last element,etc (default 1)
-- @return index of value or nil if not found
-- @usage rfind({10,10,10},10) == 3
function tablex.rfind(t,val,idx)
assert_arg(1,t,'table')
idx = idx or #t
if idx < 0 then idx = #t + idx + 1 end
for i = idx,1,-1 do
if t[i] == val then return i end
end
return nil
end
--- return the index (or key) of a value in a table using a comparison function.
-- @param t A table
-- @param cmp A comparison function
-- @param arg an optional second argument to the function
-- @return index of value, or nil if not found
-- @return value returned by comparison function
function tablex.find_if(t,cmp,arg)
assert_arg(1,t,'table')
cmp = function_arg(2,cmp)
for k,v in pairs(t) do
local c = cmp(v,arg)
if c then return k,c end
end
return nil
end
--- return a list of all values in a table indexed by another list.
-- @param tbl a table
-- @param idx an index table (a list of keys)
-- @return a list-like table
-- @usage index_by({10,20,30,40},{2,4}) == {20,40}
-- @usage index_by({one=1,two=2,three=3},{'one','three'}) == {1,3}
function tablex.index_by(tbl,idx)
assert_arg(1,tbl,'table')
assert_arg(2,idx,'table')
local res = {}
for _,i in ipairs(idx) do
append(res,tbl[i])
end
return setmeta(res,tbl,List)
end
--- apply a function to all values of a table.
-- This returns a table of the results.
-- Any extra arguments are passed to the function.
-- @param fun A function that takes at least one argument
-- @param t A table
-- @param ... optional arguments
-- @usage map(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900,fred=4}
function tablex.map(fun,t,...)
assert_arg(1,t,'table')
fun = function_arg(1,fun)
local res = {}
for k,v in pairs(t) do
res[k] = fun(v,...)
end
return setmeta(res,t)
end
--- apply a function to all values of a list.
-- This returns a table of the results.
-- Any extra arguments are passed to the function.
-- @param fun A function that takes at least one argument
-- @param t a table (applies to array part)
-- @param ... optional arguments
-- @return a list-like table
-- @usage imap(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900}
function tablex.imap(fun,t,...)
assert_arg(1,t,'table')
fun = function_arg(1,fun)
local res = {}
for i = 1,#t do
res[i] = fun(t[i],...) or false
end
return setmeta(res,t,List)
end
--- apply a named method to values from a table.
-- @param name the method name
-- @param t a list-like table
-- @param ... any extra arguments to the method
function tablex.map_named_method (name,t,...)
assert_arg(1,name,'string')
assert_arg(2,t,'table')
local res = {}
for i = 1,#t do
local val = t[i]
local fun = val[name]
res[i] = fun(val,...)
end
return setmeta(res,t,List)
end
--- apply a function to all values of a table, in-place.
-- Any extra arguments are passed to the function.
-- @param fun A function that takes at least one argument
-- @param t a table
-- @param ... extra arguments
function tablex.transform (fun,t,...)
assert_arg(1,t,'table')
fun = function_arg(1,fun)
for k,v in pairs(t) do
t[v] = fun(v,...)
end
end
--- generate a table of all numbers in a range
-- @param start number
-- @param finish number
-- @param step optional increment (default 1 for increasing, -1 for decreasing)
function tablex.range (start,finish,step)
local res = {}
local k = 1
if not step then
if finish > start then step = finish > start and 1 or -1 end
end
for i=start,finish,step do res[k]=i; k=k+1 end
return res
end
--- apply a function to values from two tables.
-- @param fun a function of at least two arguments
-- @param t1 a table
-- @param t2 a table
-- @param ... extra arguments
-- @return a table
-- @usage map2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23,m=44}
function tablex.map2 (fun,t1,t2,...)
assert_arg(1,t1,'table')
assert_arg(2,t2,'table')
fun = function_arg(1,fun)
local res = {}
for k,v in pairs(t1) do
res[k] = fun(v,t2[k],...)
end
return setmeta(res,t1,List)
end
--- apply a function to values from two arrays.
-- @param fun a function of at least two arguments
-- @param t1 a list-like table
-- @param t2 a list-like table
-- @param ... extra arguments
-- @usage imap2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23}
function tablex.imap2 (fun,t1,t2,...)
assert_arg(2,t1,'table')
assert_arg(3,t2,'table')
fun = function_arg(1,fun)
local res = {}
for i = 1,#t1 do
res[i] = fun(t1[i],t2[i],...)
end
return res
end
--- 'reduce' a list using a binary function.
-- @param fun a function of two arguments
-- @param t a list-like table
-- @return the result of the function
-- @usage reduce('+',{1,2,3,4}) == 10
function tablex.reduce (fun,t)
assert_arg(2,t,'table')
fun = function_arg(1,fun)
local n = #t
local res = t[1]
for i = 2,n do
res = fun(res,t[i])
end
return res
end
--- apply a function to all elements of a table.
-- The arguments to the function will be the value,
-- the key and <i>finally</i> any extra arguments passed to this function.
-- Note that the Lua 5.0 function table.foreach passed the <i>key</i> first.
-- @param t a table
-- @param fun a function with at least one argument
-- @param ... extra arguments
function tablex.foreach(t,fun,...)
assert_arg(1,t,'table')
fun = function_arg(2,fun)
for k,v in pairs(t) do
fun(v,k,...)
end
end
--- apply a function to all elements of a list-like table in order.
-- The arguments to the function will be the value,
-- the index and <i>finally</i> any extra arguments passed to this function
-- @param t a table
-- @param fun a function with at least one argument
-- @param ... optional arguments
function tablex.foreachi(t,fun,...)
assert_arg(1,t,'table')
fun = function_arg(2,fun)
for k,v in ipairs(t) do
fun(v,k,...)
end
end
--- Apply a function to a number of tables.
-- A more general version of map
-- The result is a table containing the result of applying that function to the
-- ith value of each table. Length of output list is the minimum length of all the lists
-- @param fun a function of n arguments
-- @param ... n tables
-- @usage mapn(function(x,y,z) return x+y+z end, {1,2,3},{10,20,30},{100,200,300}) is {111,222,333}
-- @usage mapn(math.max, {1,20,300},{10,2,3},{100,200,100}) is {100,200,300}
-- @param fun A function that takes as many arguments as there are tables
function tablex.mapn(fun,...)
fun = function_arg(1,fun)
local res = {}
local lists = {...}
local minn = 1e40
for i = 1,#lists do
minn = min(minn,#(lists[i]))
end
for i = 1,minn do
local args = {}
for j = 1,#lists do
args[#args+1] = lists[j][i]
end
res[#res+1] = fun(unpack(args))
end
return res
end
--- call the function with the key and value pairs from a table.
-- The function can return a value and a key (note the order!). If both
-- are not nil, then this pair is inserted into the result. If only value is not nil, then
-- it is appended to the result.
-- @param fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap.
-- @param t A table
-- @param ... optional arguments
-- @usage pairmap({fred=10,bonzo=20},function(k,v) return v end) is {10,20}
-- @usage pairmap({one=1,two=2},function(k,v) return {k,v},k end) is {one={'one',1},two={'two',2}}
function tablex.pairmap(fun,t,...)
assert_arg(1,t,'table')
fun = function_arg(1,fun)
local res = {}
for k,v in pairs(t) do
local rv,rk = fun(k,v,...)
if rk then
res[rk] = rv
else
res[#res+1] = rv
end
end
return res
end
local function keys_op(i,v) return i end
--- return all the keys of a table in arbitrary order.
-- @param t A table
function tablex.keys(t)
assert_arg(1,t,'table')
return makelist(tablex.pairmap(keys_op,t))
end
local function values_op(i,v) return v end
--- return all the values of the table in arbitrary order
-- @param t A table
function tablex.values(t)
assert_arg(1,t,'table')
return makelist(tablex.pairmap(values_op,t))
end
local function index_map_op (i,v) return i,v end
--- create an index map from a list-like table. The original values become keys,
-- and the associated values are the indices into the original list.
-- @param t a list-like table
-- @return a map-like table
function tablex.index_map (t)
assert_arg(1,t,'table')
return setmetatable(tablex.pairmap(index_map_op,t),Map)
end
local function set_op(i,v) return true,v end
--- create a set from a list-like table. A set is a table where the original values
-- become keys, and the associated values are all true.
-- @param t a list-like table
-- @return a set (a map-like table)
function tablex.makeset (t)
assert_arg(1,t,'table')
return setmetatable(tablex.pairmap(set_op,t),Set)
end
--- combine two tables, either as union or intersection. Corresponds to
-- set operations for sets () but more general. Not particularly
-- useful for list-like tables.
-- @param t1 a table
-- @param t2 a table
-- @param dup true for a union, false for an intersection.
-- @usage merge({alice=23,fred=34},{bob=25,fred=34}) is {fred=34}
-- @usage merge({alice=23,fred=34},{bob=25,fred=34},true) is {bob=25,fred=34,alice=23}
-- @see tablex.index_map
function tablex.merge (t1,t2,dup)
assert_arg(1,t1,'table')
assert_arg(2,t2,'table')
local res = {}
for k,v in pairs(t1) do
if dup or t2[k] then res[k] = v end
end
for k,v in pairs(t2) do
if dup or t1[k] then res[k] = v end
end
return setmeta(res,t1,Map)
end
--- a new table which is the difference of two tables.
-- With sets (where the values are all true) this is set difference and
-- symmetric difference depending on the third parameter.
-- @param s1 a map-like table or set
-- @param s2 a map-like table or set
-- @param symm symmetric difference (default false)
-- @return a map-like table or set
function tablex.difference (s1,s2,symm)
assert_arg(1,s1,'table')
assert_arg(2,s2,'table')
local res = {}
for k,v in pairs(s1) do
if not s2[k] then res[k] = v end
end
if symm then
for k,v in pairs(s2) do
if not s1[k] then res[k] = v end
end
end
return setmeta(res,s1,Map)
end
--- A table where the key/values are the values and value counts of the table.
-- @param t a list-like table
-- @param cmp a function that defines equality (otherwise uses ==)
-- @return a map-like table
-- @see seq.count_map
function tablex.count_map (t,cmp)
assert_arg(1,t,'table')
local res,mask = {},{}
cmp = function_arg(2,cmp)
local n = #t
for i,v in ipairs(t) do
if not mask[v] then
mask[v] = true
-- check this value against all other values
res[v] = 1 -- there's at least one instance
for j = i+1,n do
local w = t[j]
if cmp and cmp(v,w) or v == w then
res[v] = res[v] + 1
mask[w] = true
end
end
end
end
return setmetatable(res,Map)
end
--- filter a table's values using a predicate function
-- @param t a list-like table
-- @param pred a boolean function
-- @param arg optional argument to be passed as second argument of the predicate
function tablex.filter (t,pred,arg)
assert_arg(1,t,'table')
pred = function_arg(2,pred)
local res = {}
for k,v in ipairs(t) do
if pred(v,arg) then append(res,v) end
end
return setmeta(res,t,List)
end
--- return a table where each element is a table of the ith values of an arbitrary
-- number of tables. It is equivalent to a matrix transpose.
-- @usage zip({10,20,30},{100,200,300}) is {{10,100},{20,200},{30,300}}
function tablex.zip(...)
return tablex.mapn(function(...) return {...} end,...)
end
local _copy
function _copy (dest,src,idest,isrc,nsrc,clean_tail)
idest = idest or 1
isrc = isrc or 1
local iend
if not nsrc then
nsrc = #src
iend = #src
else
iend = isrc + min(nsrc-1,#src-isrc)
end
if dest == src then -- special case
if idest > isrc and iend >= idest then -- overlapping ranges
src = tablex.sub(src,isrc,nsrc)
isrc = 1; iend = #src
end
end
for i = isrc,iend do
dest[idest] = src[i]
idest = idest + 1
end
if clean_tail then
tablex.clear(dest,idest)
end
return dest
end
--- copy an array into another one, resizing the destination if necessary. <br>
-- @param dest a list-like table
-- @param src a list-like table
-- @param idest where to start copying values from source (default 1)
-- @param isrc where to start copying values into destination (default 1)
-- @param nsrc number of elements to copy from source (default source size)
function tablex.icopy (dest,src,idest,isrc,nsrc)
assert_arg(1,dest,'table')
assert_arg(2,src,'table')
return _copy(dest,src,idest,isrc,nsrc,true)
end
--- copy an array into another one. <br>
-- @param dest a list-like table
-- @param src a list-like table
-- @param idest where to start copying values from source (default 1)
-- @param isrc where to start copying values into destination (default 1)
-- @param nsrc number of elements to copy from source (default source size)
function tablex.move (dest,src,idest,isrc,nsrc)
assert_arg(1,dest,'table')
assert_arg(2,src,'table')
return _copy(dest,src,idest,isrc,nsrc,false)
end
function tablex._normalize_slice(self,first,last)
local sz = #self
if not first then first=1 end
if first<0 then first=sz+first+1 end
-- make the range _inclusive_!
if not last then last=sz end
if last < 0 then last=sz+1+last end
return first,last
end
--- Extract a range from a table, like 'string.sub'.
-- If first or last are negative then they are relative to the end of the list
-- eg. sub(t,-2) gives last 2 entries in a list, and
-- sub(t,-4,-2) gives from -4th to -2nd
-- @param t a list-like table
-- @param first An index
-- @param last An index
-- @return a new List
function tablex.sub(t,first,last)
assert_arg(1,t,'table')
first,last = tablex._normalize_slice(t,first,last)
local res={}
for i=first,last do append(res,t[i]) end
return setmeta(res,t,List)
end
--- set an array range to a value. If it's a function we use the result
-- of applying it to the indices.
-- @param t a list-like table
-- @param val a value
-- @param i1 start range (default 1)
-- @param i2 end range (default table size)
function tablex.set (t,val,i1,i2)
i1,i2 = i1 or 1,i2 or #t
if utils.is_callable(val) then
for i = i1,i2 do
t[i] = val(i)
end
else
for i = i1,i2 do
t[i] = val
end
end
end
--- create a new array of specified size with initial value.
-- @param n size
-- @param val initial value (can be nil, but don't expect # to work!)
-- @return the table
function tablex.new (n,val)
local res = {}
tablex.set(res,val,1,n)
return res
end
--- clear out the contents of a table.
-- @param t a table
-- @param istart optional start position
function tablex.clear(t,istart)
istart = istart or 1
for i = istart,#t do remove(t) end
end
--- insert values into a table. <br>
-- insertvalues(t, [pos,] values) <br>
-- similar to table.insert but inserts values from given table "values",
-- not the object itself, into table "t" at position "pos".
function tablex.insertvalues(t, ...)
local pos, values
if select('#', ...) == 1 then
pos,values = #t+1, ...
else
pos,values = ...
end
if #values > 0 then
for i=#t,pos,-1 do
t[i+#values] = t[i]
end
local offset = 1 - pos
for i=pos,pos+#values-1 do
t[i] = values[i + offset]
end
end
return t
end
--- remove a range of values from a table.
-- @param t a list-like table
-- @param i1 start index
-- @param i2 end index
-- @return the table
function tablex.removevalues (t,i1,i2)
i1,i2 = tablex._normalize_slice(t,i1,i2)
for i = i1,i2 do
remove(t,i1)
end
return t
end
local _find
_find = function (t,value,tables)
for k,v in pairs(t) do
if v == value then return k end
end
for k,v in pairs(t) do
if not tables[v] and type(v) == 'table' then
tables[v] = true
local res = _find(v,value,tables)
if res then
res = tostring(res)
if type(k) ~= 'string' then
return '['..k..']'..res
else
return k..'.'..res
end
end
end
end
end
--- find a value in a table by recursive search.
-- @param t the table
-- @param value the value
-- @param exclude any tables to avoid searching
-- @usage search(_G,math.sin,{package.path}) == 'math.sin'
-- @return a fieldspec, e.g. 'a.b' or 'math.sin'
function tablex.search (t,value,exclude)
assert_arg(1,t,'table')
local tables = {[t]=true}
if exclude then
for _,v in pairs(exclude) do tables[v] = true end
end
return _find(t,value,tables)
end
return tablex
@@ -0,0 +1,99 @@
--- A template preprocessor.
-- Originally by <a href="http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor">Ricki Lake</a>
-- <p>There are two rules: <ul>
-- <li>lines starting with # are Lua</li>
-- <li> otherwise, `$(expr)` is the result of evaluating `expr`</li>
-- </ul>
-- <pre class=example>
-- # for i = 1,3 do
-- $(i) Hello, Word!
-- # end
-- </pre>
-- Other escape characters can be used, when the defaults conflict
-- with the output language.
-- <pre class=example>
-- > for _,n in pairs{'one','two','three'} do
-- static int l_${n} (luaState *state);
-- > end
-- </pre>
-- See <a href="../../index.html#rici_templates">the Guide</a>.
-- @class module
-- @name pl.template
--[[
module('pl.template')
]]
local utils = require 'pl.utils'
local append,format = table.insert,string.format
local function parseHashLines(chunk,brackets,esc)
local exec_pat = "()$(%b"..brackets..")()"
local function parseDollarParen(pieces, chunk, s, e)
local s = 1
for term, executed, e in chunk:gmatch (exec_pat) do
executed = '('..executed:sub(2,-2)..')'
append(pieces,
format("%q..(%s or '')..",chunk:sub(s, term - 1), executed))
s = e
end
append(pieces, format("%q", chunk:sub(s)))
end
local esc_pat = esc.."+([^\n]*\n?)"
local esc_pat1, esc_pat2 = "^"..esc_pat, "\n"..esc_pat
local pieces, s = {"return function(_put) ", n = 1}, 1
while true do
local ss, e, lua = chunk:find (esc_pat1, s)
if not e then
ss, e, lua = chunk:find(esc_pat2, s)
append(pieces, "_put(")
parseDollarParen(pieces, chunk:sub(s, ss))
append(pieces, ")")
if not e then break end
end
append(pieces, lua)
s = e + 1
end
append(pieces, " end")
return table.concat(pieces)
end
local template = {}
--- expand the template using the specified environment.
-- @param str the template string
-- @param env the environment (by default empty). <br>
-- There are three special fields in the environment table <ul>
-- <li><code>_parent</code> continue looking up in this table</li>
-- <li><code>_brackets</code>; default is '()', can be any suitable bracket pair</li>
-- <li><code>_escape</code>; default is '#' </li>
-- </ul>
function template.substitute(str,env)
env = env or {}
if rawget(env,"_parent") then
setmetatable(env,{__index = env._parent})
end
local brackets = rawget(env,"_brackets") or '()'
local escape = rawget(env,"_escape") or '#'
local code = parseHashLines(str,brackets,escape)
local fn,err = utils.load(code,'TMP','t',env)
if not fn then return nil,err end
fn = fn()
local out = {}
local res,err = xpcall(function() fn(function(s)
out[#out+1] = s
end) end,debug.traceback)
if not res then
if env._debug then print(code) end
return nil,err
end
return table.concat(out)
end
return template
@@ -0,0 +1,116 @@
--- Useful test utilities.
-- @module pl.test
local tablex = require 'pl.tablex'
local utils = require 'pl.utils'
local pretty = require 'pl.pretty'
local path = require 'pl.path'
local print,type = print,type
local clock = os.clock
local debug = require 'debug'
local io,debug = io,debug
local function dump(x)
if type(x) == 'table' and not (getmetatable(x) and getmetatable(x).__tostring) then
return pretty.write(x,' ',true)
else
return tostring(x)
end
end
local test = {}
local function complain (x,y,msg)
local i = debug.getinfo(3)
local err = io.stderr
err:write(path.basename(i.short_src)..':'..i.currentline..': assertion failed\n')
err:write("got:\t",dump(x),'\n')
err:write("needed:\t",dump(y),'\n')
utils.quit(1,msg or "these values were not equal")
end
--- like assert, except takes two arguments that must be equal and can be tables.
-- If they are plain tables, it will use tablex.deepcompare.
-- @param x any value
-- @param y a value equal to x
-- @param eps an optional tolerance for numerical comparisons
function test.asserteq (x,y,eps)
local res = x == y
if not res then
res = tablex.deepcompare(x,y,true,eps)
end
if not res then
complain(x,y)
end
end
--- assert that the first string matches the second.
-- @param s1 a string
-- @param s2 a string
function test.assertmatch (s1,s2)
if not s1:match(s2) then
complain (s1,s2,"these strings did not match")
end
end
function test.assertraise(fn,e)
local ok, err = pcall(unpack(fn))
if not err or err:match(e)==nil then
complain (err,e,"these errors did not match")
end
end
--- a version of asserteq that takes two pairs of values.
-- <code>x1==y1 and x2==y2</code> must be true. Useful for functions that naturally
-- return two values.
-- @param x1 any value
-- @param x2 any value
-- @param y1 any value
-- @param y2 any value
function test.asserteq2 (x1,x2,y1,y2)
if x1 ~= y1 then complain(x1,y1) end
if x2 ~= y2 then complain(x2,y2) end
end
-- tuple type --
local tuple_mt = {}
function tuple_mt.__tostring(self)
local ts = {}
for i=1, self.n do
local s = self[i]
ts[i] = type(s) == 'string' and string.format('%q', s) or tostring(s)
end
return 'tuple(' .. table.concat(ts, ', ') .. ')'
end
function tuple_mt.__eq(a, b)
if a.n ~= b.n then return false end
for i=1, a.n do
if a[i] ~= b[i] then return false end
end
return true
end
--- encode an arbitrary argument list as a tuple.
-- This can be used to compare to other argument lists, which is
-- very useful for testing functions which return a number of values.
-- @usage asserteq(tuple( ('ab'):find 'a'), tuple(1,1))
function test.tuple(...)
return setmetatable({n=select('#', ...), ...}, tuple_mt)
end
--- Time a function. Call the function a given number of times, and report the number of seconds taken,
-- together with a message. Any extra arguments will be passed to the function.
-- @param msg a descriptive message
-- @param n number of times to call the function
-- @param fun the function
-- @param ... optional arguments to fun
function test.timer(msg,n,fun,...)
local start = clock()
for i = 1,n do fun(...) end
utils.printf("%s: took %7.2f sec\n",msg,clock()-start)
end
return test
@@ -0,0 +1,241 @@
--- Text processing utilities. <p>
-- This provides a Template class (modeled after the same from the Python
-- libraries, see string.Template). It also provides similar functions to those
-- found in the textwrap module.
-- See <a href="../../index.html#templates">the Guide</a>.
-- <p>
-- Calling <code>text.format_operator()</code> overloads the % operator for strings to give Python/Ruby style formated output.
-- This is extended to also do template-like substitution for map-like data.
-- <pre class=example>
-- > require 'pl.text'.format_operator()
-- > = '%s = %5.3f' % {'PI',math.pi}
-- PI = 3.142
-- > = '$name = $value' % {name='dog',value='Pluto'}
-- dog = Pluto
-- </pre>
-- @class module
-- @name pl.text
local gsub = string.gsub
local concat,append = table.concat,table.insert
local utils = require 'pl.utils'
local bind1,usplit,assert_arg,is_callable = utils.bind1,utils.split,utils.assert_arg,utils.is_callable
local function lstrip(str) return (str:gsub('^%s+','')) end
local function strip(str) return (lstrip(str):gsub('%s+$','')) end
local function make_list(l) return setmetatable(l,utils.stdmt.List) end
local function split(s,delim) return make_list(usplit(s,delim)) end
local function imap(f,t,...)
local res = {}
for i = 1,#t do res[i] = f(t[i],...) end
return res
end
--[[
module ('pl.text',utils._module)
]]
local text = {}
local function _indent (s,sp)
local sl = split(s,'\n')
return concat(imap(bind1('..',sp),sl),'\n')..'\n'
end
--- indent a multiline string.
-- @param s the string
-- @param n the size of the indent
-- @param ch the character to use when indenting (default ' ')
-- @return indented string
function text.indent (s,n,ch)
assert_arg(1,s,'string')
assert_arg(2,s,'number')
return _indent(s,string.rep(ch or ' ',n))
end
--- dedent a multiline string by removing any initial indent.
-- useful when working with [[..]] strings.
-- @param s the string
-- @return a string with initial indent zero.
function text.dedent (s)
assert_arg(1,s,'string')
local sl = split(s,'\n')
local i1,i2 = sl[1]:find('^%s*')
sl = imap(string.sub,sl,i2+1)
return concat(sl,'\n')..'\n'
end
--- format a paragraph into lines so that they fit into a line width.
-- It will not break long words, so lines can be over the length
-- to that extent.
-- @param s the string
-- @param width the margin width, default 70
-- @return a list of lines
function text.wrap (s,width)
assert_arg(1,s,'string')
width = width or 70
s = s:gsub('\n',' ')
local i,nxt = 1
local lines,line = {}
while i < #s do
nxt = i+width
if s:find("[%w']",nxt) then -- inside a word
nxt = s:find('%W',nxt+1) -- so find word boundary
end
line = s:sub(i,nxt)
i = i + #line
append(lines,strip(line))
end
return make_list(lines)
end
--- format a paragraph so that it fits into a line width.
-- @param s the string
-- @param width the margin width, default 70
-- @return a string
-- @see wrap
function text.fill (s,width)
return concat(text.wrap(s,width),'\n') .. '\n'
end
local Template = {}
text.Template = Template
Template.__index = Template
setmetatable(Template, {
__call = function(obj,tmpl)
return Template.new(tmpl)
end})
function Template.new(tmpl)
assert_arg(1,tmpl,'string')
local res = {}
res.tmpl = tmpl
setmetatable(res,Template)
return res
end
local function _substitute(s,tbl,safe)
local subst
if is_callable(tbl) then
subst = tbl
else
function subst(f)
local s = tbl[f]
if not s then
if safe then
return f
else
error("not present in table "..f)
end
else
return s
end
end
end
local res = gsub(s,'%${([%w_]+)}',subst)
return (gsub(res,'%$([%w_]+)',subst))
end
--- substitute values into a template, throwing an error.
-- This will throw an error if no name is found.
-- @param tbl a table of name-value pairs.
function Template:substitute(tbl)
assert_arg(1,tbl,'table')
return _substitute(self.tmpl,tbl,false)
end
--- substitute values into a template.
-- This version just passes unknown names through.
-- @param tbl a table of name-value pairs.
function Template:safe_substitute(tbl)
assert_arg(1,tbl,'table')
return _substitute(self.tmpl,tbl,true)
end
--- substitute values into a template, preserving indentation. <br>
-- If the value is a multiline string _or_ a template, it will insert
-- the lines at the correct indentation. <br>
-- Furthermore, if a template, then that template will be subsituted
-- using the same table.
-- @param tbl a table of name-value pairs.
function Template:indent_substitute(tbl)
assert_arg(1,tbl,'table')
if not self.strings then
self.strings = split(self.tmpl,'\n')
end
-- the idea is to substitute line by line, grabbing any spaces as
-- well as the $var. If the value to be substituted contains newlines,
-- then we split that into lines and adjust the indent before inserting.
local function subst(line)
return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
local subtmpl
local s = tbl[f]
if not s then error("not present in table "..f) end
if getmetatable(s) == Template then
subtmpl = s
s = s.tmpl
else
s = tostring(s)
end
if s:find '\n' then
s = _indent(s,sp)
end
if subtmpl then return _substitute(s,tbl)
else return s
end
end)
end
local lines = imap(subst,self.strings)
return concat(lines,'\n')..'\n'
end
------- Python-style formatting operator ------
-- (see <a href="http://lua-users.org/wiki/StringInterpolation">the lua-users wiki</a>) --
function text.format_operator()
local format = string.format
-- a more forgiving version of string.format, which applies
-- tostring() to any value with a %s format.
local function formatx (fmt,...)
local args = {...}
local i = 1
for p in fmt:gmatch('%%.') do
if p == '%s' and type(args[i]) ~= 'string' then
args[i] = tostring(args[i])
end
i = i + 1
end
return format(fmt,unpack(args))
end
local function basic_subst(s,t)
return (s:gsub('%$([%w_]+)',t))
end
-- Note this goes further than the original, and will allow these cases:
-- 1. a single value
-- 2. a list of values
-- 3. a map of var=value pairs
-- 4. a function, as in gsub
-- For the second two cases, it uses $-variable substituion.
getmetatable("").__mod = function(a, b)
if b == nil then
return a
elseif type(b) == "table" and getmetatable(b) == nil then
if #b == 0 then -- assume a map-like table
return _substitute(a,b,true)
else
return formatx(a,unpack(b))
end
elseif type(b) == 'function' then
return basic_subst(a,b)
else
return formatx(a,b)
end
end
end
return text
@@ -0,0 +1,529 @@
--- Generally useful routines.
-- See <a href="../../index.html#utils">the Guide</a>.
-- @class module
-- @name pl.utils
local format,gsub,byte = string.format,string.gsub,string.byte
local clock = os.clock
local stdout = io.stdout
local append = table.insert
local collisions = {}
local utils = {}
utils._VERSION = "0.9.4"
utils.dir_separator = _G.package.config:sub(1,1)
--- end this program gracefully.
-- @param code The exit code or a message to be printed
-- @param ... extra arguments for message's format'
-- @see utils.fprintf
function utils.quit(code,...)
if type(code) == 'string' then
utils.fprintf(io.stderr,code,...)
code = -1
else
utils.fprintf(io.stderr,...)
end
io.stderr:write('\n')
os.exit(code)
end
--- print an arbitrary number of arguments using a format.
-- @param fmt The format (see string.format)
-- @param ... Extra arguments for format
function utils.printf(fmt,...)
utils.fprintf(stdout,fmt,...)
end
--- write an arbitrary number of arguments to a file using a format.
-- @param f File handle to write to.
-- @param fmt The format (see string.format).
-- @param ... Extra arguments for format
function utils.fprintf(f,fmt,...)
utils.assert_string(2,fmt)
f:write(format(fmt,...))
end
local function import_symbol(T,k,v,libname)
local key = rawget(T,k)
-- warn about collisions!
if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then
utils.printf("warning: '%s.%s' overrides existing symbol\n",libname,k)
end
rawset(T,k,v)
end
local function lookup_lib(T,t)
for k,v in pairs(T) do
if v == t then return k end
end
return '?'
end
local already_imported = {}
--- take a table and 'inject' it into the local namespace.
-- @param t The Table
-- @param T An optional destination table (defaults to callers environment)
function utils.import(t,T)
T = T or _G
t = t or utils
if type(t) == 'string' then
t = require (t)
end
local libname = lookup_lib(T,t)
if already_imported[t] then return end
already_imported[t] = libname
for k,v in pairs(t) do
import_symbol(T,k,v,libname)
end
end
utils.patterns = {
FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
INTEGER = '[+%-%d]%d*',
IDEN = '[%a_][%w_]*',
FILE = '[%a%.\\][:%][%w%._%-\\]*'
}
--- escape any 'magic' characters in a string
-- @param s The input string
function utils.escape(s)
utils.assert_string(1,s)
return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
end
--- return either of two values, depending on a condition.
-- @param cond A condition
-- @param value1 Value returned if cond is true
-- @param value2 Value returned if cond is false (can be optional)
function utils.choose(cond,value1,value2)
if cond then return value1
else return value2
end
end
local raise
--- return the contents of a file as a string
-- @param filename The file path
-- @param is_bin open in binary mode
-- @return file contents
function utils.readfile(filename,is_bin)
local mode = is_bin and 'b' or ''
utils.assert_string(1,filename)
local f,err = io.open(filename,'r'..mode)
if not f then return utils.raise (err) end
local res,err = f:read('*a')
f:close()
if not res then return raise (err) end
return res
end
--- write a string to a file
-- @param filename The file path
-- @param str The string
-- @return true or nil
-- @return error message
-- @raise error if filename or str aren't strings
function utils.writefile(filename,str)
utils.assert_string(1,filename)
utils.assert_string(2,str)
local f,err = io.open(filename,'w')
if not f then return raise(err) end
f:write(str)
f:close()
return true
end
--- return the contents of a file as a list of lines
-- @param filename The file path
-- @return file contents as a table
-- @raise errror if filename is not a string
function utils.readlines(filename)
utils.assert_string(1,filename)
local f,err = io.open(filename,'r')
if not f then return raise(err) end
local res = {}
for line in f:lines() do
append(res,line)
end
f:close()
return res
end
--- 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 plain don't use Lua patterns
-- @param n optional maximum number of splits
-- @return a list-like table
-- @raise error if s is not a string
function utils.split(s,re,plain,n)
utils.assert_string(1,s)
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,plain)
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
--- split a string into a number of values.
-- @param s the string
-- @param re the delimiter, default space
-- @return n values
-- @usage first,next = splitv('jane:doe',':')
-- @see split
function utils.splitv (s,re)
return unpack(utils.split(s,re))
end
local lua52 = table.pack ~= nil
local lua51_load = load
if not lua52 then -- define Lua 5.2 style load()
function utils.load(str,src,mode,env)
local chunk,err
if type(str) == 'string' then
chunk,err = loadstring(str,src)
else
chunk,err = lua51_load(str,src)
end
if chunk and env then setfenv(chunk,env) end
return chunk,err
end
else
utils.load = load
-- setfenv/getfenv replacements for Lua 5.2
-- by Sergey Rozhenko
-- http://lua-users.org/lists/lua-l/2010-06/msg00313.html
-- Roberto Ierusalimschy notes that it is possible for getfenv to return nil
-- in the case of a function with no globals:
-- http://lua-users.org/lists/lua-l/2010-06/msg00315.html
function setfenv(f, t)
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
local name
local up = 0
repeat
up = up + 1
name = debug.getupvalue(f, up)
until name == '_ENV' or name == nil
if name then
debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue
debug.setupvalue(f, up, t)
end
end
function getfenv(f)
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
local name, val
local up = 0
repeat
up = up + 1
name, val = debug.getupvalue(f, up)
until name == '_ENV' or name == nil
return val
end
end
--- execute a shell command.
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
-- @param cmd a shell command
-- @return true if successful
-- @return actual return code
function utils.execute (cmd)
local res1,res2,res2 = os.execute(cmd)
if not lua52 then
return res1==0,res1
else
return res1,res2
end
end
if not lua52 then
function table.pack (...)
local n = select('#',...)
return {n=n; ...},n
end
local sep = package.config:sub(1,1)
function package.searchpath (mod,path)
mod = mod:gsub('%.',sep)
for m in path:gmatch('[^;]+') do
local nm = m:gsub('?',mod)
local f = io.open(nm,'r')
if f then f:close(); return nm end
end
end
end
if not table.pack then table.pack = _G.pack end
if not rawget(_G,"pack") then _G.pack = table.pack end
--- take an arbitrary set of arguments and make into a table.
-- This returns the table and the size; works fine for nil arguments
-- @param ... arguments
-- @return table
-- @return table size
-- @usage local t,n = utils.args(...)
--- 'memoize' a function (cache returned value for next call).
-- This is useful if you have a function which is relatively expensive,
-- but you don't know in advance what values will be required, so
-- building a table upfront is wasteful/impossible.
-- @param func a function of at least one argument
-- @return a function with at least one argument, which is used as the key.
function utils.memoize(func)
return setmetatable({}, {
__index = function(self, k, ...)
local v = func(k,...)
self[k] = v
return v
end,
__call = function(self, k) return self[k] end
})
end
--- is the object either a function or a callable object?.
-- @param obj Object to check.
function utils.is_callable (obj)
return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call
end
--- is the object of the specified type?.
-- If the type is a string, then use type, otherwise compare with metatable
-- @param obj An object to check
-- @param tp String of what type it should be
function utils.is_type (obj,tp)
if type(tp) == 'string' then return type(obj) == tp end
local mt = getmetatable(obj)
return tp == mt
end
local fileMT = getmetatable(io.stdout)
--- a string representation of a type.
-- For tables with metatables, we assume that the metatable has a `_name`
-- field. Knows about Lua file objects.
-- @param obj an object
-- @return a string like 'number', 'table' or 'List'
function utils.type (obj)
local t = type(obj)
if t == 'table' or t == 'userdata' then
local mt = getmetatable(obj)
if mt == fileMT then
return 'file'
else
return mt._name or "unknown "..t
end
else
return t
end
end
--- is this number an integer?
-- @param x a number
-- @raise error if x is not a number
function utils.is_integer (x)
return math.ceil(x)==x
end
utils.stdmt = {
List = {_name='List'}, Map = {_name='Map'},
Set = {_name='Set'}, MultiMap = {_name='MultiMap'}
}
local _function_factories = {}
--- associate a function factory with a type.
-- A function factory takes an object of the given type and
-- returns a function for evaluating it
-- @param mt metatable
-- @param fun a callable that returns a function
function utils.add_function_factory (mt,fun)
_function_factories[mt] = fun
end
local function _string_lambda(f)
local raise = utils.raise
if f:find '^|' or f:find '_' then
local args,body = f:match '|([^|]*)|(.+)'
if f:find '_' then
args = '_'
body = f
else
if not args then return raise 'bad string lambda' end
end
local fstr = 'return function('..args..') return '..body..' end'
local fn,err = loadstring(fstr)
if not fn then return raise(err) end
fn = fn()
return fn
else return raise 'not a string lambda'
end
end
--- an anonymous function as a string. This string is either of the form
-- '|args| expression' or is a function of one argument, '_'
-- @param lf function as a string
-- @return a function
-- @usage string_lambda '|x|x+1' (2) == 3
-- @usage string_lambda '_+1 (2) == 3
utils.string_lambda = utils.memoize(_string_lambda)
local ops
--- process a function argument.
-- This is used throughout Penlight and defines what is meant by a function:
-- Something that is callable, or an operator string as defined by <code>pl.operator</code>,
-- such as '>' or '#'. If a function factory has been registered for the type, it will
-- be called to get the function.
-- @param idx argument index
-- @param f a function, operator string, or callable object
-- @param msg optional error message
-- @return a callable
-- @raise if idx is not a number or if f is not callable
-- @see utils.is_callable
function utils.function_arg (idx,f,msg)
utils.assert_arg(1,idx,'number')
local tp = type(f)
if tp == 'function' then return f end -- no worries!
-- ok, a string can correspond to an operator (like '==')
if tp == 'string' then
if not ops then ops = require 'pl.operator'.optable end
local fn = ops[f]
if fn then return fn end
local fn, err = utils.string_lambda(f)
if not fn then error(err..': '..f) end
return fn
elseif tp == 'table' or tp == 'userdata' then
local mt = getmetatable(f)
if not mt then error('not a callable object',2) end
local ff = _function_factories[mt]
if not ff then
if not mt.__call then error('not a callable object',2) end
return f
else
return ff(f) -- we have a function factory for this type!
end
end
if not msg then msg = " must be callable" end
if idx > 0 then
error("argument "..idx..": "..msg,2)
else
error(msg,2)
end
end
--- bind the first argument of the function to a value.
-- @param fn a function of at least two values (may be an operator string)
-- @param p a value
-- @return a function such that f(x) is fn(p,x)
-- @raise same as @{function_arg}
-- @see pl.func.curry
function utils.bind1 (fn,p)
fn = utils.function_arg(1,fn)
return function(...) return fn(p,...) end
end
--- assert that the given argument is in fact of the correct type.
-- @param n argument index
-- @param val the value
-- @param tp the type
-- @param verify an optional verfication function
-- @param msg an optional custom message
-- @param lev optional stack position for trace, default 2
-- @raise if the argument n is not the correct type
-- @usage assert_arg(1,t,'table')
-- @usage assert_arg(n,val,'string',path.isdir,'not a directory')
function utils.assert_arg (n,val,tp,verify,msg,lev)
if type(val) ~= tp then
error(("argument %d expected a '%s', got a '%s'"):format(n,tp,type(val)),2)
end
if verify and not verify(val) then
error(("argument %d: '%s' %s"):format(n,val,msg),lev or 2)
end
end
--- assert the common case that the argument is a string.
-- @param n argument index
-- @param val a value that must be a string
-- @raise val must be a string
function utils.assert_string (n,val)
utils.assert_arg(n,val,'string',nil,nil,nil,3)
end
local err_mode = 'default'
--- control the error strategy used by Penlight.
-- Controls how <code>utils.raise</code> works; the default is for it
-- to return nil and the error string, but if the mode is 'error' then
-- it will throw an error. If mode is 'quit' it will immediately terminate
-- the program.
-- @param mode - either 'default', 'quit' or 'error'
-- @see utils.raise
function utils.on_error (mode)
err_mode = mode
end
--- used by Penlight functions to return errors. Its global behaviour is controlled
-- by <code>utils.on_error</code>
-- @param err the error string.
-- @see utils.on_error
function utils.raise (err)
if err_mode == 'default' then return nil,err
elseif err_mode == 'quit' then utils.quit(err)
else error(err,2)
end
end
raise = utils.raise
--- load a code string or bytecode chunk.
-- @param code Lua code as a string or bytecode
-- @param name for source errors
-- @param mode kind of chunk, 't' for text, 'b' for bytecode, 'bt' for all (default)
-- @param env the environment for the new chunk (default nil)
-- @return compiled chunk
-- @return error message (chunk is nil)
-- @function utils.load
--- Lua 5.2 Compatible Functions
-- @section lua52
--- pack an argument list into a table.
-- @param ... any arguments
-- @return a table with field n set to the length
-- @return the length
-- @function table.pack
------
-- return the full path where a Lua module name would be matched.
-- @param mod module name, possibly dotted
-- @param path a path in the same form as package.path or package.cpath
-- @see path.package_path
-- @function package.searchpath
return utils
@@ -0,0 +1,676 @@
--- XML LOM Utilities.
-- This implements some useful things on LOM documents, such as returned by lxp.lom.parse.
-- In particular, it can convert LOM back into XML text, with optional pretty-printing control.
-- It's based on stanza.lua from Prosody http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua)
--
-- Can be used as a lightweight one-stop-shop for simple XML processing; a simple XML parser is included
-- but the default is to use lxp.lom if it can be found.
-- <pre>
-- Prosody IM
-- Copyright (C) 2008-2010 Matthew Wild
-- Copyright (C) 2008-2010 Waqas Hussain
--
-- classic Lua XML parser by Roberto Ierusalimschy.
-- modified to output LOM format.
-- http://lua-users.org/wiki/LuaXml
-- </pre>
-- @module pl.xml
local t_insert = table.insert;
local t_concat = table.concat;
local t_remove = table.remove;
local s_format = string.format;
local s_match = string.match;
local tostring = tostring;
local setmetatable = setmetatable;
local getmetatable = getmetatable;
local pairs = pairs;
local ipairs = ipairs;
local type = type;
local next = next;
local print = print;
local unpack = unpack or table.unpack;
local s_gsub = string.gsub;
local s_char = string.char;
local s_find = string.find;
local os = os;
local pcall,require,io = pcall,require,io
local split = require 'pl.utils'.split
local _M = {}
local Doc = { __type = "doc" };
Doc.__index = Doc;
--- create a new document node.
-- @param tag the tag name
-- @param attr optional attributes (table of name-value pairs)
function _M.new(tag, attr)
local doc = { tag = tag, attr = attr or {}, last_add = {}};
return setmetatable(doc, Doc);
end
--- parse an XML document. By default, this uses lxp.lom.parse, but
-- falls back to basic_parse, or if use_basic is true
-- @param text_or_file file or string representation
-- @param is_file whether text_or_file is a file name or not
-- @param use_basic do a basic parse
-- @return a parsed LOM document with the document metatatables set
-- @return nil, error the error can either be a file error or a parse error
function _M.parse(text_or_file, is_file, use_basic)
local parser,status,lom
if use_basic then parser = _M.basic_parse
else
status,lom = pcall(require,'lxp.lom')
if not status then parser = _M.basic_parse else parser = lom.parse end
end
if is_file then
local f,err = io.open(text_or_file)
if not f then return nil,err end
text_or_file = f:read '*a'
f:close()
end
local doc,err = parser(text_or_file)
if not doc then return nil,err end
if lom then
_M.walk(doc,false,function(_,d)
setmetatable(d,Doc)
end)
end
return doc
end
---- convenient function to add a document node, This updates the last inserted position.
-- @param tag a tag name
-- @param attrs optional set of attributes (name-string pairs)
function Doc:addtag(tag, attrs)
local s = _M.new(tag, attrs);
(self.last_add[#self.last_add] or self):add_direct_child(s);
t_insert(self.last_add, s);
return self;
end
--- convenient function to add a text node. This updates the last inserted position.
-- @param text a string
function Doc:text(text)
(self.last_add[#self.last_add] or self):add_direct_child(text);
return self;
end
---- go up one level in a document
function Doc:up()
t_remove(self.last_add);
return self;
end
function Doc:reset()
local last_add = self.last_add;
for i = 1,#last_add do
last_add[i] = nil;
end
return self;
end
--- append a child to a document directly.
-- @param child a child node (either text or a document)
function Doc:add_direct_child(child)
t_insert(self, child);
end
--- append a child to a document at the last element added
-- @param child a child node (either text or a document)
function Doc:add_child(child)
(self.last_add[#self.last_add] or self):add_direct_child(child);
return self;
end
--accessing attributes: useful not to have to expose implementation (attr)
--but also can allow attr to be nil in any future optimizations
--- set attributes of a document node.
-- @param t a table containing attribute/value pairs
function Doc:set_attribs (t)
for k,v in pairs(t) do
self.attr[k] = v
end
end
--- set a single attribute of a document node.
-- @param a attribute
-- @param v its value
function Doc:set_attrib(a,v)
self.attr[a] = v
end
--- access the attributes of a document node.
function Doc:get_attribs()
return self.attr
end
--- function to create an element with a given tag name and a set of children.
-- @param tag a tag name
-- @param items either text or a table where the hash part is the attributes and the list part is the children.
function _M.elem(tag,items)
local s = _M.new(tag)
if type(items) == 'string' then items = {items} end
if _M.is_tag(items) then
t_insert(s,items)
elseif type(items) == 'table' then
for k,v in pairs(items) do
if type(k) == 'string' then
s.attr[k] = v
t_insert(s.attr,k)
else
s[k] = v
end
end
end
return s
end
--- given a list of names, return a number of element constructors.
-- @param list a list of names, or a comma-separated string.
-- @usage local parent,children = doc.tags 'parent,children' <br>
-- doc = parent {child 'one', child 'two'}
function _M.tags(list)
local ctors = {}
local elem = _M.elem
if type(list) == 'string' then list = split(list,'%s*,%s*') end
for _,tag in ipairs(list) do
local ctor = function(items) return _M.elem(tag,items) end
t_insert(ctors,ctor)
end
return unpack(ctors)
end
local templ_cache = {}
local function is_data(data)
return #data == 0 or type(data[1]) ~= 'table'
end
local function prepare_data(data)
-- a hack for ensuring that $1 maps to first element of data, etc.
-- Either this or could change the gsub call just below.
for i,v in ipairs(data) do
data[tostring(i)] = v
end
end
--- create a substituted copy of a document,
-- @param templ may be a document or a string representation which will be parsed and cached
-- @param data a table of name-value pairs or a list of such tables
-- @return an XML document
function Doc.subst(templ, data)
if type(data) ~= 'table' or not next(data) then return nil, "data must be a non-empty table" end
if is_data(data) then
prepare_data(data)
end
if type(templ) == 'string' then
if templ_cache[templ] then
templ = templ_cache[templ]
else
local str,err = templ
templ,err = _M.parse(str)
if not templ then return nil,err end
templ_cache[str] = templ
end
end
local function _subst(item)
return _M.clone(templ,function(s)
return s:gsub('%$(%w+)',item)
end)
end
if is_data(data) then return _subst(data) end
local list = {}
for _,item in ipairs(data) do
prepare_data(item)
t_insert(list,_subst(item))
end
if data.tag then
list = _M.elem(data.tag,list)
end
return list
end
--- get the first child with a given tag name.
-- @param tag the tag name
function Doc:child_with_name(tag)
for _, child in ipairs(self) do
if child.tag == tag then return child; end
end
end
local _children_with_name
function _children_with_name(self,tag,list,recurse)
for _, child in ipairs(self) do if type(child) == 'table' then
if child.tag == tag then t_insert(list,child) end
if recurse then _children_with_name(child,tag,list,recurse) end
end end
end
--- get all elements in a document that have a given tag.
-- @param tag a tag name
-- @param dont_recurse optionally only return the immediate children with this tag name
-- @return a list of elements
function Doc:get_elements_with_name(tag,dont_recurse)
local res = {}
_children_with_name(self,tag,res,not dont_recurse)
return res
end
-- iterate over all children of a document node, including text nodes.
function Doc:children()
local i = 0;
return function (a)
i = i + 1
return a[i];
end, self, i;
end
-- return the first child element of a node, if it exists.
function Doc:first_childtag()
if #self == 0 then return end
for _,t in ipairs(self) do
if type(t) == 'table' then return t end
end
end
function Doc:matching_tags(tag, xmlns)
xmlns = xmlns or self.attr.xmlns;
local tags = self;
local start_i, max_i = 1, #tags;
return function ()
for i=start_i,max_i do
v = tags[i];
if (not tag or v.tag == tag)
and (not xmlns or xmlns == v.attr.xmlns) then
start_i = i+1;
return v;
end
end
end, tags, i;
end
--- iterate over all child elements of a document node.
function Doc:childtags()
local i = 0;
return function (a)
local v
repeat
i = i + 1
v = self[i]
if v and type(v) == 'table' then return v; end
until not v
end, self[1], i;
end
--- visit child element of a node and call a function, possibility modifying the document.
-- @param callback a function passed the node (text or element). If it returns nil, that node will be removed.
-- If it returns a value, that will replace the current node.
function Doc:maptags(callback)
local is_tag = _M.is_tag
local i = 1;
while i <= #self do
if is_tag(self[i]) then
local ret = callback(self[i]);
if ret == nil then
t_remove(self, i);
else
self[i] = ret;
i = i + 1;
end
end
end
return self;
end
local xml_escape
do
local escape_table = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
_M.xml_escape = xml_escape;
end
-- pretty printing
-- if indent, then put each new tag on its own line
-- if attr_indent, put each new attribute on its own line
local function _dostring(t, buf, self, xml_escape, parentns, idn, indent, attr_indent)
local nsid = 0;
local tag = t.tag
local lf,alf = ""," "
if indent then lf = '\n'..idn end
if attr_indent then alf = '\n'..idn..attr_indent end
t_insert(buf, lf.."<"..tag);
for k, v in pairs(t.attr) do
if type(k) ~= 'number' then -- LOM attr table has list-like part
if s_find(k, "\1", 1, true) then
local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
nsid = nsid + 1;
t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'");
elseif not(k == "xmlns" and v == parentns) then
t_insert(buf, alf..k.."='"..xml_escape(v).."'");
end
end
end
local len,has_children = #t;
if len == 0 then
local out = "/>"
if attr_indent then out = '\n'..idn..out end
t_insert(buf, out);
else
t_insert(buf, ">");
for n=1,len do
local child = t[n];
if child.tag then
self(child, buf, self, xml_escape, t.attr.xmlns,idn and idn..indent, indent, attr_indent );
has_children = true
else -- text element
t_insert(buf, xml_escape(child));
end
end
t_insert(buf, (has_children and lf or '').."</"..tag..">");
end
end
---- pretty-print an XML document
--- @param t an XML document
--- @param idn an initial indent (indents are all strings)
--- @param indent an indent for each level
--- @param attr_indent if given, indent each attribute pair and put on a separate line
--- @return a string representation
function _M.tostring(t,idn,indent, attr_indent)
local buf = {};
_dostring(t, buf, _dostring, xml_escape, nil,idn,indent, attr_indent);
return t_concat(buf);
end
Doc.__tostring = _M.tostring
--- get the full text value of an element
function Doc:get_text()
local res = {}
for i,el in ipairs(self) do
if type(el) == 'string' then t_insert(res,el) end
end
return t_concat(res);
end
--- make a copy of a document
-- @param doc the original document
-- @param strsubst an optional function for handling string copying which could do substitution, etc.
function _M.clone(doc, strsubst)
local lookup_table = {};
local function _copy(object)
if type(object) ~= "table" then
if strsubst and type(object) == 'string' then return strsubst(object)
else return object;
end
elseif lookup_table[object] then
return lookup_table[object];
end
local new_table = {};
lookup_table[object] = new_table;
for index, value in pairs(object) do
new_table[_copy(index)] = _copy(value); -- is cloning keys much use, hm?
end
return setmetatable(new_table, getmetatable(object));
end
return _copy(doc)
end
--- compare two documents.
-- @param t1 any value
-- @param t2 any value
function _M.compare(t1,t2)
local ty1 = type(t1)
local ty2 = type(t2)
if ty1 ~= ty2 then return false, 'type mismatch' end
if ty1 == 'string' then
return t1 == t2 and true or 'text '..t1..' ~= text '..t2
end
if ty1 ~= 'table' or ty2 ~= 'table' then return false, 'not a document' end
if t1.tag ~= t2.tag then return false, 'tag '..t1.tag..' ~= tag '..t2.tag end
if #t1 ~= #t2 then return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag end
-- compare attributes
for k,v in pairs(t1.attr) do
if t2.attr[k] ~= v then return false, 'mismatch attrib' end
end
for k,v in pairs(t2.attr) do
if t1.attr[k] ~= v then return false, 'mismatch attrib' end
end
-- compare children
for i = 1,#t1 do
local yes,err = _M.compare(t1[i],t2[i])
if not yes then return err end
end
return true
end
--- is this value a document element?
-- @param d any value
function _M.is_tag(d)
return type(d) == 'table' and type(d.tag) == 'string'
end
--- call the desired function recursively over the document.
-- @param doc the document
-- @param depth_first visit child notes first, then the current node
-- @param operation a function which will receive the current tag name and current node.
function _M.walk (doc, depth_first, operation)
if not depth_first then operation(doc.tag,doc) end
for _,d in ipairs(doc) do
if _M.is_tag(d) then
_M.walk(d,depth_first,operation)
end
end
if depth_first then operation(doc.tag,doc) end
end
local escapes = { quot = "\"", apos = "'", lt = "<", gt = ">", amp = "&" }
local function unescape(str) return (str:gsub( "&(%a+);", escapes)); end
local function parseargs(s)
local arg = {}
s:gsub("([%w:]+)%s*=%s*([\"'])(.-)%2", function (w, _, a)
arg[w] = unescape(a)
end)
return arg
end
--- Parse a simple XML document using a pure Lua parser based on Robero Ierusalimschy's original version.
-- @param s the XML document to be parsed.
-- @param all_text if true, preserves all whitespace. Otherwise only text containing non-whitespace is included.
function _M.basic_parse(s,all_text)
local t_insert,t_remove = table.insert,table.remove
local s_find,s_sub = string.find,string.sub
local stack = {}
local top = {}
t_insert(stack, top)
local ni,c,label,xarg, empty
local i, j = 1, 1
-- we're not interested in <?xml version="1.0"?>
local _,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*')
if istart then i = istart+1 end
while true do
ni,j,c,label,xarg, empty = s_find(s, "<(%/?)([%w:%-_]+)(.-)(%/?)>", i)
if not ni then break end
local text = s_sub(s, i, ni-1)
if all_text or not s_find(text, "^%s*$") then
t_insert(top, unescape(text))
end
if empty == "/" then -- empty element tag
t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc))
elseif c == "" then -- start tag
top = setmetatable({tag=label, attr=parseargs(xarg)},Doc)
t_insert(stack, top) -- new level
else -- end tag
local toclose = t_remove(stack) -- remove top
top = stack[#stack]
if #stack < 1 then
error("nothing to close with "..label)
end
if toclose.tag ~= label then
error("trying to close "..toclose.tag.." with "..label)
end
t_insert(top, toclose)
end
i = j+1
end
local text = s_sub(s, i)
if all_text or not s_find(text, "^%s*$") then
t_insert(stack[#stack], unescape(text))
end
if #stack > 1 then
error("unclosed "..stack[#stack].tag)
end
local res = stack[1]
return type(res[1])=='string' and res[2] or res[1]
end
local function empty(attr) return not attr or not next(attr) end
local function is_text(s) return type(s) == 'string' end
local function is_element(d) return type(d) == 'table' and d.tag ~= nil end
-- returns the key,value pair from a table if it has exactly one entry
local function has_one_element(t)
local key,value = next(t)
if next(t,key) ~= nil then return false end
return key,value
end
local function append_capture(res,tbl)
if not empty(tbl) then -- no point in capturing empty tables...
local key
if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table
key = tbl._
tbl._ = nil
if empty(tbl) then return end
end
-- a table with only one pair {[0]=value} shall be reduced to that value
local numkey,val = has_one_element(tbl)
if numkey == 0 then tbl = val end
if key then
res[key] = tbl
else -- otherwise, we append the captured table
t_insert(res,tbl)
end
end
end
local function make_number(pat)
if pat:find '^%d+$' then -- $1 etc means use this as an array location
pat = tonumber(pat)
end
return pat
end
local function capture_attrib(res,pat,value)
pat = make_number(pat:sub(2))
res[pat] = value
return true
end
local match
function match(d,pat,res,keep_going)
local ret = true
if d == nil then return false end
-- attribute string matching is straight equality, except if the pattern is a $ capture,
-- which always succeeds.
if type(d) == 'string' then
if type(pat) ~= 'string' then return false end
if _M.debug then print(d,pat) end
if pat:find '^%$' then
return capture_attrib(res,pat,d)
else
return d == pat
end
else
if _M.debug then print(d.tag,pat.tag) end
-- this is an element node. For a match to succeed, the attributes must
-- match as well.
-- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute
local tagpat = pat.tag:match '^(.-)%-$'
if tagpat then
tagpat = make_number(tagpat)
res[tagpat] = d.tag
end
if d.tag == pat.tag or tagpat then
if not empty(pat.attr) then
if empty(d.attr) then ret = false
else
for prop,pval in pairs(pat.attr) do
local dval = d.attr[prop]
if not match(dval,pval,res) then ret = false; break end
end
end
end
-- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..}
if ret and #pat > 0 then
local i,j = 1,1
local function next_elem()
j = j + 1 -- next child element of data
if is_text(d[j]) then j = j + 1 end
return j <= #d
end
repeat
local p = pat[i]
-- repeated {{<...>}} patterns shall match one or more elements
-- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X}
if is_element(p) and p.repeated then
local found
repeat
local tbl = {}
ret = match(d[j],p,tbl,false)
if ret then
found = false --true
append_capture(res,tbl)
end
until not next_elem() or (found and not ret)
i = i + 1
else
ret = match(d[j],p,res,false)
if ret then i = i + 1 end
end
until not next_elem() or i > #pat -- run out of elements or patterns to match
-- if every element in our pattern matched ok, then it's been a successful match
if i > #pat then return true end
end
if ret then return true end
else
ret = false
end
-- keep going anyway - look at the children!
if keep_going then
for child in d:childtags() do
ret = match(child,pat,res,keep_going)
if ret then break end
end
end
end
return ret
end
function Doc:match(pat)
if is_text(pat) then
pat = _M.parse(pat,false,true)
end
_M.walk(pat,false,function(_,d)
if is_text(d[1]) and is_element(d[2]) and is_text(d[3]) and
d[1]:find '%s*{{' and d[3]:find '}}%s*' then
t_remove(d,1)
t_remove(d,2)
d[1].repeated = true
end
end)
local res = {}
local ret = match(self,pat,res,true)
return res,ret
end
return _M