556 lines
16 KiB
Lua
556 lines
16 KiB
Lua
--- Extension of the Lua 5.2 table library.
|
|
---
|
|
--- **NOTE:** Several functions in this module will only work with [arrays](https://www.lua.org/pil/11.1.html),
|
|
--- which are tables with sequentially numbered keys. All table functions will work with arrays as well, but
|
|
--- array functions **will not** work with tables.
|
|
--- ```lua
|
|
--- local flib_table: = require("__flib__/table")
|
|
--- ```
|
|
--- @class flib_table: tablelib
|
|
local flib_table = {}
|
|
|
|
-- Import lua table functions
|
|
for name, func in pairs(table) do
|
|
flib_table[name] = func
|
|
end
|
|
|
|
--- Shallow copy an array's values into a new array.
|
|
---
|
|
--- This function is optimized specifically for arrays, and should be used in place of `table.shallow_copy` for arrays.
|
|
--- @param arr Array
|
|
--- @return Array
|
|
function flib_table.array_copy(arr)
|
|
local new_arr = {}
|
|
for i = 1, #arr do
|
|
new_arr[i] = arr[i]
|
|
end
|
|
return new_arr
|
|
end
|
|
|
|
--- Merge all of the given arrays into a single array.
|
|
--- @param arrays Array An array of arrays to merge.
|
|
--- @return Array
|
|
function flib_table.array_merge(arrays)
|
|
local output = {}
|
|
local i = 0
|
|
for j = 1, #arrays do
|
|
local arr = arrays[j]
|
|
for k = 1, #arr do
|
|
i = i + 1
|
|
output[i] = arr[k]
|
|
end
|
|
end
|
|
return output
|
|
end
|
|
|
|
--- Recursively compare two tables for inner equality.
|
|
---
|
|
--- Does not compare metatables.
|
|
--- @param tbl1 table
|
|
--- @param tbl2 table
|
|
--- @return boolean
|
|
function flib_table.deep_compare(tbl1, tbl2)
|
|
if tbl1 == tbl2 then
|
|
return true
|
|
end
|
|
for k, v in pairs(tbl1) do
|
|
if type(v) == "table" and type(tbl2[k]) == "table" then
|
|
if not flib_table.deep_compare(v, tbl2[k]) then
|
|
return false
|
|
end
|
|
else
|
|
if v ~= tbl2[k] then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
for k in pairs(tbl2) do
|
|
if tbl1[k] == nil then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
--- Recursively copy the contents of a table into a new table.
|
|
---
|
|
--- Does not create new copies of Factorio objects.
|
|
--- @generic T
|
|
--- @param tbl T The table to make a copy of.
|
|
--- @return T
|
|
function flib_table.deep_copy(tbl)
|
|
local lookup_table = {}
|
|
local function _copy(object)
|
|
if type(object) ~= "table" then
|
|
return object
|
|
-- don't copy factorio rich objects
|
|
elseif object.__self then
|
|
return object
|
|
elseif lookup_table[object] then
|
|
return lookup_table[object]
|
|
end
|
|
|
|
local new_table = {}
|
|
lookup_table[object] = new_table
|
|
for index, value in pairs(object) do
|
|
new_table[_copy(index)] = _copy(value)
|
|
end
|
|
|
|
return setmetatable(new_table, getmetatable(object))
|
|
end
|
|
return _copy(tbl)
|
|
end
|
|
|
|
--- Recursively merge two or more tables.
|
|
---
|
|
--- Values from earlier tables are overwritten by values from later tables, unless both values are tables, in which case
|
|
--- they are recursively merged.
|
|
---
|
|
--- Non-merged tables are deep-copied, so the result is brand-new.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local tbl = {foo = "bar"}
|
|
--- log(tbl.foo) -- logs "bar"
|
|
--- log (tbl.bar) -- errors (key is nil)
|
|
--- tbl = table.merge{tbl, {foo = "baz", set = 3}}
|
|
--- log(tbl.foo) -- logs "baz"
|
|
--- log(tbl.set) -- logs "3"
|
|
--- ```
|
|
--- @param tables Array An array of tables to merge.
|
|
--- @return table
|
|
function flib_table.deep_merge(tables)
|
|
local output = {}
|
|
for _, tbl in ipairs(tables) do
|
|
for k, v in pairs(tbl) do
|
|
if type(v) == "table" then
|
|
if type(output[k] or false) == "table" then
|
|
output[k] = flib_table.deep_merge({ output[k], v })
|
|
else
|
|
output[k] = flib_table.deep_copy(v)
|
|
end
|
|
else
|
|
output[k] = v
|
|
end
|
|
end
|
|
end
|
|
return output
|
|
end
|
|
|
|
--- Find and return the first key containing the given value.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local tbl = {"foo", "bar"}
|
|
--- local key_of_foo = table.find(tbl, "foo") -- 1
|
|
--- local key_of_baz = table.find(tbl, "baz") -- nil
|
|
--- ```
|
|
--- @generic K, V
|
|
--- @param tbl table<K, V> The table to search.
|
|
--- @param value V The value to match. Must have an `eq` metamethod set, otherwise will error.
|
|
--- @return K? key The first key corresponding to `value`, if any.
|
|
function flib_table.find(tbl, value)
|
|
for k, v in pairs(tbl) do
|
|
if v == value then
|
|
return k
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Call the given function for each item in the table, and abort if the function returns truthy.
|
|
---
|
|
--- Calls `callback(value, key)` for each item in the table, and immediately ceases iteration if the callback returns truthy.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local tbl = {1, 2, 3, 4, 5}
|
|
--- -- Run a function for each item (identical to a standard FOR loop)
|
|
--- table.for_each(tbl, function(v) game.print(v) end)
|
|
--- -- Determine if any value in the table passes the test
|
|
--- local value_is_even = table.for_each(tbl, function(v) return v % 2 == 0 end)
|
|
--- -- Determine if ALL values in the table pass the test (invert the test result and function return)
|
|
--- local all_values_less_than_six = not table.for_each(tbl, function(v) return not (v < 6) end)
|
|
--- ```
|
|
--- @generic K, V
|
|
--- @param tbl table<K, V>
|
|
--- @param callback fun(value: V, key: K): boolean Receives `value` and `key` as parameters.
|
|
--- @return boolean Whether the callback returned truthy for any one item, and thus halted iteration.
|
|
function flib_table.for_each(tbl, callback)
|
|
for k, v in pairs(tbl) do
|
|
if callback(v, k) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Call the given function on a set number of items in a table, returning the next starting key.
|
|
---
|
|
--- Calls `callback(value, key)` over `n` items from `tbl`, starting after `from_k`.
|
|
---
|
|
--- The first return value of each invocation of `callback` will be collected and returned in a table keyed by the
|
|
--- current item's key.
|
|
---
|
|
--- The second return value of `callback` is a flag requesting deletion of the current item.
|
|
---
|
|
--- The third return value of `callback` is a flag requesting that the iteration be immediately aborted. Use this flag to
|
|
--- early return on some condition in `callback`. When aborted, `for_n_of` will return the previous key as `from_k`, so
|
|
--- the next call to `for_n_of` will restart on the key that was aborted (unless it was also deleted).
|
|
---
|
|
--- **DO NOT** delete entires from `tbl` from within `callback`, this will break the iteration. Use the deletion flag
|
|
--- instead.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local extremely_large_table = {
|
|
--- [1000] = 1,
|
|
--- [999] = 2,
|
|
--- [998] = 3,
|
|
--- ...,
|
|
--- [2] = 999,
|
|
--- [1] = 1000,
|
|
--- }
|
|
--- event.on_tick(function()
|
|
--- global.from_k = table.for_n_of(extremely_large_table, global.from_k, 10, function(v) game.print(v) end)
|
|
--- end)
|
|
--- ```
|
|
--- @generic K, V, C
|
|
--- @param tbl table<K, V> The table to iterate over.
|
|
--- @param from_k K The key to start iteration at, or `nil` to start at the beginning of `tbl`. If the key does not exist in `tbl`, it will be treated as `nil`, _unless_ a custom `_next` function is used.
|
|
--- @param n number The number of items to iterate.
|
|
--- @param callback fun(value: V, key: K):C,boolean,boolean Receives `value` and `key` as parameters.
|
|
--- @param _next? fun(tbl: table<K, V>, from_k: K):K,V A custom `next()` function. If not provided, the default `next()` will be used.
|
|
--- @return K? next_key Where the iteration ended. Can be any valid table key, or `nil`. Pass this as `from_k` in the next call to `for_n_of` for `tbl`.
|
|
--- @return table<K, C> results The results compiled from the first return of `callback`.
|
|
--- @return boolean reached_end Whether or not the end of the table was reached on this iteration.
|
|
function flib_table.for_n_of(tbl, from_k, n, callback, _next)
|
|
-- Bypass if a custom `next` function was provided
|
|
if not _next then
|
|
-- Verify start key exists, else start from scratch
|
|
if from_k and not tbl[from_k] then
|
|
from_k = nil
|
|
end
|
|
-- Use default `next`
|
|
_next = next
|
|
end
|
|
|
|
local delete
|
|
local prev
|
|
local abort
|
|
local result = {}
|
|
|
|
-- Run `n` times
|
|
for _ = 1, n, 1 do
|
|
local v
|
|
if not delete then
|
|
prev = from_k
|
|
end
|
|
from_k, v = _next(tbl, from_k)
|
|
if delete then
|
|
tbl[delete] = nil
|
|
end
|
|
|
|
if from_k then
|
|
result[from_k], delete, abort = callback(v, from_k)
|
|
if delete then
|
|
delete = from_k
|
|
end
|
|
if abort then
|
|
break
|
|
end
|
|
else
|
|
return from_k, result, true
|
|
end
|
|
end
|
|
|
|
if delete then
|
|
tbl[delete] = nil
|
|
from_k = prev
|
|
elseif abort then
|
|
from_k = prev
|
|
end
|
|
return from_k, result, false
|
|
end
|
|
|
|
-- TODO: Remove array_insert, create separate array_filter function
|
|
|
|
--- Create a filtered version of a table based on the results of a filter function.
|
|
---
|
|
--- Calls `filter(value, key)` on each element in the table, returning a new table with only pairs for which
|
|
--- `filter` returned a truthy value.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local tbl = {1, 2, 3, 4, 5, 6}
|
|
--- local just_evens = table.filter(tbl, function(v) return v % 2 == 0 end) -- {[2] = 2, [4] = 4, [6] = 6}
|
|
--- local just_evens_arr = table.filter(tbl, function(v) return v % 2 == 0 end, true) -- {2, 4, 6}
|
|
--- ```
|
|
--- @generic K, V
|
|
--- @param tbl table<K, V>
|
|
--- @param filter fun(value: V, key: K): boolean
|
|
--- @param array_insert boolean? If true, the result will be constructed as an array of values that matched the filter. Key references will be lost.
|
|
--- @return table<K, V>
|
|
function flib_table.filter(tbl, filter, array_insert)
|
|
local output = {}
|
|
local i = 0
|
|
for k, v in pairs(tbl) do
|
|
if filter(v, k) then
|
|
if array_insert then
|
|
i = i + 1
|
|
output[i] = v
|
|
else
|
|
output[k] = v
|
|
end
|
|
end
|
|
end
|
|
return output
|
|
end
|
|
|
|
--- Retrieve the value at the key, or insert the default value.
|
|
--- @generic K, V
|
|
--- @param table table<K, V>
|
|
--- @param key K
|
|
--- @param default_value V
|
|
--- @return V
|
|
function flib_table.get_or_insert(table, key, default_value)
|
|
local value = table[key]
|
|
if not value then
|
|
table[key] = default_value
|
|
return default_value
|
|
end
|
|
return value
|
|
end
|
|
|
|
--- Invert the given table such that `[value] = key`, returning a new table.
|
|
---
|
|
--- Non-unique values are overwritten based on the ordering from `pairs()`.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local tbl = {"foo", "bar", "baz", set = "baz"}
|
|
--- local inverted = table.invert(tbl) -- {foo = 1, bar = 2, baz = "set"}
|
|
--- ```
|
|
--- @generic K, V
|
|
--- @param tbl table<K, V>
|
|
--- @return table<V, K>
|
|
function flib_table.invert(tbl)
|
|
local inverted = {}
|
|
for k, v in pairs(tbl) do
|
|
inverted[v] = k
|
|
end
|
|
return inverted
|
|
end
|
|
|
|
--- Create a transformed table using the output of a mapper function.
|
|
---
|
|
--- Calls `mapper(value, key)` on each element in the table, using the return as the new value for the key.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local tbl = {1, 2, 3, 4, 5}
|
|
--- local tbl_times_ten = table.map(tbl, function(v) return v * 10 end) -- {10, 20, 30, 40, 50}
|
|
--- ```
|
|
--- @generic K, V, N
|
|
--- @param tbl table<K, V>
|
|
--- @param mapper fun(value: V, key: V):N?
|
|
--- @return table<K, N>
|
|
function flib_table.map(tbl, mapper)
|
|
local output = {}
|
|
for k, v in pairs(tbl) do
|
|
output[k] = mapper(v, k)
|
|
end
|
|
return output
|
|
end
|
|
|
|
local function default_comp(a, b)
|
|
return a < b
|
|
end
|
|
|
|
--- Partially sort an array.
|
|
---
|
|
--- This function utilizes [insertion sort](https://en.wikipedia.org/wiki/Insertion_sort), which is _extremely_ inefficient with large data sets. However, you can spread the sorting over multiple ticks, reducing the performance impact. Only use this function if `table.sort` is too slow.
|
|
--- @generic V
|
|
--- @param arr Array<V>
|
|
--- @param from_index number? The index to start iteration at (inclusive). Pass `nil` or a number less than `2` to begin at the start of the array.
|
|
--- @param iterations number The number of iterations to perform. Higher is more performance-heavy. This number should be adjusted based on the performance impact of the custom `comp` function (if any) and the size of the array.
|
|
--- @param comp fun(a: V, b: V) A comparison function for sorting. Must return truthy if `a < b`.
|
|
--- @return number? next_index The index to start the next iteration at, or `nil` if the end was reached.
|
|
function flib_table.partial_sort(arr, from_index, iterations, comp)
|
|
comp = comp or default_comp
|
|
local start_index = (from_index and from_index > 2) and from_index or 2
|
|
local end_index = start_index + (iterations - 1)
|
|
|
|
for j = start_index, end_index do
|
|
local key = arr[j]
|
|
if not key then
|
|
return nil
|
|
end
|
|
local i = j - 1
|
|
|
|
while i > 0 and comp(key, arr[i]) do
|
|
arr[i + 1] = arr[i]
|
|
i = i - 1
|
|
end
|
|
|
|
arr[i + 1] = key
|
|
end
|
|
|
|
return end_index + 1
|
|
end
|
|
|
|
--- "Reduce" a table's values into a single output value, using the results of a reducer function.
|
|
---
|
|
--- Calls `reducer(accumulator, value, key)` on each element in the table, returning a single accumulated output value.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local tbl = {10, 20, 30, 40, 50}
|
|
--- local sum = table.reduce(tbl, function(acc, v) return acc + v end)
|
|
--- local sum_minus_ten = table.reduce(tbl, function(acc, v) return acc + v end, -10)
|
|
--- ```
|
|
--- @generic K, V, R
|
|
--- @param tbl table<K, V>
|
|
--- @param reducer fun(acc: R, value: V, key: K):R
|
|
--- @param initial_value R? The initial value for the accumulator. If not provided or is falsy, the first value in the table will be used as the initial `accumulator` value and skipped as `key`. Calling `reduce()` on an empty table without an `initial_value` will cause a crash.
|
|
--- @return R
|
|
function flib_table.reduce(tbl, reducer, initial_value)
|
|
local accumulator = initial_value
|
|
for key, value in pairs(tbl) do
|
|
if accumulator then
|
|
accumulator = reducer(accumulator, value, key)
|
|
else
|
|
accumulator = value
|
|
end
|
|
end
|
|
return accumulator
|
|
end
|
|
|
|
--- @deprecated use `table.remove`.
|
|
flib_table.retrieve = flib_table.remove
|
|
|
|
--- Shallowly copy the contents of a table into a new table.
|
|
---
|
|
--- The parent table will have a new table reference, but any subtables within it will still have the same table
|
|
--- reference.
|
|
---
|
|
--- Does not copy metatables.
|
|
--- @generic T
|
|
--- @param tbl T
|
|
--- @param use_rawset boolean? Use rawset to set the values (ignores metamethods).
|
|
--- @return T The copied table.
|
|
function flib_table.shallow_copy(tbl, use_rawset)
|
|
local output = {}
|
|
for k, v in pairs(tbl) do
|
|
if use_rawset then
|
|
rawset(output, k, v)
|
|
else
|
|
output[k] = v
|
|
end
|
|
end
|
|
return output
|
|
end
|
|
|
|
--- Shallowly merge two or more tables.
|
|
--- Unlike `table.deep_merge`, this will only combine the top level of the tables.
|
|
--- @param tables table[]
|
|
--- @return table
|
|
function flib_table.shallow_merge(tables)
|
|
local output = {}
|
|
for _, tbl in pairs(tables) do
|
|
for key, value in pairs(tbl) do
|
|
output[key] = value
|
|
end
|
|
end
|
|
return output
|
|
end
|
|
|
|
--- Retrieve the size of a table.
|
|
---
|
|
--- Uses Factorio's built-in `table_size` function.
|
|
--- @type fun(tbl: table):number
|
|
flib_table.size = _ENV.table_size
|
|
|
|
--- Retrieve a shallow copy of a portion of an array, selected from `start` to `end` inclusive.
|
|
---
|
|
--- The original array **will not** be modified.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local arr = {10, 20, 30, 40, 50, 60, 70, 80, 90}
|
|
--- local sliced = table.slice(arr, 3, 7) -- {30, 40, 50, 60, 70}
|
|
--- log(serpent.line(arr)) -- {10, 20, 30, 40, 50, 60, 70, 80, 90} (unchanged)
|
|
--- ```
|
|
--- @generic V
|
|
--- @param arr Array<V>
|
|
--- @param start number? default: `1`
|
|
--- @param stop number? Stop at this index. If zero or negative, will stop `n` items from the end of the array (default: `#arr`).
|
|
--- @return Array<V> A new array with the copied values.
|
|
function flib_table.slice(arr, start, stop)
|
|
local output = {}
|
|
local n = #arr
|
|
|
|
start = start or 1
|
|
stop = stop or n
|
|
stop = stop <= 0 and (n + stop) or stop
|
|
|
|
if start < 1 or start > n then
|
|
return {}
|
|
end
|
|
|
|
local k = 1
|
|
for i = start, stop do
|
|
output[k] = arr[i]
|
|
k = k + 1
|
|
end
|
|
return output
|
|
end
|
|
|
|
--- Extract a portion of an array, selected from `start` to `end` inclusive.
|
|
--
|
|
--- The original array **will** be modified.
|
|
---
|
|
--- ### Examples
|
|
---
|
|
--- ```lua
|
|
--- local arr = {10, 20, 30, 40, 50, 60, 70, 80, 90}
|
|
--- local spliced = table.splice(arr, 3, 7) -- {30, 40, 50, 60, 70}
|
|
--- log(serpent.line(arr)) -- {10, 20, 80, 90} (values were removed)
|
|
--- ```
|
|
--- @generic V
|
|
--- @param arr Array<V>
|
|
--- @param start number default: `1`
|
|
--- @param stop number? Stop at this index. If zero or negative, will stop `n` items from the end of the array (default: `#arr`).
|
|
--- @return Array<V> A new array with the extracted values.
|
|
function flib_table.splice(arr, start, stop)
|
|
local output = {}
|
|
local n = #arr
|
|
|
|
start = start or 1
|
|
stop = stop or n
|
|
stop = stop <= 0 and (n + stop) or stop
|
|
|
|
if start < 1 or start > n then
|
|
return {}
|
|
end
|
|
|
|
local k = 1
|
|
for _ = start, stop do
|
|
output[k] = table.remove(arr, start)
|
|
k = k + 1
|
|
end
|
|
return output
|
|
end
|
|
|
|
--- @class Array<T>: { [integer]: T }
|
|
|
|
return flib_table
|