Documentation for this module may be created at Module:TableTools/doc

  1. --[[
  2. ------------------------------------------------------------------------------------
  3. -- TableTools --
  4. -- --
  5. -- This module includes a number of functions for dealing with Lua tables. --
  6. -- It is a meta-module, meant to be called from other Lua modules, and should --
  7. -- not be called directly from #invoke. --
  8. ------------------------------------------------------------------------------------
  9. --]]
  10.  
  11. local libraryUtil = require('libraryUtil')
  12.  
  13. local p = {}
  14.  
  15. -- Define often-used variables and functions.
  16. local floor = math.floor
  17. local infinity = math.huge
  18. local checkType = libraryUtil.checkType
  19.  
  20. --[[
  21. ------------------------------------------------------------------------------------
  22. -- isPositiveInteger
  23. --
  24. -- This function returns true if the given value is a positive integer, and false
  25. -- if not. Although it doesn't operate on tables, it is included here as it is
  26. -- useful for determining whether a given table key is in the array part or the
  27. -- hash part of a table.
  28. ------------------------------------------------------------------------------------
  29. --]]
  30. function p.isPositiveInteger(v)
  31. if type(v) == 'number' and v >= 1 and floor(v) == v and v < infinity then
  32. return true
  33. else
  34. return false
  35. end
  36. end
  37.  
  38. --[[
  39. ------------------------------------------------------------------------------------
  40. -- isNan
  41. --
  42. -- This function returns true if the given number is a NaN value, and false
  43. -- if not. Although it doesn't operate on tables, it is included here as it is
  44. -- useful for determining whether a value can be a valid table key. Lua will
  45. -- generate an error if a NaN is used as a table key.
  46. ------------------------------------------------------------------------------------
  47. --]]
  48. function p.isNan(v)
  49. if type(v) == 'number' and tostring(v) == '-nan' then
  50. return true
  51. else
  52. return false
  53. end
  54. end
  55.  
  56. --[[
  57. ------------------------------------------------------------------------------------
  58. -- shallowClone
  59. --
  60. -- This returns a clone of a table. The value returned is a new table, but all
  61. -- subtables and functions are shared. Metamethods are respected, but the returned
  62. -- table will have no metatable of its own.
  63. ------------------------------------------------------------------------------------
  64. --]]
  65. function p.shallowClone(t)
  66. local ret = {}
  67. for k, v in pairs(t) do
  68. ret[k] = v
  69. end
  70. return ret
  71. end
  72.  
  73. --[[
  74. ------------------------------------------------------------------------------------
  75. -- removeDuplicates
  76. --
  77. -- This removes duplicate values from an array. Non-positive-integer keys are
  78. -- ignored. The earliest value is kept, and all subsequent duplicate values are
  79. -- removed, but otherwise the array order is unchanged.
  80. ------------------------------------------------------------------------------------
  81. --]]
  82. function p.removeDuplicates(t)
  83. checkType('removeDuplicates', 1, t, 'table')
  84. local isNan = p.isNan
  85. local ret, exists = {}, {}
  86. for i, v in ipairs(t) do
  87. if isNan(v) then
  88. -- NaNs can't be table keys, and they are also unique, so we don't need to check existence.
  89. ret[#ret + 1] = v
  90. else
  91. if not exists[v] then
  92. ret[#ret + 1] = v
  93. exists[v] = true
  94. end
  95. end
  96. end
  97. return ret
  98. end
  99.  
  100. --[[
  101. ------------------------------------------------------------------------------------
  102. -- numKeys
  103. --
  104. -- This takes a table and returns an array containing the numbers of any numerical
  105. -- keys that have non-nil values, sorted in numerical order.
  106. ------------------------------------------------------------------------------------
  107. --]]
  108. function p.numKeys(t)
  109. checkType('numKeys', 1, t, 'table')
  110. local isPositiveInteger = p.isPositiveInteger
  111. local nums = {}
  112. for k, v in pairs(t) do
  113. if isPositiveInteger(k) then
  114. nums[#nums + 1] = k
  115. end
  116. end
  117. table.sort(nums)
  118. return nums
  119. end
  120.  
  121. --[[
  122. ------------------------------------------------------------------------------------
  123. -- affixNums
  124. --
  125. -- This takes a table and returns an array containing the numbers of keys with the
  126. -- specified prefix and suffix. For example, for the table
  127. -- {a1 = 'foo', a3 = 'bar', a6 = 'baz'} and the prefix "a", affixNums will
  128. -- return {1, 3, 6}.
  129. ------------------------------------------------------------------------------------
  130. --]]
  131. function p.affixNums(t, prefix, suffix)
  132. checkType('affixNums', 1, t, 'table')
  133. checkType('affixNums', 2, prefix, 'string', true)
  134. checkType('affixNums', 3, suffix, 'string', true)
  135.  
  136. local function cleanPattern(s)
  137. -- Cleans a pattern so that the magic characters ()%.[]*+-?^$ are interpreted literally.
  138. s = s:gsub('([%(%)%%%.%[%]%*%+%-%?%^%$])', '%%%1')
  139. return s
  140. end
  141.  
  142. prefix = prefix or ''
  143. suffix = suffix or ''
  144. prefix = cleanPattern(prefix)
  145. suffix = cleanPattern(suffix)
  146. local pattern = '^' .. prefix .. '([1-9]%d*)' .. suffix .. '$'
  147.  
  148. local nums = {}
  149. for k, v in pairs(t) do
  150. if type(k) == 'string' then
  151. local num = mw.ustring.match(k, pattern)
  152. if num then
  153. nums[#nums + 1] = tonumber(num)
  154. end
  155. end
  156. end
  157. table.sort(nums)
  158. return nums
  159. end
  160.  
  161. --[[
  162. ------------------------------------------------------------------------------------
  163. -- numData
  164. --
  165. -- Given a table with keys like ("foo1", "bar1", "foo2", "baz2"), returns a table
  166. -- of subtables in the format
  167. -- { [1] = {foo = 'text', bar = 'text'}, [2] = {foo = 'text', baz = 'text'} }
  168. -- Keys that don't end with an integer are stored in a subtable named "other".
  169. -- The compress option compresses the table so that it can be iterated over with
  170. -- ipairs.
  171. ------------------------------------------------------------------------------------
  172. --]]
  173. function p.numData(t, compress)
  174. checkType('numData', 1, t, 'table')
  175. checkType('numData', 2, compress, 'boolean', true)
  176. local ret = {}
  177. for k, v in pairs(t) do
  178. local prefix, num = mw.ustring.match(tostring(k), '^([^0-9]*)([1-9][0-9]*)$')
  179. if num then
  180. num = tonumber(num)
  181. local subtable = ret[num] or {}
  182. if prefix == '' then
  183. -- Positional parameters match the blank string; put them at the start of the subtable instead.
  184. prefix = 1
  185. end
  186. subtable[prefix] = v
  187. ret[num] = subtable
  188. else
  189. local subtable = ret.other or {}
  190. subtable[k] = v
  191. ret.other = subtable
  192. end
  193. end
  194. if compress then
  195. local other = ret.other
  196. ret = p.compressSparseArray(ret)
  197. ret.other = other
  198. end
  199. return ret
  200. end
  201.  
  202. --[[
  203. ------------------------------------------------------------------------------------
  204. -- compressSparseArray
  205. --
  206. -- This takes an array with one or more nil values, and removes the nil values
  207. -- while preserving the order, so that the array can be safely traversed with
  208. -- ipairs.
  209. ------------------------------------------------------------------------------------
  210. --]]
  211. function p.compressSparseArray(t)
  212. checkType('compressSparseArray', 1, t, 'table')
  213. local ret = {}
  214. local nums = p.numKeys(t)
  215. for _, num in ipairs(nums) do
  216. ret[#ret + 1] = t[num]
  217. end
  218. return ret
  219. end
  220.  
  221. --[[
  222. ------------------------------------------------------------------------------------
  223. -- sparseIpairs
  224. --
  225. -- This is an iterator for sparse arrays. It can be used like ipairs, but can
  226. -- handle nil values.
  227. ------------------------------------------------------------------------------------
  228. --]]
  229. function p.sparseIpairs(t)
  230. checkType('sparseIpairs', 1, t, 'table')
  231. local nums = p.numKeys(t)
  232. local i = 0
  233. local lim = #nums
  234. return function ()
  235. i = i + 1
  236. if i <= lim then
  237. local key = nums[i]
  238. return key, t[key]
  239. else
  240. return nil, nil
  241. end
  242. end
  243. end
  244.  
  245. --[[
  246. ------------------------------------------------------------------------------------
  247. -- size
  248. --
  249. -- This returns the size of a key/value pair table. It will also work on arrays,
  250. -- but for arrays it is more efficient to use the # operator.
  251. ------------------------------------------------------------------------------------
  252. --]]
  253. function p.size(t)
  254. checkType('size', 1, t, 'table')
  255. local i = 0
  256. for k in pairs(t) do
  257. i = i + 1
  258. end
  259. return i
  260. end
  261.  
  262. return p

--[[


-- TableTools -- -- -- -- This module includes a number of functions for dealing with Lua tables. -- -- It is a meta-module, meant to be called from other Lua modules, and should -- -- not be called directly from #invoke. --


--]]

local libraryUtil = require('libraryUtil')

local p = {}

-- Define often-used variables and functions. local floor = math.floor local infinity = math.huge local checkType = libraryUtil.checkType

--[[


-- isPositiveInteger -- -- This function returns true if the given value is a positive integer, and false -- if not. Although it doesn't operate on tables, it is included here as it is -- useful for determining whether a given table key is in the array part or the -- hash part of a table.


--]] function p.isPositiveInteger(v) if type(v) == 'number' and v >= 1 and floor(v) == v and v < infinity then return true else return false end end

--[[


-- isNan -- -- This function returns true if the given number is a NaN value, and false -- if not. Although it doesn't operate on tables, it is included here as it is -- useful for determining whether a value can be a valid table key. Lua will -- generate an error if a NaN is used as a table key.


--]] function p.isNan(v) if type(v) == 'number' and tostring(v) == '-nan' then return true else return false end end

--[[


-- shallowClone -- -- This returns a clone of a table. The value returned is a new table, but all -- subtables and functions are shared. Metamethods are respected, but the returned -- table will have no metatable of its own.


--]] function p.shallowClone(t) local ret = {} for k, v in pairs(t) do ret[k] = v end return ret end

--[[


-- removeDuplicates -- -- This removes duplicate values from an array. Non-positive-integer keys are -- ignored. The earliest value is kept, and all subsequent duplicate values are -- removed, but otherwise the array order is unchanged.


--]] function p.removeDuplicates(t) checkType('removeDuplicates', 1, t, 'table') local isNan = p.isNan local ret, exists = {}, {} for i, v in ipairs(t) do if isNan(v) then -- NaNs can't be table keys, and they are also unique, so we don't need to check existence. ret[#ret + 1] = v else if not exists[v] then ret[#ret + 1] = v exists[v] = true end end end return ret end

--[[


-- numKeys -- -- This takes a table and returns an array containing the numbers of any numerical -- keys that have non-nil values, sorted in numerical order.


--]] function p.numKeys(t) checkType('numKeys', 1, t, 'table') local isPositiveInteger = p.isPositiveInteger local nums = {} for k, v in pairs(t) do if isPositiveInteger(k) then nums[#nums + 1] = k end end table.sort(nums) return nums end

--[[


-- affixNums -- -- This takes a table and returns an array containing the numbers of keys with the -- specified prefix and suffix. For example, for the table -- {a1 = 'foo', a3 = 'bar', a6 = 'baz'} and the prefix "a", affixNums will -- return {1, 3, 6}.


--]] function p.affixNums(t, prefix, suffix) checkType('affixNums', 1, t, 'table') checkType('affixNums', 2, prefix, 'string', true) checkType('affixNums', 3, suffix, 'string', true)

local function cleanPattern(s) -- Cleans a pattern so that the magic characters ()%.[]*+-?^$ are interpreted literally. s = s:gsub('([%(%)%%%.%[%]%*%+%-%?%^%$])', '%%%1') return s end

prefix = prefix or suffix = suffix or prefix = cleanPattern(prefix) suffix = cleanPattern(suffix) local pattern = '^' .. prefix .. '([1-9]%d*)' .. suffix .. '$'

local nums = {} for k, v in pairs(t) do if type(k) == 'string' then local num = mw.ustring.match(k, pattern) if num then nums[#nums + 1] = tonumber(num) end end end table.sort(nums) return nums end

--[[


-- numData -- -- Given a table with keys like ("foo1", "bar1", "foo2", "baz2"), returns a table -- of subtables in the format -- { [1] = {foo = 'text', bar = 'text'}, [2] = {foo = 'text', baz = 'text'} } -- Keys that don't end with an integer are stored in a subtable named "other". -- The compress option compresses the table so that it can be iterated over with -- ipairs.


--]] function p.numData(t, compress) checkType('numData', 1, t, 'table') checkType('numData', 2, compress, 'boolean', true) local ret = {} for k, v in pairs(t) do local prefix, num = mw.ustring.match(tostring(k), '^([^0-9]*)([1-9][0-9]*)$') if num then num = tonumber(num) local subtable = ret[num] or {} if prefix == then -- Positional parameters match the blank string; put them at the start of the subtable instead. prefix = 1 end subtable[prefix] = v ret[num] = subtable else local subtable = ret.other or {} subtable[k] = v ret.other = subtable end end if compress then local other = ret.other ret = p.compressSparseArray(ret) ret.other = other end return ret end

--[[


-- compressSparseArray -- -- This takes an array with one or more nil values, and removes the nil values -- while preserving the order, so that the array can be safely traversed with -- ipairs.


--]] function p.compressSparseArray(t) checkType('compressSparseArray', 1, t, 'table') local ret = {} local nums = p.numKeys(t) for _, num in ipairs(nums) do ret[#ret + 1] = t[num] end return ret end

--[[


-- sparseIpairs -- -- This is an iterator for sparse arrays. It can be used like ipairs, but can -- handle nil values.


--]] function p.sparseIpairs(t) checkType('sparseIpairs', 1, t, 'table') local nums = p.numKeys(t) local i = 0 local lim = #nums return function () i = i + 1 if i <= lim then local key = nums[i] return key, t[key] else return nil, nil end end end

--[[


-- size -- -- This returns the size of a key/value pair table. It will also work on arrays, -- but for arrays it is more efficient to use the # operator.


--]] function p.size(t) checkType('size', 1, t, 'table') local i = 0 for k in pairs(t) do i = i + 1 end return i end

return p