Jump to content

Module:Table utilities

From HopperWiki

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

--[[
Module:Table utilities
Part of the Module:Utilities family.

Generic Lua table manipulation - no Cargo dependency, no wiki output.
All functions are re-exported through Module:Utilities for backward compatibility.

Functions:
  unpack_table                Flatten a nested table into a single key/value table
  remove_dupes_from_flat_list Remove duplicates from a flat list (returns seen-set; carried forward as-is)
  filter_by_intersection      Keep only elements of table_a that appear in table_b
  in_a_not_b                  Return elements in array a that are not in array b
  table_has_value             Check if an array contains a specific value
  get_downstream_items        Return all elements after a focal item in an ordered list
  key_value_table_to_csv      Extract unique values of a field from a cargo-like table → CSV string
  deduplicate_rows            Remove duplicate rows from a cargo result based on key fields
  table_to_string             Join an array into a comma-delimited string
  merge_schemas               Merge two or more field-schema tables into one
--]]


local tbl = {}


-- ============================================================
-- Flattening / unpacking
-- ============================================================

--[[
Flattens a nested table (e.g. a raw Cargo results object) into a single
key/value table. Prints a warning on key conflicts.

  tbl.unpack_table({{a=1},{b=2}}) → {a=1, b=2}
--]]
function tbl.unpack_table(nested_table)
    local flat_table = {}
    for _, inner_table in pairs(nested_table) do
        if type(inner_table) == "table" then
            for key, value in pairs(inner_table) do
                if flat_table[key] then
                    print("Warning: Key conflict for '" .. key .. "'. Existing value will be overwritten.")
                end
                flat_table[key] = value
            end
        end
    end
    return flat_table
end


-- ============================================================
-- Deduplication
-- ============================================================

--[[
Removes duplicates from a flat list in-place.
NOTE: Carried forward as-is. Returns the `seen` set (a dict), not a clean
array - this is a known quirk of the original implementation.
--]]
function tbl.remove_dupes_from_flat_list(flat_table)
    local seen = {}
    for index, item in ipairs(flat_table) do
        if seen[item] then
            table.remove(flat_table, index)
        else
            seen[item] = true
        end
    end
    local simple_list = {}
    return seen
end


--[[
Removes duplicate rows from a Cargo results array based on one or more key fields.
Preserves first-encounter order.

  tbl.deduplicate_rows(rows, {"Name", "Family"})
--]]
function tbl.deduplicate_rows(rows, key_fields)
    if not rows or type(rows) ~= "table" then return {} end
    if not key_fields or type(key_fields) ~= "table" then
        key_fields = { "Name" }
    end
    if #rows == 0 then return {} end
    if #key_fields == 0 then return rows end

    local unique_rows = {}
    local seen = {}

    for _, row in ipairs(rows) do
        if type(row) == "table" then
            local row_key = ""
            for _, field in ipairs(key_fields) do
                local v = row[field]
                row_key = row_key .. "|" .. (v ~= nil and tostring(v) or "")
            end
            if not seen[row_key] then
                table.insert(unique_rows, row)
                seen[row_key] = true
            end
        end
    end

    return unique_rows
end


-- ============================================================
-- Set operations
-- ============================================================

-- Returns a new array containing only elements of table_a that also appear in table_b.
function tbl.filter_by_intersection(table_a, table_b)
    local filtered = {}
    for _, value in ipairs(table_a) do
        for _, b in ipairs(table_b) do
            if value == b then
                table.insert(filtered, value)
                break
            end
        end
    end
    return filtered
end


-- Returns elements that are in array a but not in array b.
function tbl.in_a_not_b(a, b)
    local b_set = {}
    for _, value in ipairs(b) do
        b_set[value] = true
    end
    local result = {}
    for _, value in ipairs(a) do
        if not b_set[value] then
            table.insert(result, value)
        end
    end
    return result
end


-- Returns true if the array tbl contains value.
function tbl.table_has_value(t, value)
    for _, v in ipairs(t) do
        if v == value then return true end
    end
    return false
end


-- ============================================================
-- Ordered list operations
-- ============================================================

--[[
Returns all elements that appear after focal_item in list.
Raises an error if focal_item is not found.

  tbl.get_downstream_items({"family","genus","species"}, "genus") → {"species"}
--]]
function tbl.get_downstream_items(list, focal_item)
    if not list or type(list) ~= "table" or #list == 0 then
        error("Error: The rank list is empty or invalid.")
    end
    if not focal_item or type(focal_item) ~= "string" then
        error("Error: The focal rank is missing or invalid.")
    end

    local start_index = nil
    for i, item in ipairs(list) do
        if item == focal_item then
            start_index = i
            break
        end
    end

    if not start_index then
        error("Error: The focal rank '" .. focal_item .. "' is not found in the rank list. Rank list: {" ..
            table.concat(list, ", ") .. "}")
    end

    local downstream = {}
    for i = start_index + 1, #list do
        table.insert(downstream, list[i])
    end
    return downstream
end


-- ============================================================
-- Cargo-like table → string conversions
-- ============================================================

-- Extracts unique values of field_name from a cargo-like array of row tables,
-- sorts them, and returns a comma-separated string.
--   format = "string" (default) - plain text
--   format = "page"             - wrap in wiki page links (italicised for species/genus)
function tbl.key_value_table_to_csv(dict, field_name, format)
    format = format or "string"

    local uniques = {}
    for i = 1, #dict do
        local entry = dict[i]
        if entry and entry[field_name] then
            uniques[entry[field_name]] = true
        else
            mw.log("Invalid entry at index " .. i .. " in dict")
        end
    end

    local sorted = {}
    for v, _ in pairs(uniques) do
        table.insert(sorted, v)
    end
    table.sort(sorted)

    local list = {}
    for i = 1, #sorted do
        local item = sorted[i]:gsub("%**$", "")
        if format == "page" then
            if string.lower(field_name) == "species" or string.lower(field_name) == "genus" then
                table.insert(list, "''" .. "[[" .. item .. "]]" .. "''")
            else
                table.insert(list, "[[" .. item .. "]]")
            end
        else
            table.insert(list, item)
        end
    end

    return #list > 0 and table.concat(list, ", ") or nil
end


-- Joins an array into a comma-delimited string.
-- Intended for simple rank lists and similar 1-D arrays.
function tbl.table_to_string(rank_table)
    return table.concat(rank_table, ", ")
end


-- ============================================================
-- Schema helpers
-- ============================================================

--[[
Merges two or more field-schema tables into one.
Later arguments take precedence on key conflicts.

  tbl.merge_schemas(schema_a, schema_b, schema_c)
--]]
function tbl.merge_schemas(...)
    local merged = {}
    for i = 1, select("#", ...) do
        local schema = select(i, ...)
        if schema and type(schema) == "table" then
            for field, config in pairs(schema) do
                merged[field] = config
            end
        end
    end
    return merged
end


return tbl
Cookies help us deliver our services. By using our services, you agree to our use of cookies.