Module:TableTools: Difference between revisions

From NvWiki
Jump to navigation Jump to search
Undid revision 1317119166 by 70.62.87.34 (talk) nothing to do with improving this module
m 1 revision imported: Import modules used with Template:Infobox software
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
{{Permanently protected}}
------------------------------------------------------------------------------------
{{oldtfdfull|date= 2018 May 13 |result=Merge |disc=Module:Table}}
--                                  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.                                              --
------------------------------------------------------------------------------------


== removeDuplicate does not remove duplicate NaN ==
local libraryUtil = require('libraryUtil')
<syntaxhighlight lang="lua">
 
function p.removeDuplicates(t)
local p = {}
checkType('removeDuplicates', 1, t, 'table')
 
-- Define often-used variables and functions.
local floor = math.floor
local infinity = math.huge
local checkType = libraryUtil.checkType
local checkTypeMulti = libraryUtil.checkTypeMulti
 
------------------------------------------------------------------------------------
-- 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)
return type(v) == 'number' and v >= 1 and floor(v) == v and v < infinity
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)
return type(v) == 'number' and v ~= v
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)
checkType('shallowClone', 1, t, 'table')
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(arr)
checkType('removeDuplicates', 1, arr, 'table')
local isNan = p.isNan
local isNan = p.isNan
local ret, exists = {}, {}
local ret, exists = {}, {}
for i, v in ipairs(t) do
for _, v in ipairs(arr) do
if isNan(v) then
if isNan(v) then
-- NaNs can't be table keys, and they are also unique, so we don't need to check existence.
-- NaNs can't be table keys, and they are also unique, so we don't need to check existence.
ret[#ret + 1] = v
ret[#ret + 1] = v
else
elseif not exists[v] then
if not exists[v] then
ret[#ret + 1] = v
ret[#ret + 1] = v
exists[v] = true
exists[v] = true
end
end
end
end
end
return ret
return ret
end
end
</syntaxhighlight>
 
This should be:
------------------------------------------------------------------------------------
<syntaxhighlight lang="lua">
-- numKeys
function p.removeDuplicates(t, uniqueNan)
--
checkType('removeDuplicates', 1, t, 'table')
-- This takes a table and returns an array containing the numbers of any numerical
local ret, isNan, hasNan, exists = {}, p.isNan, false, {}
-- keys that have non-nil values, sorted in numerical order.
for _, v in ipairs(t) do
------------------------------------------------------------------------------------
-- NaNs can't be table keys in exists[], and they are also equal to each other in Lua.
function p.numKeys(t)
if isNan(v) then
checkType('numKeys', 1, t, 'table')
-- But we may want only one Nan in ret[], and there may be multiple Nan's in t[].
local isPositiveInteger = p.isPositiveInteger
if uniqueNan == nil or uniqueNan == false or uniqueNan == true and not hasNan then
local nums = {}
hasNan = true
for k in pairs(t) do
ret[#ret + 1] = v
if isPositiveInteger(k) then
end
nums[#nums + 1] = k
else
end
if not exists[v] then
exists[v] = true
ret[#ret + 1] = v
end
end
end
end
return ret
table.sort(nums)
return nums
end
end
</syntaxhighlight>
-- [[User:Verdy p|verdy_p]] ([[User talk:Verdy p|talk]]) 07:50, 2 February 2014 (UTC)


:<span class="template-ping">@[[:User:Verdy p|Verdy p]]:</span> This was by design, as comparing two NaNs always results in <code>false</code>. My reasoning was that since two NaNs can never be equal to each other - even if they were made by the exact same calculation - then they shouldn't be treated as duplicates by the algorithm. Although if there's some sort of precedent for doing things a different way, please let me know. I'm fairly new to the world of NaNs, after all. — '''''[[User:Mr. Stradivarius|<span style="color: #194D00; font-family: Palatino, Times, serif">Mr. Stradivarius</span>]]''''' <sup>[[User talk:Mr. Stradivarius|♪ talk ♪]]</sup> 08:01, 2 February 2014 (UTC)
------------------------------------------------------------------------------------
:: That's the Lua interpretation anyway. Even if it has a single Nan value (no distinction between signaling and non-signaling ones, or Nan's carrying an integer type, like in IEEE binary 32-bit float and 64-bit double formats, neither does Java...), there are some apps that depend on using Nan as a distinctive key equal to itself, but still different from nil.
-- affixNums
:: The other kind of usage of Nan is "value not set, ignore it": when computing averages for example, Nan must not be summed and not counted, so all Nan's should be removed from the table. For this case May be there should be an option to either preserve all Nan's, or nuke them all from the result: the kill option would be tested in the if-branch of your first version, and a second alternate option tested after it would be to make Nan's unique in the result.... The first case being quite common for statistics when it means "unset", while nil means something else (such as compute this value before determinig if it's a Nan, nil bring used also for weak references that can be retreived from another slow data store, and the table storing nil being a fast cache of that slow data store) [[User:Verdy p|verdy_p]] ([[User talk:Verdy p|talk]]) 08:29, 2 February 2014 (UTC)
--
:::I had a quick look at the functions, but cannot work out the intended usage—that usage would probably determine what should happen with NaNs. The docs for <code>removeDuplicates</code> says that keys that are not positive integers are ignored—but what it means is that such keys are ''removed''. On that principle, I think it would be better to default to removing NaNs. I cannot imagine a usage example where I would want to call this function and have a NaN in the result—what would I do with it? If it were desirable to test for the presence of NaN members, why not return an extra value that is true if one or more NaNs are encountered and omitted? How would it help to ever have both <code>0/0</code> and <code>-0/0</code> (or multiple instances of <code>0/0</code>) in the result?
-- This takes a table and returns an array containing the numbers of keys with the
:::Regarding verdy_p's code: I don't think it is a good idea to deviate from Lua's idioms, and if there were a <code>uniqueNan</code> parameter, it should not be tested for explicit "false" and "true" values. The function regards "hello" as neither false nor true and while that is a very defensible position, it's not how Lua code is supposed to work. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 03:26, 3 February 2014 (UTC)
-- specified prefix and suffix. For example, for the table
:: It takes three values (it is a "tri-state boolean"):
-- {a1 = 'foo', a3 = 'bar', a6 = 'baz'} and the prefix "a", affixNums will return
::* nil the default is your existing code that keeps them all,
-- {1, 3, 6}.
::* true keeps a single Nan,
------------------------------------------------------------------------------------
::* false does not even keep this one
function p.affixNums(t, prefix, suffix)
::* any other (unsupported) value in other types is considered like nil.
checkType('affixNums', 1, t, 'table')
:: There's no such "Lua idiom" (about NaN) except that the default value is nil (already a common Lua practive), and otherwise the parameter is used as a boolean flag whose value is meant by its explicit name (and is a general good practive for naming boolean flags)... Tri-state boooleans are very common objects in many situations where we need a supplementary "may be / unknown / don't know / other / not specified / not available / missing data / to be determined later / uninitialized / refuse to say / no opinion / fuzzy" value. This fuzzy concept is also carried by Nan (within the field of numbers instead of booleans) so it is really appropriate here !
checkType('affixNums', 2, prefix, 'string', true)
:: There's also no such ideom of "NaN" in Mediawiki parameters. Given that Lua already states that multiple NaN values compare effectively as "equal", it makes no sense to keep these duplicates which occur occasionally as result of computed numeric expressions (numbers in Lua never distinguish any NaN, neither does MediaWiki). You "idiom" is that used in Java or C++, which is NOT directly usd in MadiaWiki or Lua (hidden in the underlying internal representation). Keeping these duplicates should complicates situation where we expect "keys" in Lua tables to be unique. Not doing that means that we'll not properly detect overrides, and it will be impossible to map any action of value to NaN, meaning that we'll always get unspecified results that we can never catch in MediaWiki templates (this means undetected errors and unspecified behavior, wrong results in all cases). Lua treats all NaN as unique values which compare equal between each other, but different from all other non-NaN values (including "nil"/unmapped). Note that "nil" in (which matches unspecified MediaWiki values) is NOT "NaN" and is also NOT an "empty string" value (which is only one of the possible default values for unspecified but cannot match any "NaN"). "NaN" in Mediawiki is a string, and does not match the Lua NaN numeric value.
checkType('affixNums', 3, suffix, 'string', true)
:: It's only when we start speaking about fuzzy logic with measured accuracy, that we introduce many other values by representing fuzzy booleans by a real number between 0.0 and 1.0, but then even this accuracy may be sometimes not measurable and will also needs an additional Nan value, so fuzzy booleans have infinite number of values between 0.0 and 1.0 plus Nan; (0.0 representing absolutely false, 0.5 representing false or true equally, and 1.0 representing absolutely true, the rule being that if you sum the response rates, you'll get always 1.0). [User:Verdy p|verdy_p]] ([[User talk:Verdy p|talk]])
:: In summary the statement "-- NaNs can't be table keys, and they are also unique, so we don't need to check existence." is compeltely false: not checking this existence means that the line "ret[#ret + 1] = v" will be executed multiple times (each time "v" is NaN) so "ret[]" will contain multiple "NaN" values... And this is not expected.
:: Note you may want to have "uniqueNan" set to true by default (the code above keeps the existing default behavior when the optional "uniqueNan" parameter is not specified, i.e. it keeps these duplicates).
:: If you drop that optional parameter, by forcing the function to behave like if it was always true, all that is needed is to change the line "if uniqueNan == nil or uniqueNan == false or uniqueNan == true and not hasNan then" to "if not hasNan then"... I.e. you just need to check and set "hasNan = true" before adding to "ret[]". [[User:Verdy p|verdy_p]] ([[User talk:Verdy p|talk]]) 20:40, 5 August 2018 (UTC)
::: I don't know what the "correct" behavior for removeDuplicates should be when passed a NaN, either keeping them all or keeping only one. I do know that the following statements are incorrect or irrelevant:
:::* {{tq|there are some apps that depend on using Nan as a distinctive key equal to itself, but still different from nil}} – Whether such apps exist or not, in Lua neither NaN nor nil can be a ''key'' in a table. Only a value.
:::* {{tq|The other kind of usage of Nan is "value not set, ignore it": when computing averages for example [...] while nil means something else}} – That seems a rather idiosyncratic use, and not relevant to a generic "removeDuplicates" method. Particularly since for calculating averages you wouldn't want to remove duplicates in the first place.
:::* {{tq|For this case May be there should be an option to either preserve all Nan's, or nuke them all from the result}} – Removing all NaNs would be better done by a "removeNaNs" method rather than a weird flag on "removeDuplicates".
:::* {{tq|Given that Lua already states that multiple NaN values compare effectively as "equal"}} – That's incorrect. Lua treats all NaN values as ''unequal''.
:::* {{tq|numbers in Lua never distinguish any NaN}} – I have no idea what you're trying to say here. Lua does have NaNs, for example as the result of calculating "0/0".
:::* {{tq|You "idiom" is that used in Java or C++, which is NOT directly usd in MadiaWiki or Lua (hidden in the underlying internal representation).}} – MediaWiki and Lua both use IEEE NaNs, although neither does anything special with the various types of NaN (quiet versus signaling and so on). Lua is implemented in C and uses its floating point types, including NaNs, while MediaWiki is written in PHP which is written in C (or C++ when using HHVM) and again uses C's floating point types.
:::* {{tq|Keeping these duplicates should complicates situation where we expect "keys" in Lua tables to be unique.}} — Since Lua tables cannot have NaN as a key, this seems irrelevant.
:::* {{tq|Lua treats all NaN as unique values which compare equal between each other}} – Again, this is wrong.
:::* {{tq|Note that "nil" in (which matches unspecified MediaWiki values) is NOT "NaN" and is also NOT an "empty string" value}} – I don't see where anyone else ever said it was.
:::* {{tq|"NaN" in Mediawiki is a string, and does not match the Lua NaN numeric value.}} – If you pass <code>NaN</code> as a template parameter, then yes, it will come into Lua as a string and will not match an actual Lua NaN. That doesn't seem at all relevant to this removeDuplicates method though.
:::* {{tq|It's only when we start speaking about fuzzy logic with measured accuracy}} – You seem to be going of on some completely unrelated tangent here.
:::* {{tq|In summary the statement "-- NaNs can't be table keys, and they are also unique, so we don't need to check existence." is compeltely false}} – On the contrary, it's largely true. In Lua NaN cannot be a table key. Since <code>nan ~= nan</code> is always true, arguably every NaN is "unique".
::: HTH. [[User:Anomie|Anomie]][[User talk:Anomie|⚔]] 23:11, 5 August 2018 (UTC)


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


{{edit template-protected|Module:TableTools|answered=yes}}
prefix = prefix or ''
Please add function ipairsAt()/ipairsAtOffset() as demonstrated {{diff|
suffix = suffix or ''
Module:TableTools/sandbox|diff=886292279|oldid=884152164|label=here}}. [[User:Ans|Ans]] ([[User talk:Ans|talk]]) 10:23, 4 March 2019 (UTC)
prefix = cleanPattern(prefix)
: {{ping|Ans}} What is the usecase for this function? [[User:Pppery|&#123;&#123;3x&#124;p&#125;&#125;ery]] ([[User talk:Pppery|talk]])  15:08, 4 March 2019 (UTC)
suffix = cleanPattern(suffix)
:: I plan to use in function p.call() in [[Module:LuaCall]] --[[User:Ans|Ans]] ([[User talk:Ans|talk]]) 10:40, 5 March 2019 (UTC)
local pattern = '^' .. prefix .. '([1-9]%d*)' .. suffix .. '$'
::: Those are both so simple you could just do it inline. Also, I really hope you're not planning on actually using [[Module:LuaCall]] somewhere outside of maybe your user space, per [[Module talk:LuaCall#I sincerely hope that no one ever actually uses this]]. [[User:Anomie|Anomie]][[User talk:Anomie|⚔]] 13:35, 5 March 2019 (UTC)
*{{notdone}} per above. — [[User:Xaosflux|<span style="color:#FF9933; font-weight:bold; font-family:monotype;">xaosflux</span>]] <sup>[[User talk:Xaosflux|<span style="color:#009933;">Talk</span>]]</sup> 15:00, 5 March 2019 (UTC)
::: It is not so simple, as the user need to understand the mechanism of ipairs() and iterator to do it inline.  The proposed function could help users who want to iterate like this, but don't know how to do it inline. Moreover, using ipairsAt() instead of inline will improve code readability. It does not just help developers write code, but also help beginners to understand what the code do when reading the code that use this function rather than inline code.
::: [[Module:LuaCall]] could be used to help write debugging code instantaneously in template.
::: --[[User:Ans|Ans]] ([[User talk:Ans|talk]]) 05:00, 7 March 2019 (UTC)
*{{not done}} {{tq|Edit requests to fully protected pages should only be used for edits that are either uncontroversial or supported by consensus.}} As this has been objected to already consider you are already at stage 3 of [[WP:BRD]]. — [[User:Xaosflux|<span style="color:#FF9933; font-weight:bold; font-family:monotype;">xaosflux</span>]] <sup>[[User talk:Xaosflux|<span style="color:#009933;">Talk</span>]]</sup> 02:51, 8 March 2019 (UTC)
:If after some period (ex. 7 days), no one has responsed or objected to my last reasons, does it be considered to be uncontroversial? --[[User:Ans|Ans]] ([[User talk:Ans|talk]]) 04:40, 12 March 2019 (UTC)


== Template-protected edit request on 8 March 2019 ==
local nums = {}
for k 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


{{edit template-protected|Module:TableTools|answered=yes}}
------------------------------------------------------------------------------------
Please change function p.length() to reduce loops count by one, as demonstrated {{diff|
-- numData
Module:TableTools/sandbox|diff=886736778|oldid=886292279|label=here}} (also add note for #frame.args). [[User:Ans|Ans]] ([[User talk:Ans|talk]]) 05:38, 8 March 2019 (UTC)
--
:[[File:Yes check.svg|20px|link=|alt=]] '''Done'''<!-- Template:ETp --> -- <span style="text-shadow:0 0 1px #8dd">''/[[User:Alex 21|<span style="color:#008">Alex</span>]]/[[User talk:Alex 21|<sub style="color:#008">21</sub>]]''</span> 13:06, 12 March 2019 (UTC)
-- 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


==Delete this module==
------------------------------------------------------------------------------------
{{ping|Hhkohh}} Is it a mistake that you added a tag saying this module is going to be deleted? I can't see that from the TfD. [[User:Christian75|Christian75]] ([[User talk:Christian75|talk]]) 12:37, 13 March 2019 (UTC)
-- compressSparseArray
:{{u|Christian75}}, not delete, just being merged per TfD outcome. [[User:Hhkohh|Hhkohh]] ([[User talk:Hhkohh|talk]]) 12:46, 13 March 2019 (UTC)
--
::I thought that, but the template you added says "This template is currently being deleted." [[User:Christian75|Christian75]] ([[User talk:Christian75|talk]]) 12:49, 13 March 2019 (UTC)
-- 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


==Help in writing better testcases==
------------------------------------------------------------------------------------
Hello developers, I am working with [[mw:Multilingual Templates and Modules]] and to convert this module into a shared one, we need better [[mw:Module:TableTools/testcases]]. Can anyone please help? {{ping|Frietjes}}, {{ping|RexxS}} [[User:Capankajsmilyo|Capankajsmilyo]]<sup>([[User talk:Capankajsmilyo|Talk]] | [[Wikipedia:WikiProject Infoboxes/assistance|Infobox assistance]])</sup> 10:32, 22 May 2019 (UTC)
-- sparseIpairs
:Already done, sorry to bother. [[User:Capankajsmilyo|Capankajsmilyo]]<sup>([[User talk:Capankajsmilyo|Talk]] | [[Wikipedia:WikiProject Infoboxes/assistance|Infobox assistance]])</sup> 10:38, 22 May 2019 (UTC)
--
-- 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


== Array length merger ==
------------------------------------------------------------------------------------
-- 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 _ in pairs(t) do
i = i + 1
end
return i
end


{{ping|Pppery}} Is there any reason the merged version in the sandbox was never synced with the main template? --[[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 20:48, 31 July 2019 (UTC)
local function defaultKeySort(item1, item2)
: Lack of confidence in my own ability to edit a module used on 8% of all pages without breaking something (Yes, I know I did edit this module in February). The code should be ready to go live. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 21:02, 31 July 2019 (UTC)
-- "number" < "string", so numbers will be sorted before strings.
::That is probably the correct response when dealing with these things... Having a second pair of eyes look at it (which won't be me with no module coding experience yet) sounds sensible. Also sorry for deleting your note tat you had merged it, my bad. --[[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 22:06, 31 July 2019 (UTC)
local type1, type2 = type(item1), type(item2)
::: {{ping|Mr. Stradivarius}} Does the merged code in [[Module:TableTools/sandbox]] ([[Special:Diff/899722883/899724164]]) look good to you? [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 01:43, 23 September 2019 (UTC)
if type1 ~= type2 then
:::: {{ping|Pppery}} Sorry for the late reply. The implementation is mostly good, but for empty arrays it should return 0, whereas currently it returns nil. Also, any chance you could add some test cases for it to [[Module:TableTools/testcases]]? As for naming, I would go with something different. "Binary length" sounds like it could mean the length in bytes of a table (which I know doesn't really have a meaning for Lua tables, but hey). It also isn't clear that this only works for arrays or array-like tables. Finally and most nit-pickingly, it isn't ''technically'' a [[binary search]]; [[exponential search]]es and binary searches are slightly different. How about "arrayLength" instead? The explanation of which algorithm is used would have to be relegated to the doc page, but I think this might be a clearer approach overall. Best — '''''[[User:Mr. Stradivarius|<span style="color: #194D00; font-family: Palatino, Times, serif">Mr.&nbsp;Stradivarius</span>]]''''' <sup>[[User talk:Mr. Stradivarius|♪&nbsp;talk&nbsp;♪]]</sup> 08:53, 29 September 2019 (UTC)
return type1 < type2
::::: {{ping|Mr. Stradivarius}} OK, I've [[Special:Diff/918679523|fixed the 0 versus nil bug]], and [[Special:Diff/587868130/918677165|added some test cases]]. As to the naming, I don't like <code>arrayLength</code>, because it is not meaningfully distinguishable from the pre-existing <code>length</code> function, and two functions having names that are synonyms feels like bad coding style. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 20:57, 29 September 2019 (UTC)
elseif type1 == 'table' or type1 == 'boolean' or type1 == 'function' then
:::::: {{ping|Pppery}} Ah, I had forgotten that there was also a <code>length</code> function. I agree that it is confusing to have both <code>length</code> and <code>arrayLength</code>. How about replacing <code>length</code> with <code>binaryLength</code>? It is a much more efficient algorithm for large arrays, and only marginally less efficient for very small arrays; plus it has the same requirement that no intermediate elements in the array can be nil. If any code is affected by switching from an incremental to an exponential algorithm, that code was probably broken to begin with. — '''''[[User:Mr. Stradivarius|<span style="color: #194D00; font-family: Palatino, Times, serif">Mr.&nbsp;Stradivarius</span>]]''''' <sup>[[User talk:Mr. Stradivarius|♪&nbsp;talk&nbsp;♪]]</sup> 14:25, 30 September 2019 (UTC)
return tostring(item1) < tostring(item2)
:::::: Also, thank you for the algorithm fix and for adding the test case. I split the test case up into four separate test cases; this should make it easier to pinpoint the error if a future edit causes a problem with the function. — '''''[[User:Mr. Stradivarius|<span style="color: #194D00; font-family: Palatino, Times, serif">Mr.&nbsp;Stradivarius</span>]]''''' <sup>[[User talk:Mr. Stradivarius|♪&nbsp;talk&nbsp;♪]]</sup> 14:27, 30 September 2019 (UTC)
else
::::::: {{ping|Mr. Stradivarius}} I didn't go with that originally because [[Module:Array length]] has text on its documentation page saying it shouldn't be used for small arrays but yes, that suggestion does seem reasonable. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 21:13, 30 September 2019 (UTC)
return item1 < item2
::::::: Replacement of <code>length</code> with <code>binaryLength</code> done. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 18:46, 5 October 2019 (UTC)
end
{{od}} And {{done|merged}} with main module. Let's see if anything breaks ... [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 03:41, 21 December 2019 (UTC)
end
------------------------------------------------------------------------------------
-- keysToList
--
-- Returns an array of the keys in a table, sorted using either a default
-- comparison function or a custom keySort function.
------------------------------------------------------------------------------------
function p.keysToList(t, keySort, checked)
if not checked then
checkType('keysToList', 1, t, 'table')
checkTypeMulti('keysToList', 2, keySort, {'function', 'boolean', 'nil'})
end


== Potential code simplification ==
local arr = {}
local index = 1
for k in pairs(t) do
arr[index] = k
index = index + 1
end


Keeping in mind that I don't actually know much about Lua and could easily be overlooking some obvious problem with this suggestion, the functions <code>isPositiveInteger()</code> and <code>isNan()</code> could be slightly simplified by removing the <code>if</code> structures and instead directly returning on the logic, since it should return <code>true</code> or <code>false</code>, as appropriate, already. I'd demonstrate exactly what I mean in the sandbox, but there are other outstanding changes there and I didn't want to introduce irrelevant changes without approval first. <span class=nowrap>「[[User:Dinoguy1000|<span style=color:#00f>ディノ</span><span style=color:#080>奴</span>]][[Special:Contributions/Dinoguy1000|<span style=color:#F90>千?!</span>]]」<sup>[[User talk:Dinoguy1000#top|☎ Dinoguy1000]]</sup></span> 02:15, 31 October 2019 (UTC)
if keySort ~= false then
: {{ping|Dinoguy1000}} As the user who coded the changes currently in the sandbox, feel free to demonstrate your simplification edit. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 23:43, 4 November 2019 (UTC)
keySort = type(keySort) == 'function' and keySort or defaultKeySort
::[https://en.wikipedia.org/w/index.php?title=Module:TableTools/sandbox&diff=924665819&oldid=923649632 Done]. Promisingly, all [[Module talk:TableTools/testcases|testcases]] appear to be passing after this, assuming I'm understanding that page correctly. <span class=nowrap>「[[User:Dinoguy1000|<span style=color:#00f>ディノ</span><span style=color:#080>奴</span>]][[Special:Contributions/Dinoguy1000|<span style=color:#F90>千?!</span>]]」<sup>[[User talk:Dinoguy1000#top|☎ Dinoguy1000]]</sup></span> 05:38, 5 November 2019 (UTC)
table.sort(arr, keySort)
::: {{ping|Dinoguy1000}} Looks like an improvement to me, which I will include if I stop worrying too much about editing a module with this many transclusions and actually implement the TfD I initiated in February. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 01:59, 8 November 2019 (UTC)
end
::::{{ping|Pppery}} I think a similar improvement can be made in the <code>cleanPattern()</code> subroutine of the <code>affixNums()</code> function, though I'm not sure (again, I don't know enough about Lua or Scribunto to know if there might be some gotcha there), in the event you want to look at another thing to bundle into the edit too. <span class=nowrap>「[[User:Dinoguy1000|<span style=color:#00f>ディノ</span><span style=color:#080>奴</span>]][[Special:Contributions/Dinoguy1000|<span style=color:#F90>千?!</span>]]」<sup>[[User talk:Dinoguy1000#top|☎ Dinoguy1000]]</sup></span> 03:56, 8 November 2019 (UTC)


== Protected edit request on 7 March 2020 ==
return arr
end


{{edit fully-protected|Module:TableTools|answered=yes}}
------------------------------------------------------------------------------------
change -- requiring module inline so that [[Module:Exponental search]] to -- requiring module inline so that [[Module:Exponential search]]
-- sortedPairs
(rationale : typo in Exponental / Exponential) [[Special:Contributions/82.255.235.43|82.255.235.43]] ([[User talk:82.255.235.43|talk]]) 03:45, 7 March 2020 (UTC)
--
:[[File:Red information icon with gradient background.svg|20px|link=|alt=]] '''Not done for now:'''<!-- Template:EP --> Please feel free to queue this in the sandbox. This module is used on many pages and making minor typos like that one doesn't make sense as its own edit. [[User:Izno|Izno]] ([[User talk:Izno|talk]]) 03:49, 7 March 2020 (UTC)
-- Iterates through a table, with the keys sorted using the keysToList function.
::I'm pretty late to mention this, but I fixed this and another comment, and made another minor code simplification, in [https://en.wikipedia.org/w/index.php?title=Module:TableTools/sandbox&diff=prev&oldid=952640189 this edit] to the sandbox last month. I'll leave it to more regular module editors to decide if that's significant enough to edit the live module now, though. <span class=nowrap>「[[User:Dinoguy1000|<span style=color:#00f>ディノ</span><span style=color:#080>奴</span>]][[Special:Contributions/Dinoguy1000|<span style=color:#F90>千?!</span>]]」<sup>[[User talk:Dinoguy1000#top|☎ Dinoguy1000]]</sup></span> 18:22, 19 May 2020 (UTC)
-- If there are only numerical keys, sparseIpairs is probably more efficient.
::: This module gets edited so rarely that the approach "wait for a substantive edit" isn't really feasible. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 21:49, 19 May 2020 (UTC)
------------------------------------------------------------------------------------
::::Fair enough, I've gone ahead and deployed the sandbox version. If I misunderstood your intent, feel free to [[WP:TROUT|correct me]]. =) <span class=nowrap>「[[User:Dinoguy1000|<span style=color:#00f>ディノ</span><span style=color:#080>奴</span>]][[Special:Contributions/Dinoguy1000|<span style=color:#F90>千?!</span>]]」<sup>[[User talk:Dinoguy1000#top|☎ Dinoguy1000]]</sup></span> 15:51, 23 May 2020 (UTC)
function p.sortedPairs(t, keySort)
::::: You did not misunderstand me; I would have done it myself as soon as this module was no longer used on the Main Page. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 16:08, 23 May 2020 (UTC)
checkType('sortedPairs', 1, t, 'table')
checkType('sortedPairs', 2, keySort, 'function', true)


== Bug in deepCopy ==
local arr = p.keysToList(t, keySort, true)


Has anyone tried p.deepCopy which was apparently merged in from [[Module:Table]] by {{u|Pppery}} in [[Special:Diff/884153516|diff]]? The function has a bug: in function <code>_deepCopy</code>, the three occurrences of <code>deepcopy</code> should be <code>_deepCopy</code>. If the function has never been used, perhaps it should be deleted? I see the documentation mentions that it has some benefits over mw.clone, but I don't think functions should be included in this massively used module unless they are needed by established modules. Also, there has to be some kind of testcase for each function. Searching shows there are a few modules using their own deepcopy functions, but I can't find any trying to use this one. Please don't fix the bug yet because if the module is to be changed, the proposed changes should be in the sandbox for a week or so to give others a chance to tweak things. There are a couple of comment inelegancies that might be fixed if the module is updated. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 04:02, 2 November 2020 (UTC)
local i = 0
: I have no objections to deleting the functionality. Kind of embarassing that I made a mistake like that when editing a highly-visible module. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 04:42, 2 November 2020 (UTC)
return function ()
 
i = i + 1
== reverseNumKeys, reverseSparseIpairs ==
local key = arr[i]
 
if key ~= nil then
I have added these functions to the sandbox and test cases. [[User:Trigenibinion|Trigenibinion]] ([[User talk:Trigenibinion|talk]]) 17:32, 22 February 2021 (UTC)
return key, t[key]
 
else
== Shouldn't isArray be more generic? ==
return nil, nil
end
end
end


{{ping|Pppery|Johnuniq|Izno}} Currently, isArray raises an error if the input is not a table, shouldn't it just return false, which is what a user would expect from an isXxx function? Furthermore, wouldn't it be better if it checked if the input was array-like? This would allow custom objects/containers to be considered array-like if they are iterable and only have numeric keys. [[User:Alexiscoutinho|Alexiscoutinho]] ([[User talk:Alexiscoutinho|talk]]) 14:23, 9 July 2021 (UTC)
------------------------------------------------------------------------------------
:I agree that isArray should return true or false and should never raise an error—that would be much more in keeping with Lua's style of doing the right thing when reasonable. The checkType should be replaced with a line to return false if the input is not a table. BTW, that's an amazing implementation which would never have occurred to me. I see [[#Bug in deepCopy]] above has not been addressed yet. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 00:13, 10 July 2021 (UTC)
-- isArray
::{{re|Johnuniq}} What do you think of this?
--
<syntaxhighlight lang="lua">
-- Returns true if the given value is a table and all keys are consecutive
function (obj)
-- integers starting at 1.
if not pcall(pairs, obj) then
------------------------------------------------------------------------------------
function p.isArray(v)
if type(v) ~= 'table' then
return false
return false
end
end
local i = 0
local i = 0
for _ in pairs(obj) do
for _ in pairs(v) do
i = i + 1
i = i + 1
if obj[i] == nil then
if v[i] == nil then
return false
return false
end
end
Line 170: Line 310:
return true
return true
end
end
</syntaxhighlight>
 
:::Good, but I would stick with:
------------------------------------------------------------------------------------
<syntaxhighlight lang="lua">
-- isArrayLike
function p.isArray(t)
--
if type(t) ~= 'table' then
-- Returns true if the given value is iterable and all keys are consecutive
-- integers starting at 1.
------------------------------------------------------------------------------------
function p.isArrayLike(v)
if not pcall(pairs, v) then
return false
return false
end
end
local i = 0
local i = 0
for _ in pairs(t) do
for _ in pairs(v) do
i = i + 1
i = i + 1
if t[i] == nil then
if v[i] == nil then
return false
return false
end
end
Line 186: Line 330:
return true
return true
end
end
</syntaxhighlight>
:::That's using <code>t</code> for simplicity and consistency with the rest of the module. It would not be possible for <code>pcall(pairs, obj)</code> to be successful unless <code>obj</code> is a table so whereas it is more pure to test for that, it is clearer to admit that a table is what works. I see that the function returns true if given <code>{}</code> (an empty table) and that seems desirable. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 07:16, 10 July 2021 (UTC)
::::{{re|Johnuniq}} <code>pcall(pairs, obj)</code> would work if <code>obj</code> implements <code>__pairs</code>. Although <code>obj</code> would technically still be a table, it could have a custom type (by overriding <code>type</code>, which is what [[Module:Lua class]] does). Do you think it would be better to name my function something different (isArrayLike) then? The original one could have your tweak too. [[User:Alexiscoutinho|Alexiscoutinho]] ([[User talk:Alexiscoutinho|talk]]) 15:23, 10 July 2021 (UTC)
:::::Yikes, I hadn't absorbed that [[Module:Lua class]] was seriously implementing classes. The question becomes a matter of what approach would be best in the long term, given that modules are (or will be) maintained by Wikipedians who might struggle when faced with code that has no apparent starting point or path of execution (which is the consequence of fully using classes). Also, consistency is important and isArray would look out-of-place in [[Module:TableTools]] if it is the only function with <code>obj</code> as the parameter and which tests for the method it is going to use rather than the variable type. I'm highly pragmatic and would use the naive code I put above, but you have a reason for wanting the proper implementation and that's ok by me. Regarding pragmatism, the heading comment for isArray (with its mention of a table) might be confusing, but a fully correct comment would be three times longer with the new code. Regarding duplicating the function, that's ugly as I'm sure you know. I haven't done anything with this module and I'm not sure why I'm watching here, although I have commented a couple of times. In other words, I'm happy for you to do whatever is wanted. It might be best to try something in the sandbox, preferably also fixing the deepCopy bug I mentioned, then update the module. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 02:21, 11 July 2021 (UTC)
::::::I personally think that learning to maintain class based modules is just as easy as function based ones. For more complex modules, I think OOP greatly improves readability/understanding for anyone for many reasons, e.g. variables wouldn't have to be carried everywhere in function signatures. Of course, simpler modules might not need classes. It's just a matter of using it where it's appropriate. I agree it wouldn't be symmetrical if <code>isArray</code> received <code>obj</code>, thus I will propose <code>isArrayLike</code> instead. Since these functions are so small, I don't think a small duplication would be bad. The two functions would have their own different use cases too. [[User:Alexiscoutinho|Alexiscoutinho]] ([[User talk:Alexiscoutinho|talk]]) 21:04, 11 July 2021 (UTC)
:::::::To stick up for functions, at least you can tell what the inputs are! I'm happy for you to edit as wanted and there doesn't appear to be anyone else with an opinion so please go for it. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 22:32, 11 July 2021 (UTC)
@Alexiscoutinho: I checked your edit to [[Module:TableTools/sandbox]] and it looks good, thanks. You will see that I removed trailing whitespace and removed two unused functions added to the sandbox on 22 February 2021 because I don't think we should add functions without a specified use. After my minor tweaks, along with your comment changes (a very good idea to make them consistent), it is now a nightmare to compare the main module with the sandbox, but I believe the change is good.
{{#invoke:convert/tester|compare|TableTools}}
[[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 05:37, 12 July 2021 (UTC)


=== Protected edit request on 25 July 2021 ===
------------------------------------------------------------------------------------
{{edit fully-protected|Module:TableTools|answered=y}}
-- invert
This request is to implement all the changes made to the sandbox until this time. The differences can be checked with the diff link of the above section. Summarizing changes: improved module formatting; improved isArray; added isArrayLike; fixed _deepCopy; and improved defaultKeySort. [[User:Alexiscoutinho|Alexiscoutinho]] ([[User talk:Alexiscoutinho|talk]]) 14:27, 25 July 2021 (UTC)
--
:Sorry this has taken so long to get to. {{done}}, on assurances this has been tested by you and also checked by Johnuniq. &mdash;&nbsp;Martin <small>([[User:MSGJ|MSGJ]]&nbsp;·&nbsp;[[User talk:MSGJ|talk]])</small> 10:42, 4 October 2021 (UTC)
-- Transposes the keys and values in an array. For example, {"a", "b", "c"} ->
-- {a = 1, b = 2, c = 3}. Duplicates are not supported (result values refer to
-- the index of the last duplicate) and NaN values are ignored.
------------------------------------------------------------------------------------
function p.invert(arr)
checkType("invert", 1, arr, "table")
local isNan = p.isNan
local map = {}
for i, v in ipairs(arr) do
if not isNan(v) then
map[v] = i
end
end


== keySort documentation or examples please ==
return map
end


Could someone add to documentation, or point to good live examples, of the '''{{para|keySort}}''' ''function'', as used in {{slink|Module:TableTools|keysToList|nopage=true}} and {{slink|Module:TableTools|sortedPairs|nopage=true}}? Related to [[:mw:Extension:Scribunto/Lua_reference_manual#table.sort|Lua manual:table.sort]]? I need a simple non-numerical sort ;-) Thx. -[[User:DePiep|DePiep]] ([[User talk:DePiep|talk]]) 10:19, 20 November 2021 (UTC)
------------------------------------------------------------------------------------
:If you provide a short example of dummy data to be sorted I'll have a look. As a quick example, if <code>keys</code> is a list of values each of which might be a number or a string, the following would sort the table so numbers are sorted before strings.
-- listToSet
<syntaxhighlight lang="lua">
--
table.sort(keys, function (a, b)
-- Creates a set from the array part of the table. Indexing the set by any of the
    if type(a) == type(b) then
-- values of the array returns true. For example, {"a", "b", "c"} ->
        return a < b
-- {a = true, b = true, c = true}. NaN values are ignored as Lua considers them
    elseif type(a) == 'number' then
-- never equal to any value (including other NaNs or even themselves).
        return true
------------------------------------------------------------------------------------
    end
function p.listToSet(arr)
end)
checkType("listToSet", 1, arr, "table")
</syntaxhighlight>
local isNan = p.isNan
:[[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 23:29, 20 November 2021 (UTC)
local set = {}
for _, v in ipairs(arr) do
if not isNan(v) then
set[v] = true
end
end


== Improved NaN handling ==
return set
end


{{edit template-protected|Module:TableTools|answered=y}}
------------------------------------------------------------------------------------
Please pull in [[Special:Diff/1048120640/1060800426|my changes from the sandbox]] that correct issues with NaN handling. I already [[Special:Diff/1060767140/prev|updated the testcases]] (I first made them fail and then fixed the issue in the sandbox). I considered further simplifying {{code|isNaN|lua}} since I believe NaNs are the only values in Lua that fail the not equal to self test but it might be possible with external code, e.g., userdata (but I believe Scribunto uses none of that). That said, anything not equal to itself probably should not be considered for removal in {{code|removeDuplicates|lua}}. In Lua, NaNs are always an issue when attempting to use arbitrary table values as table keys (unlike {{code|nil|lua}} which cannot be used as a table value or key and mostly come into play when dealing with sparse tables or arbitrary expression lists). —[[User:Uzume|Uzume]] ([[User talk:Uzume|talk]]) 19:37, 17 December 2021 (UTC)
-- deepCopy
 
--
{{ping|Verdy p|Johnuniq|Anomie}} It should be noted that both {{code|nil|lua}} and NaN values cannot be table keys, however, it is not an error to attempt to index a table with such values. Lua will throw errors preventing these values from being used as actual keys (i.e., {{code|rawset(tableX, nil, true)|lua}} and {{code|rawset(tableX, 0/0, true)|lua}} are guaranteed to error) but these values can be handled in index and newindex events (via {{code|__index|lua}} and {{code|__newindex|lua}} metamethods) without issue. As an example, I [[Special:Diff/1060785316/prev|initially modified]] {{code|listToSet|lua}} to consider all NaN values as if they were the same (i.e., if there is at least one NaN in the input array, indexing the returned set with any NaN yeilds {{code|true|lua}}) but decided that was not particularly useful, given Lua considers them never equal to any value (including other NaNs or even themselves). If that is considered valuable we could revert to such. —[[User:Uzume|Uzume]] ([[User talk:Uzume|talk]]) 19:37, 17 December 2021 (UTC)
-- Recursive deep copy function. Preserves identities of subtables.
:Thanks but the sandbox p.listToSet has a typo due to use of <code>v</code> in p.invert and <code>item</code> in p.listToSet. It's probably best to use <code>v</code> in both of them. By the way, the ping did not work because it was added in an edit of the comment. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 00:50, 18 December 2021 (UTC)
------------------------------------------------------------------------------------
::{{re|Johnuniq}} the mispaste was easily fixed but I cleaned up the variables across the functions anyway. Thanks. —[[User:Uzume|Uzume]] ([[User talk:Uzume|talk]]) 22:43, 30 December 2021 (UTC)
local function _deepCopy(orig, includeMetatable, already_seen)
:::I'm not going to think about anything remotely tricky for at least a week. Someone else might want to action this but I'm posting to let you know that if nothing happens you might ping me in ten days. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 23:30, 30 December 2021 (UTC)
if type(orig) ~= "table" then
:{{re|Uzume|Johnuniq}} is this still being worked on, or is there an agreeable version ready to go? — [[User:Xaosflux|<span style="color:#FF9933; font-weight:bold; font-family:monotype;">xaosflux</span>]] <sup>[[User talk:Xaosflux|<span style="color:#009933;">Talk</span>]]</sup> 15:29, 10 January 2022 (UTC)
return orig
::{{re|Xaosflux}} As far as I am concerned the version in the current sandbox is "ready to go". I have rectified the one issue {{U|Johnuniq}} found since I originally made the request. —[[User:Uzume|Uzume]] ([[User talk:Uzume|talk]]) 16:10, 10 January 2022 (UTC)
end
:::I looked at the new code. There is another problem at line 67 due to replacement of <code>t</code> with <code>arr</code> in <code>p.removeDuplicates</code> in two of the three places where <code>t</code> was used. I will have a look at what should be done but introducing <code>arr</code> when the rest of the module uses <code>t</code> is not desirable and I suspect the new <code>arr</code> should go. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 02:47, 11 January 2022 (UTC)
::::{{re|Johnuniq|Xaosflux}} That too was easily rectified. Please find the fixes in the current sandbox. Thank you, —[[User:Uzume|Uzume]] ([[User talk:Uzume|talk]]) 20:10, 27 January 2022 (UTC)
-- already_seen stores copies of tables indexed by the original table.
:::::That looks good. However now we need to think about whether a module used on 4.9 million pages should be updated with some nice-to-have improvements that are very unlikely to make a practical difference. I'm happy either way but will wait in the hope that someone else makes that decision. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 02:20, 28 January 2022 (UTC)
local copy = already_seen[orig]
::::::{{re|Johnuniq}} I find it amusing you consider correcting a bug in the NaN handling "some nice-to-have improvements that are very unlikely to make a practical difference". Such is certainly true if one never has any NaN values anywhere but clearly it has (broken) NaN support so apparently someone needed that. This was the main reason for updating the testcases first (to demonstrate the brokenness and subsequently rectify such). —[[User:Uzume|Uzume]] ([[User talk:Uzume|talk]]) 13:12, 30 January 2022 (UTC)
if copy ~= nil then
::::::I've made the changes. Thanks for working on this and Johnuniq for checking code &mdash;&nbsp;Martin <small>([[User:MSGJ|MSGJ]]&nbsp;·&nbsp;[[User talk:MSGJ|talk]])</small> 13:10, 31 January 2022 (UTC)
return copy
 
end
== Duplicate handling with "invert" ==
 
copy = {}
I wonder if it would be useful to have something like {{code|invert|lua}} that supports duplicates by providing values that are arrays of indices from the original input array. —[[User:Uzume|Uzume]] ([[User talk:Uzume|talk]]) 19:37, 17 December 2021 (UTC)
already_seen[orig] = copy -- memoize before any recursion, to avoid infinite loops
:Currently, <code>p.invert({'a','b','c','a'})</code> would give <code>{a=4,b=2,c=3}</code>. I think the proposal is that it might give <code>{a={1,4},b=2,c=3}</code>. Perhaps, but this kind of consideration might be best handled after considering a real need at Wikipedia. Another possibility would be to throw an error if a duplicate occurs, possibly controlled by a parameter saying what should happen with duplicates. I don't think we should worry about that unless a need arises. [[User:Johnuniq|Johnuniq]] ([[User talk:Johnuniq|talk]]) 01:01, 18 December 2021 (UTC)
::{{re|Johnuniq}} I agree about waiting for an actual need but I thought I would bring up this lack of functionality anyway. I was thinking more along the lines of: {{code|p.invert{'a','b','c','a'}|lua}}→{{code|{a{{=}}{1,4},b{{=}}{2},c{{=}}{3}<noinclude/>}|lua}} but you got the gist of my suggestion/observation.—[[User:Uzume|Uzume]] ([[User talk:Uzume|talk]]) 23:00, 30 December 2021 (UTC)
for orig_key, orig_value in pairs(orig) do
 
copy[_deepCopy(orig_key, includeMetatable, already_seen)] = _deepCopy(orig_value, includeMetatable, already_seen)
== p.merge() function ==
end
 
Hello, this module is missing one function that allows to merge multiple tables into one table. It would be nice if someone add it:
if includeMetatable then
<syntaxhighlight lang="lua">
local mt = getmetatable(orig)
function p.merge(...)
if mt ~= nil then
local tables = { ... }
setmetatable(copy, _deepCopy(mt, true, already_seen))
local result = {}
for i, t in ipairs(tables) do
checkType('merge', i, t, 'table')
for k, v in pairs(t) do
if not result[k] then
result[k] = v
end
end
end
end
end
return result
return copy
end
end
</syntaxhighlight>


Example: <code>p.merge({1, 2, ["a"] = "b"}, {10, [3] = 3, ["a"] = "a"}, {["b"] = "test"})</code>
function p.deepCopy(orig, noMetatable, already_seen)
checkType("deepCopy", 3, already_seen, "table", true)
return _deepCopy(orig, not noMetatable, already_seen or {})
end


Output: <code>{1, 2, 3, ["a"] = "b", ["b"] = "test"}</code> — [[User:Антрактидов|🎭 <span style="color: #A23B6C">Антарктидов (AKA Antraktidov)</span>]] ([[User talk:Антрактидов|talk page]] | [[Special:Contributions/Антрактидов|contribution]]) 21:41, 13 December 2023 (UTC)
------------------------------------------------------------------------------------
-- sparseConcat
--
-- Concatenates all values in the table that are indexed by a number, in order.
-- sparseConcat{a, nil, c, d}  ="acd"
-- sparseConcat{nil, b, c, d}  => "bcd"
------------------------------------------------------------------------------------
function p.sparseConcat(t, sep, i, j)
local arr = {}


:{{re|Антрактидов}} [[Module:Set]] already implements such merging functions of arbitrary tables with its "union" functions. [[User:Alexiscoutinho|Alexis Coutinho]] ([[User talk:Alexiscoutinho|talk]]) <sup>&#91;''[[WP:PING|ping&nbsp;me]]''&#93;</sup> 23:59, 22 April 2024 (UTC)
local arr_i = 0
for _, v in p.sparseIpairs(t) do
arr_i = arr_i + 1
arr[arr_i] = v
end


== deepEquals function ==
return table.concat(arr, sep, i, j)
end


@[[User:Pppery|Pppery]] You may or may not be interested in a revised version of the <code>deepEquals</code> function currently at [[wikt:Module:table]], which I note this module lacks, which checks for structural equivalence between key/value pairs; arbitrary recursive nesting and tables-as-keys are both supported. It's got a few speed optimisations that make the code a little verbose in places, but parts could easily be simplified if you feel that's not necessary here. [[User:Theknightwho|Theknightwho]] ([[User talk:Theknightwho|talk]]) 11:24, 14 April 2024 (UTC)
------------------------------------------------------------------------------------
-- length
--
-- Finds the length of an array, or of a quasi-array with keys such as "data1",
-- "data2", etc., using an exponential search algorithm. It is similar to the
-- operator #, but may return a different value when there are gaps in the array
-- portion of the table. Intended to be used on data loaded with mw.loadData. For
-- other tables, use #.
-- Note: #frame.args in frame object always be set to 0, regardless of the number
-- of unnamed template parameters, so use this function for frame.args.
------------------------------------------------------------------------------------
function p.length(t, prefix)
-- requiring module inline so that [[Module:Exponential search]] which is
-- only needed by this one function doesn't get millions of transclusions
local expSearch = require("Module:Exponential search")
checkType('length', 1, t, 'table')
checkType('length', 2, prefix, 'string', true)
return expSearch(function (i)
local key
if prefix then
key = prefix .. tostring(i)
else
key = i
end
return t[key] ~= nil
end) or 0
end


== Infinite loop in deepCopy ==
------------------------------------------------------------------------------------
-- inArray
--
-- Returns true if searchElement is a member of the array, and false otherwise.
-- Equivalent to JavaScript array.includes(searchElement) or
-- array.includes(searchElement, fromIndex), except fromIndex is 1 indexed
------------------------------------------------------------------------------------
function p.inArray(array, searchElement, fromIndex)
checkType("inArray", 1, array, "table")
-- if searchElement is nil, error?


There's a bug in the <code>deepCopy</code> function which manifests with any kind of recursive table nesting. However, it's easy to fix, as you just need to cache the new copy in {{code|already_seen|lua}} before doing the {{code|pairs|lua}} loop, like this:
fromIndex = tonumber(fromIndex)
<syntaxhighlight lang=lua>
if fromIndex then
if type(orig) == 'table' then
if (fromIndex < 0) then
copy = {}
fromIndex = #array + fromIndex + 1
already_seen[orig] = copy
for orig_key, orig_value in pairs(orig) do
copy[_deepCopy(orig_key, includeMetatable, already_seen)] = _deepCopy(orig_value, includeMetatable, already_seen)
end
end
 
if fromIndex < 1 then fromIndex = 1 end
if includeMetatable then
for _, v in ipairs({unpack(array, fromIndex)}) do
local mt = getmetatable(orig)
if v == searchElement then
if mt ~= nil then
return true
local mt_copy = _deepCopy(mt, includeMetatable, already_seen)
end
setmetatable(copy, mt_copy)
end
already_seen[mt] = mt_copy
else
for _, v in pairs(array) do
if v == searchElement then
return true
end
end
end
end
else -- number, string, boolean, etc
copy = orig
end
end
</syntaxhighlight>
return false
[[User:Theknightwho|Theknightwho]] ([[User talk:Theknightwho|talk]]) 13:03, 14 April 2024 (UTC)
end


:{{re|Theknightwho}} You may want to propose such changes in the sandbox. [[User:Alexiscoutinho|Alexis Coutinho]] ([[User talk:Alexiscoutinho|talk]]) <sup>&#91;''[[WP:PING|ping&nbsp;me]]''&#93;</sup> 00:09, 23 April 2024 (UTC)
------------------------------------------------------------------------------------
::@[[User:Alexiscoutinho|Alexiscoutinho]] I've updated <code>deepCopy</code> with a fix in the sandbox, and introduced various optimisations, but I haven't added <code>deepEquals</code> since I'm not sure whether the WP would want table metamethods to be respected when traversing over the tables. The Wiktionary version does a strict comparison (i.e. metamethods are ignored), but that's what makes sense for our needs; however, that might be unintuitive for users who want to compare frame argument tables, for instance. [[User:Theknightwho|Theknightwho]] ([[User talk:Theknightwho|talk]]) 16:05, 24 April 2024 (UTC)
-- merge
--
-- Given the arrays, returns an array containing the elements of each input array
-- in sequence.
------------------------------------------------------------------------------------
function p.merge(...)
local arrays = {...}
local ret = {}
for i, arr in ipairs(arrays) do
checkType('merge', i, arr, 'table')
for _, v in ipairs(arr) do
ret[#ret + 1] = v
end
end
return ret
end


== Edit request 23 April 2024 ==
------------------------------------------------------------------------------------
{{Edit fully-protected|answered=yes}}
-- extend
--
-- Extends the first array in place by appending all elements from the second
-- array.
------------------------------------------------------------------------------------
function p.extend(arr1, arr2)
checkType('extend', 1, arr1, 'table')
checkType('extend', 2, arr2, 'table')


This request is to implement all the changes made to the sandbox until this time. The differences can be checked with [https://en.wikipedia.org/w/index.php?title=Module%3ATableTools%2Fsandbox&diff=1220306247&oldid=1068321711 this diff link].
for _, v in ipairs(arr2) do
 
arr1[#arr1 + 1] = v
Notably, I added the <code>merge</code> and <code>extend</code> functions for arrays. Pretty sure such functions are standard in any language that has array-like objects. Therefore, I found it appropriate to make them available here since Scribunto apparently didn't port them from base Lua. [[User:Alexiscoutinho|Alexis Coutinho]] ([[User talk:Alexiscoutinho|talk]]) <sup>&#91;''[[WP:PING|ping&nbsp;me]]''&#93;</sup> 00:07, 23 April 2024 (UTC)
end
 
end
:Guess I should add test cases. [[User:Alexiscoutinho|Alexis Coutinho]] ([[User talk:Alexiscoutinho|talk]]) <sup>&#91;''[[WP:PING|ping&nbsp;me]]''&#93;</sup> 18:13, 24 April 2024 (UTC)
::@[[User:Alexiscoutinho|Alexiscoutinho]] I don't really see any benefit from throwing an error if <code>merge</code> only receives 1 argument; it's plausible that a module might want to merge an arbitrary number of arrays based on user input, and all this restriction does is push the check for 1 argument onto the calling module. It's the same reason you can call (e.g.) {{code|math.min()|lua}} or {{code|math.max()|lua}} with only 1 argument, despite that being pointless in many situations. [[User:Theknightwho|Theknightwho]] ([[User talk:Theknightwho|talk]]) 00:30, 25 April 2024 (UTC)
:::👍 [[User:Alexiscoutinho|Alexis Coutinho]] ([[User talk:Alexiscoutinho|talk]]) <sup>&#91;''[[WP:PING|ping&nbsp;me]]''&#93;</sup> 01:14, 25 April 2024 (UTC)
:::{{done}}. But I wonder if a check should exist to verify if the inputs form a sequence (i.e. not sparse table)... [[User:Alexiscoutinho|Alexis Coutinho]] ([[User talk:Alexiscoutinho|talk]]) <sup>&#91;''[[WP:PING|ping&nbsp;me]]''&#93;</sup> 07:09, 26 April 2024 (UTC)
::{{done}} aswell. This edit request is ready for review (and I guess {{u|Theknightwho}}'s changes too). [[User:Alexiscoutinho|Alexis Coutinho]] ([[User talk:Alexiscoutinho|talk]]) <sup>&#91;''[[WP:PING|ping&nbsp;me]]''&#93;</sup> 08:03, 26 April 2024 (UTC)
: {{done|Copied}} over to live module. [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 01:49, 27 April 2024 (UTC)
::{{ty}}. [[User:Alexiscoutinho|Alexis Coutinho]] ([[User talk:Alexiscoutinho|talk]]) <sup>&#91;''[[WP:PING|ping&nbsp;me]]''&#93;</sup> 06:07, 27 April 2024 (UTC)
 
== Protected edit request on 14 August 2024 ==


{{edit fully-protected|Module:TableTools|answered=yes}}
return p
Replace inArray with the code from [[Module:Includes]], as done in the [[Special:Permalink/1240320809|sandbox]]. Functionality is equivalent to the current function, but it adds a "fromIndex" parameter to bring it up to feature parity with the javascript [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes Array.prototype.includes()] function. <span class="nowrap">--[[User:Ahecht|Ahecht]] ([[User talk:Ahecht|<b style="color:#FFF;background:#04A;display:inline-block;padding:1px;vertical-align:middle;font:bold 50%/1 sans-serif;text-align:center">TALK<br />PAGE</b>]])</span> 20:05, 14 August 2024 (UTC)
: {{done}} [[User:Pppery|* Pppery *]] [[User talk:Pppery|<sub style="color:#800000">it has begun...</sub>]] 04:19, 18 August 2024 (UTC)

Latest revision as of 16:58, 26 December 2025

Template:Used in system Template:Module rating Template:Cascade-protected template

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.

Loading the module

To use any of the functions, first you must load the module.

local TableTools = require('Module:TableTools')

isPositiveInteger

TableTools.isPositiveInteger(value)

Returns true if 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.

isNan

TableTools.isNan(value)

Returns true if value 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 value is used as a table key.)

shallowClone

TableTools.shallowClone(t)

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. If you want to make a new table with no shared subtables and with metatables transferred, you can use mw.clone instead. If you want to make a new table with no shared subtables and without metatables transferred, use deepCopy with the noMetatable option.

removeDuplicates

TableTools.removeDuplicates(t)

Removes duplicate values from an array. This function is only designed to work with standard arrays: keys that are not positive integers are ignored, as are all values after the first nil value. (For arrays containing nil values, you can use compressSparseArray first.) The function tries to preserve the order of the array: the earliest non-unique value is kept, and all subsequent duplicate values are removed. For example, for the table Template:Code removeDuplicates will return Template:Code.

numKeys

TableTools.numKeys(t)

Takes a table t and returns an array containing the numbers of any positive integer keys that have non-nil values, sorted in numerical order. For example, for the table Template:Code, numKeys will return Template:Code.

affixNums

TableTools.affixNums(t, prefix, suffix)

Takes a table t and returns an array containing the numbers of keys with the optional prefix prefix and the optional suffix suffix. For example, for the table Template:Code and the prefix 'a', affixNums will return Template:Code. All characters in prefix and suffix are interpreted literally.

See Template:Mfl and Template:Mfl for doing something similar from a wiki template.

numData

TableTools.numData(t, compress)

Given a table with keys like "foo1", "bar1", "foo2", and "baz2", returns a table of subtables in the format Template:Code. 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.

compressSparseArray

TableTools.compressSparseArray(t)

Takes an array t 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. Any keys that are not positive integers are removed. For example, for the table Template:Code, compressSparseArray will return Template:Code.

See Template:Mfl for doing something similar from a wiki template.

sparseIpairs

TableTools.sparseIpairs(t)

This is an iterator function for traversing a sparse array t. It is similar to ipairs, but will continue to iterate until the highest numerical key, whereas ipairs may stop after the first nil value. Any keys that are not positive integers are ignored.

Usually sparseIpairs is used in a generic for loop.

for i, v in TableTools.sparseIpairs(t) do
   -- code block
end

Note that sparseIpairs uses the pairs function in its implementation. Although some table keys appear to be ignored, all table keys are accessed when it is run.

size

TableTools.size(t)

Finds the size of a key/value pair table (associative array). For example, for Template:Code, size will return 2. The function will also work on arrays, but for arrays it is more efficient to use the # operator. Note that to find the size, this function uses the pairs function to iterate through all of the keys.

keysToList

TableTools.keysToList(t, keySort, checked)

Returns a list of the keys in a table, sorted using either a default comparison function or a custom keySort function, which follows the same rules as the comp function supplied to table.sort. If keySort is false, no sorting is done. Set checked to true to skip the internal type checking.

sortedPairs

TableTools.sortedPairs(t, keySort)

Iterates through a table, with the keys sorted using the keysToList function. If there are only numerical keys, sparseIpairs is probably more efficient.

isArray

TableTools.isArray(value)

Returns true if value is a table and all keys are consecutive integers starting at 1.

isArrayLike

TableTools.isArrayLike(value)

Returns true if value is iterable and all keys are consecutive integers starting at 1.

invert

TableTools.invert(arr)

Transposes the keys and values in an array. For example, Template:Code yields Template:Code.

listToSet

TableTools.listToSet(arr)

Creates a set from the array part of the table arr. Indexing the set by any of the values of the array returns true. For example, Template:Code yields Template:Code.

deepCopy

TableTools.deepCopy(orig, noMetatable, alreadySeen)

Creates a copy of the table orig. As with mw.clone, all values that are not functions are duplicated and the identity of tables is preserved. If noMetatable is true, then the metatable (if any) is not copied. Can copy tables loaded with mw.loadData.

Similar to mw.clone, but mw.clone cannot copy tables loaded with mw.loadData and does not allow metatables not to be copied.

sparseConcat

TableTools.sparseConcat(t, sep, i, j)

Concatenates all values in the table that are indexed by a positive integer, in order. For example, Template:Code yields Template:Code and Template:Code yields Template:Code.

length

TableTools.length(t, prefix)

Finds the length of an array or of a quasi-array with keys with an optional prefix such as "data1", "data2", etc. It uses an exponential search algorithm to find the length, so as to use as few table lookups as possible.

This algorithm is useful for arrays that use metatables (e.g. frame.args) and for quasi-arrays. For normal arrays, just use the # operator, as it is implemented in C and will be quicker.

inArray

TableTools.inArray(array, searchElement)
TableTools.inArray(array, searchElement, fromIndex)

Returns true if searchElement is a member of the array array, and false otherwise. Equivalent to the javascript Array.prototype.includes() function, except fromIndex is 1-indexed instead of zero-indexed.

fromIndex

fromIndex is the optional 1-based index at which to start searching. If fromIndex is not present, all values in the array will be searched and the array will be treated as a table/associative array (it will be iterated over using pairs()).

If fromIndex is present and an integer, the array is assumed to be a conventional array/sequence/list (indexed with consecutive integer keys starting at 1, and interated over using ipairs()). Only the values whose index is fromIndex or higher will be searched.

In the following examples, #array represents the length of the integer-keyed portion of the array.

  • If fromIndex < 0 it will count back from the end of the array, e.g. a value of -1 will only search the last integer-keyed element in the array. If fromIndex <= (-1 * #array), the entire integer-keyed portion of the array will be searched.
  • If fromIndex = 0 it will be treated as a 1 and the entire integer-keyed portion of the array will be searched.
  • If fromIndex > #array, the array is not searched and false is returned.

merge

TableTools.merge(...)

Given the arrays, returns an array containing the elements of each input array in sequence.

extend

TableTools.extend(arr1, arr2)

Extends the first array in place by appending all elements from the second array.

See also


------------------------------------------------------------------------------------
--                                   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
local checkTypeMulti = libraryUtil.checkTypeMulti

------------------------------------------------------------------------------------
-- 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)
	return type(v) == 'number' and v >= 1 and floor(v) == v and v < infinity
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)
	return type(v) == 'number' and v ~= v
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)
	checkType('shallowClone', 1, t, 'table')
	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(arr)
	checkType('removeDuplicates', 1, arr, 'table')
	local isNan = p.isNan
	local ret, exists = {}, {}
	for _, v in ipairs(arr) 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
		elseif not exists[v] then
			ret[#ret + 1] = v
			exists[v] = true
		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 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.
		return s:gsub('([%(%)%%%.%[%]%*%+%-%?%^%$])', '%%%1')
	end

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

	local nums = {}
	for k 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 _ in pairs(t) do
		i = i + 1
	end
	return i
end

local function defaultKeySort(item1, item2)
	-- "number" < "string", so numbers will be sorted before strings.
	local type1, type2 = type(item1), type(item2)
	if type1 ~= type2 then
		return type1 < type2
	elseif type1 == 'table' or type1 == 'boolean' or type1 == 'function' then
		return tostring(item1) < tostring(item2)
	else
		return item1 < item2
	end
end
------------------------------------------------------------------------------------
-- keysToList
--
-- Returns an array of the keys in a table, sorted using either a default
-- comparison function or a custom keySort function.
------------------------------------------------------------------------------------
function p.keysToList(t, keySort, checked)
	if not checked then
		checkType('keysToList', 1, t, 'table')
		checkTypeMulti('keysToList', 2, keySort, {'function', 'boolean', 'nil'})
	end

	local arr = {}
	local index = 1
	for k in pairs(t) do
		arr[index] = k
		index = index + 1
	end

	if keySort ~= false then
		keySort = type(keySort) == 'function' and keySort or defaultKeySort
		table.sort(arr, keySort)
	end

	return arr
end

------------------------------------------------------------------------------------
-- sortedPairs
--
-- Iterates through a table, with the keys sorted using the keysToList function.
-- If there are only numerical keys, sparseIpairs is probably more efficient.
------------------------------------------------------------------------------------
function p.sortedPairs(t, keySort)
	checkType('sortedPairs', 1, t, 'table')
	checkType('sortedPairs', 2, keySort, 'function', true)

	local arr = p.keysToList(t, keySort, true)

	local i = 0
	return function ()
		i = i + 1
		local key = arr[i]
		if key ~= nil then
			return key, t[key]
		else
			return nil, nil
		end
	end
end

------------------------------------------------------------------------------------
-- isArray
--
-- Returns true if the given value is a table and all keys are consecutive
-- integers starting at 1.
------------------------------------------------------------------------------------
function p.isArray(v)
	if type(v) ~= 'table' then
		return false
	end
	local i = 0
	for _ in pairs(v) do
		i = i + 1
		if v[i] == nil then
			return false
		end
	end
	return true
end

------------------------------------------------------------------------------------
-- isArrayLike
--
-- Returns true if the given value is iterable and all keys are consecutive
-- integers starting at 1.
------------------------------------------------------------------------------------
function p.isArrayLike(v)
	if not pcall(pairs, v) then
		return false
	end
	local i = 0
	for _ in pairs(v) do
		i = i + 1
		if v[i] == nil then
			return false
		end
	end
	return true
end

------------------------------------------------------------------------------------
-- invert
--
-- Transposes the keys and values in an array. For example, {"a", "b", "c"} ->
-- {a = 1, b = 2, c = 3}. Duplicates are not supported (result values refer to
-- the index of the last duplicate) and NaN values are ignored.
------------------------------------------------------------------------------------
function p.invert(arr)
	checkType("invert", 1, arr, "table")
	local isNan = p.isNan
	local map = {}
	for i, v in ipairs(arr) do
		if not isNan(v) then
			map[v] = i
		end
	end

	return map
end

------------------------------------------------------------------------------------
-- listToSet
--
-- Creates a set from the array part of the table. Indexing the set by any of the
-- values of the array returns true. For example, {"a", "b", "c"} ->
-- {a = true, b = true, c = true}. NaN values are ignored as Lua considers them
-- never equal to any value (including other NaNs or even themselves).
------------------------------------------------------------------------------------
function p.listToSet(arr)
	checkType("listToSet", 1, arr, "table")
	local isNan = p.isNan
	local set = {}
	for _, v in ipairs(arr) do
		if not isNan(v) then
			set[v] = true
		end
	end

	return set
end

------------------------------------------------------------------------------------
-- deepCopy
--
-- Recursive deep copy function. Preserves identities of subtables.
------------------------------------------------------------------------------------
local function _deepCopy(orig, includeMetatable, already_seen)
	if type(orig) ~= "table" then
		return orig
	end
	
	-- already_seen stores copies of tables indexed by the original table.
	local copy = already_seen[orig]
	if copy ~= nil then
		return copy
	end
	
	copy = {}
	already_seen[orig] = copy -- memoize before any recursion, to avoid infinite loops
	
	for orig_key, orig_value in pairs(orig) do
		copy[_deepCopy(orig_key, includeMetatable, already_seen)] = _deepCopy(orig_value, includeMetatable, already_seen)
	end
	
	if includeMetatable then
		local mt = getmetatable(orig)
		if mt ~= nil then
			setmetatable(copy, _deepCopy(mt, true, already_seen))
		end
	end
	
	return copy
end

function p.deepCopy(orig, noMetatable, already_seen)
	checkType("deepCopy", 3, already_seen, "table", true)
	return _deepCopy(orig, not noMetatable, already_seen or {})
end

------------------------------------------------------------------------------------
-- sparseConcat
--
-- Concatenates all values in the table that are indexed by a number, in order.
-- sparseConcat{a, nil, c, d}  =>  "acd"
-- sparseConcat{nil, b, c, d}  =>  "bcd"
------------------------------------------------------------------------------------
function p.sparseConcat(t, sep, i, j)
	local arr = {}

	local arr_i = 0
	for _, v in p.sparseIpairs(t) do
		arr_i = arr_i + 1
		arr[arr_i] = v
	end

	return table.concat(arr, sep, i, j)
end

------------------------------------------------------------------------------------
-- length
--
-- Finds the length of an array, or of a quasi-array with keys such as "data1",
-- "data2", etc., using an exponential search algorithm. It is similar to the
-- operator #, but may return a different value when there are gaps in the array
-- portion of the table. Intended to be used on data loaded with mw.loadData. For
-- other tables, use #.
-- Note: #frame.args in frame object always be set to 0, regardless of  the number
-- of unnamed template parameters, so use this function for frame.args.
------------------------------------------------------------------------------------
function p.length(t, prefix)
	-- requiring module inline so that [[Module:Exponential search]] which is
	-- only needed by this one function doesn't get millions of transclusions
	local expSearch = require("Module:Exponential search")
	checkType('length', 1, t, 'table')
	checkType('length', 2, prefix, 'string', true)
	return expSearch(function (i)
		local key
		if prefix then
			key = prefix .. tostring(i)
		else
			key = i
		end
		return t[key] ~= nil
	end) or 0
end

------------------------------------------------------------------------------------
-- inArray
--
-- Returns true if searchElement is a member of the array, and false otherwise.
-- Equivalent to JavaScript array.includes(searchElement) or
-- array.includes(searchElement, fromIndex), except fromIndex is 1 indexed
------------------------------------------------------------------------------------
function p.inArray(array, searchElement, fromIndex)
	checkType("inArray", 1, array, "table")
	-- if searchElement is nil, error?

	fromIndex = tonumber(fromIndex)
	if fromIndex then
		if (fromIndex < 0) then
			fromIndex = #array + fromIndex + 1
		end
		if fromIndex < 1 then fromIndex = 1 end
		for _, v in ipairs({unpack(array, fromIndex)}) do
			if v == searchElement then
				return true
			end
		end
	else
		for _, v in pairs(array) do
			if v == searchElement then
				return true
			end
		end
	end
	return false
end

------------------------------------------------------------------------------------
-- merge
--
-- Given the arrays, returns an array containing the elements of each input array
-- in sequence.
------------------------------------------------------------------------------------
function p.merge(...)
	local arrays = {...}
	local ret = {}
	for i, arr in ipairs(arrays) do
		checkType('merge', i, arr, 'table')
		for _, v in ipairs(arr) do
			ret[#ret + 1] = v
		end
	end
	return ret
end

------------------------------------------------------------------------------------
-- extend
--
-- Extends the first array in place by appending all elements from the second
-- array.
------------------------------------------------------------------------------------
function p.extend(arr1, arr2)
	checkType('extend', 1, arr1, 'table')
	checkType('extend', 2, arr2, 'table')

	for _, v in ipairs(arr2) do
		arr1[#arr1 + 1] = v
	end
end

return p