Module:Legislature
Appearance
local p = {}
local seats
local members
local committees
local base_module_name = "Module:Legislature"
-- ============================================================================
-- GENERAL HELPERS
-- ============================================================================
function p.initEnvironment(args)
local path_parts = {base_module_name}
-- Iterate through all positional arguments (args[1], args[2], etc.)
-- e.g., "US", "TX", "lower"
local i = 1
while args[i] do
table.insert(path_parts, args[i])
i = i + 1
end
-- Joins them into "Module:Legislature/US/TX/lower"
local base_path = table.concat(path_parts, "/")
-- Load the data modules dynamically
seats = mw.loadData(base_path .. "/seats")
local getSeats = require(base_module_name .. "/" .. args[1]).flattenDistricts -- TODO: generalize this naming
if getSeats ~= nil then
seats = getSeats(seats)
end
members = mw.loadData(base_path .. "/members")
committees = mw.loadData(base_path .. "/committees")
return args[1] -- Returns the top-level country code (e.g., "US", "BR")
end
-- Helper to safely extract arguments from a template frame or direct table call
function p.getArgs(frame)
if type(frame) == 'table' and type(frame.args) == 'table' then
return frame.args
elseif type(frame) == 'table' then
return frame
else
return { [1] = frame } -- Wraps a plain string into an args-like table
end
end
function p.qidToWikilink(qid)
-- Gets the actual English Wikipedia article title (e.g., "Jim Jordan (American politician)")
local article = mw.wikibase.sitelink(qid)
-- Gets the clean Wikidata label (e.g., "Jim Jordan")
local label = mw.wikibase.label(qid)
-- if article then
if label and label ~= article then
-- Returns: [[Jim Jordan (American politician)|Jim Jordan]]
return "[[" .. article .. "|" .. label .. "]]"
else
-- Returns: [[Jim Jordan]]
return "[[" .. article .. "]]"
end
-- else
-- -- Fallback if they don't have an English Wikipedia article yet
-- return label or qid
-- end
end
-- ============================================================================
-- NAME TO ID
-- ============================================================================
function p.nameToID(targetName, fieldName, dictionaryToSearch)
targetName = targetName:lower()
local matches = {}
for id, data in pairs(dictionaryToSearch) do
if data[fieldName]:lower():find(targetName, 1, true) then
table.insert(matches, id)
end
end
if #matches > 1 then
error("name matched more than one id")
end
if #matches == 0 then
return nil
end
return matches[1]
end
function p.repNameToID(targetName)
return p.nameToID(targetName, "rep_name", members)
end
function p.committeeNameToID(targetName)
return p.nameToID(targetName, "name", committees.metadata)
end
function p.committeeSubcommitteeNamePairToID(committee_name, subcommittee_name)
local parent_id = p.committeeNameToID(committee_name)
local subcommittee_ids = p.committeeIDToSubcommittees(parent_id)
for _, sid in ipairs(subcommittee_ids) do
if committees.metadata[sid].name == subcommittee_name then
return sid
end
end
assert(false, "This should not be nil")
end
function p.frameToCommitteeID(frame)
local args = p.getArgs(frame)
-- local country_code = p.initEnvironment(args)
p.initEnvironment(args)
local committee_name = mw.text.trim(args.committee) -- or args[1]
local subcommittee_name = args.subcommittee -- or args[2]
if subcommittee_name == nil then
return p.committeeNameToID(committee_name)
end
subcommittee_name = mw.text.trim(subcommittee_name)
return p.committeeSubcommitteeNamePairToID(committee_name, subcommittee_name)
end
-- ============================================================================
-- SEATS
-- ============================================================================
-- DISTRICT TO REPRESENTATIVE (O(1) Direct Lookup)
-- Returns: The raw member data dictionary for that specific seat
-- Core: O(1) lookup returning only the QID
function p.seatToRepID(seat_id)
-- if not state or not dist then return nil end
-- if not seat_data then return nil end
return seats[seat_id].rep_id
end
-- Wrapper: Returns the full biographical data dictionary
function p.seatToRep(seat_id)
local rep_id = p.seatToRepID(seat_id)
-- if not rep_id or rep_id == "VACANT" then return nil end
if rep_id == "VACANT" then return nil end
return members[rep_id]
end
-- BALANCE OF POWER
-- Returns: { Democratic = int, Republican = int, Vacant = int, [Other] = int }
-- or similar for other countries
-- TODO: i don't think this works anymore
function p.balanceOfPower()
local counts = { Vacant = 0 }
for _, data in pairs(seats) do
if data.nonvoting == false then
if data.rep_id == "VACANT" then
counts.Vacant = counts.Vacant + 1
else
local rep = members[data.rep_id]
if rep then
-- The universal fallback: uses caucus if it exists, otherwise legal party -- TODO: of dubious utility
local bloc = rep.party_caucus or rep.party or "Other"
counts[bloc] = (counts[bloc] or 0) + 1
end
end
end
end
return counts
end
-- ============================================================================
-- MEMBERS
-- ============================================================================
-- ====================== HELPERS ======================
function p.getMember(rep_id)
return members[rep_id]
end
-- REPRESENTATIVE TO DISTRICT
-- Returns: Array of matching districts (e.g., {{"LA", 4}} in the case of the US)
-- in case one rep occupies several districts for some reason
-- Core: Takes a precise QID and returns matching district strings
function p.repIDToSeatID(rep_id)
-- if not targetId then return {} end
local matches = {}
for seat_id, data in pairs(seats) do
if data.rep_id == rep_id then
table.insert(matches, seat_id)
end
end
if #matches > 1 then
error("rep id matched more than one seat")
end
if #matches == 0 then
return nil
end
return matches[1]
end
-- Wrapper: Accepts a name, resolves it to a QID, and executes the core search
function p.repToSeatID(rep_name)
local rep_id = p.repNameToID(rep_name)
-- if rep_id == nil then return {} end
return p.repIDToSeatID(rep_id)
end
-- ====================== MEMBERSHIPS ======================
-- LIST A REP'S COMMITTEE ASSIGNMENTS
-- Returns: The member's specific committees dictionary containing ranks/subs
-- Core: Enumerate committees by QID
function p.repIDToCommittees(rep_id)
-- if not targetId then return nil end
local out = {}
for code, roster in pairs(committees.rosters) do
for _, member_info in ipairs(roster) do
if member_info.rep_id == rep_id then
out[code] = member_info
break
end
end
end
return out
end
-- Wrapper: Enumerate committees by Name
function p.repToCommittees(rep_name)
local rep_id = p.repNameToID(rep_name)
return p.repIDToCommittees(rep_id)
end
-- LIST A REP'S CAUCUS MEMBERSHIPS
-- Returns: { party_caucus = str, other_caucuses = {[caucusName] = {leadership = str/nil}} }
-- Core: Enumerate caucuses by QID
function p.repIDToCaucuses(rep_id)
local targetId = rep_id
-- if not targetId or not members[targetId] then return nil end
local data = members[targetId]
return {
bloc = data.party_caucus or data.party,
party = data.party,
other_caucuses = data.other_caucuses or {}
}
end
-- Wrapper: Enumerate caucuses by Name
function p.repToCaucuses(targetName)
local rep_id = p.repNameToID(targetName)
return p.repIDToCaucuses(rep_id)
end
-- ============================================================================
-- COMMITTEES
-- ============================================================================
-- ====================== HELPERS/LISTING ======================
function p.getCommitteeMetadata(id)
return committees.metadata[id]
end
function p.committeeToMetadata(frame)
local id = p.frameToCommitteeID(frame)
return p.getCommitteeMetadata(id)
end
function p.subcommitteeIDToParentCommitteeID(code)
-- -- if not code or #code < 4 then return nil end
-- local parent_code = string.sub(code, 1, 2) .. "00"
-- -- If it's already the parent committee, return nil
-- if parent_code == code then return nil end
-- if committees.metadata[parent_code] == nil then
-- for c, data in pairs(committees.metadata) do
-- if data["subcommittees"] ~= nil then
-- for _, subcommittee_id in ipairs(data["subcommittees"]) do
-- if code == subcommittee_id then
-- return c
-- end
-- end
-- end
-- end
-- else
-- return parent_code
-- end
-- return nil
return p.getCommitteeMetadata(code).parent_id
end
-- LIST ALL COMMITTEES AND SUBCOMMITTEES (NESTED)
-- Returns: {[committeeName] = {[subcommitteeName] = true}}
function p.committeeIDToSubcommittees(id)
-- assert(code:sub(-2) == "00")
local metadata = committees.metadata[id]
-- assert(metadata.subcommittees ~= nil)
return metadata.subcommittees
end
function p.committeeToSubcommittees(frame)
local id = p.frameToCommitteeID(frame)
return p.committeeIDToSubcommittees(id)
end
function p.listAllCommittees() -- TODO: this should be broken into 2 functions
local tree = {}
for code, meta in pairs(committees.metadata) do
-- Identifies parent committees by the "00" suffix in the schema
if code:sub(-2) == "00" then
tree[meta.name] = {}
if meta.subcommittees then
for _, sub_code in ipairs(meta.subcommittees) do
local sub_meta = committees.metadata[sub_code]
if sub_meta then
tree[meta.name][sub_meta.name] = true
end
end
end
end
end
return tree
end
-- ====================== PRINCIPAL MEMBERS ======================
function p.committeeIDToChairID(id)
local roster = p.committeeIDToRoster(id)
for _, member in ipairs(roster) do
if member.leadership == "Chair" or member.leadership == "Chairman" or member.leadership == "Chairwoman" then
return member.rep_id
end
end
for _, member in ipairs(roster) do -- fallback - shouldn't be necessary but oh well
if member.party_status == "majority" and member.rank == 1 then
return member.rep_id
end
end
end
function p.committeeToChairID(frame)
local id = p.frameToCommitteeID(frame)
return p.committeeIDToChairID(id)
end
function p.committeeIDToRankingMemberID(id)
local roster = p.committeeIDToRoster(id)
for _, member in ipairs(roster) do
if member.party_status == "minority" and member.rank == 1 then
return member.rep_id
end
end
end
function p.committeeToRankingMemberID(frame)
local id = p.frameToCommitteeID(frame)
return p.committeeIDToRankingMemberID(id)
end
-- ====================== ROSTERS ======================
function p.committeeIDToRoster(committeeID)
return committees.rosters[committeeID]
end
-- function p.committeeToRoster(committee_name)
function p.committeeToMembers(committee_name)
return p.committeeIDToRoster(p.committeeNameToID(committee_name))
end
-- ultimate formatting function that outputs a wikitable (it wraps the specific function thence)
-- LIST A COMMITTEE'S MAJORITY AND MINORITY MEMBERS
-- Returns: { majority = {{name=str, dist=str, rank=int}}, minority = {{...}} }
function p.committeeRoster(frame)
local args = p.getArgs(frame)
-- 1. Build paths and load data universally
local country_code = p.initEnvironment(args)
-- TODO: have a standard one where it just lists them if such a function doesn't exist
-- 2. Dynamically load the country's specific formatter (e.g., Module:Legislature/US)
local country_formatter = require(base_module_name .. "/" .. country_code).formatCommitteeRosterTable
-- 3. Pass control to the country's custom formatting function
return country_formatter(frame, p)
end
-- ====================== SEAT ALLOCATIONS ======================
-- TODO: possibly US-specific and to be moved to the US module if so
function p.committeeIDToSeatAllocations(id)
local metadata = p.getCommitteeMetadata(id)
return {
["majority"] = metadata.majority_allocation,
["minority"] = metadata.minority_allocation
}
end
function p.committeeSeatAllocations(frame)
local id = p.frameToCommitteeID(frame)
return p.committeeIDToSeatAllocations(id)
end
function p.committeeSeatAllocation(frame)
local tallies = p.committeeSeatAllocations(frame)
local args = p.getArgs(frame)
local bloc = args.bloc or args.party or args.party_status or nil
if bloc ~= nil then
return tallies[mw.text.trim(bloc)]
end
return (function(t) local sum = 0 for _, value in pairs(t) do sum = sum + value end return sum end)(tallies)
end
-- ====================== NESTED TALLIES ======================
function p.committeeIDToBlocAndPartyTallies(id)
local membersList = p.committeeIDToRoster(id)
-- local count_majority = 0
-- local count_minority = 0
local counts = {}
for _, member in ipairs(membersList) do
local party_status = member.party_status
local party = p.getMember(member.rep_id).party
if counts[party_status] == nil then
counts[party_status] = {}
end
if counts[party_status][party] == nil then
counts[party_status][party] = 1
else
counts[party_status][party] = counts[party_status][party] + 1
end
-- count = count + 1
end
return counts -- + senateOffset
end
function p.committeeToBlocAndPartyTallies(frame)
local id = p.frameToCommitteeID(frame)
return p.committeeIDToBlocAndPartyTallies(id)
end
-- ====================== FLAT TALLIES ======================
-- TODO: use country-specific functions to flatten and count this in a general way if necessary, but i think it shouldn't be necessary
-- TODO: add flag for nonvoting
function p.committeeIDToTallies(id)
local membersList = p.committeeIDToRoster(id)
-- local count_majority = 0
-- local count_minority = 0
local counts = {}
for _, member in ipairs(membersList) do
if counts[member.party_status] == nil then
counts[member.party_status] = 1
else
counts[member.party_status] = counts[member.party_status] + 1
end
-- count = count + 1
end
return counts -- + senateOffset
end
function p.committeeToTallies(frame)
local id = p.frameToCommitteeID(frame)
return p.committeeIDToTallies(id)
end
-- function p.committeeNameToTallies(committee_name)
-- -- local targetCommittee = committee_name
-- -- local membersList = p.committeeToMembers(targetCommittee)
-- local id = p.committeeNameToID(committee_name)
-- return p.committeeIDToTallies(id)
-- end
function p.committeeTally(frame)
local args = p.getArgs(frame)
-- -- local country_code = p.initEnvironment(args)
-- p.initEnvironment(args)
-- local targetCommittee = args.committee or args[1]
-- local id = p.frameToCommitteeID(frame)
local bloc = args.bloc or args.party or nil
-- local tallies = p.committeeToTallies(targetCommittee)
local tallies = p.committeeToTallies(frame)
if bloc ~= nil then
return tallies[mw.text.trim(bloc)]
end
return (function(t) local sum = 0 for _, value in pairs(t) do sum = sum + value end return sum end)(tallies)
end
-- ============================================================================
-- CAUCUSES
-- ============================================================================
-- LIST ALL CAUCUSES
-- Returns: Array of unique caucus name strings
function p.listAllCaucuses()
local unique = {}
for _, data in pairs(members) do
if data.other_caucuses then
for cName in pairs(data.other_caucuses) do
unique[cName] = true
end
end
end
local out = {}
for k in pairs(unique) do table.insert(out, k) end
table.sort(out)
return out
end
-- LIST A CAUCUS'S MEMBERS
-- function p.caucusIDToMembers(caucus_id)
-- pass
-- end
function p.caucusToMembers(caucus_name)
local list = {}
for rep_id, rep in pairs(members) do
local isMember = false
local leadership = nil
if rep.party_caucus == caucus_name then
isMember = true
elseif rep.other_caucuses ~= nil and rep.other_caucuses[caucus_name] ~= nil then
isMember = true
leadership = rep.other_caucuses[caucus_name].leadership
end
if isMember then
local to_add = {[rep_id] = {}}
if leadership ~= nil then
to_add[rep_id]["leadership"] = leadership
end
table.insert(list, to_add)
end
end
return list
end
-- Function: Get Caucus Tally (Returns integer only)
function p.caucusTally(frame)
local args = p.getArgs(frame)
p.initEnvironment(args)
local targetCaucus = args.caucus or args[1]
targetCaucus = mw.text.trim(targetCaucus)
-- if not targetCaucus then return 0 end
local doCountVotingRepsOnly = (args.voting_only ~= nil and mw.text.trim(args.voting_only) == "yes")
-- local senateOffset = tonumber(args.senate) or 0
local membersList = p.caucusToMembers(targetCaucus)
local count = 0
for _, member in ipairs(membersList) do
if not (doCountVotingRepsOnly and member.nonvoting) then
count = count + 1
end
end
return count -- + senateOffset
end
-- -- ==========================================
-- -- Function: Get Caucus Members
-- -- ==========================================
-- function p.getCaucusMembers(frame)
-- local args = getArgs(frame)
-- local targetCaucus = args.caucus or args[1]
-- if not targetCaucus then
-- return "Error: No caucus specified."
-- end
-- local membersList = {}
-- local count = 0
-- for state, dists in pairs(seats) do
-- for distId, data in pairs(dists) do
-- if data.rep_id ~= "VACANT" then
-- local rep = members[data.rep_id]
-- if rep and rep.other_caucuses and rep.other_caucuses[targetCaucus] then
-- local listEntry = rep.rep_name .. " (" .. state .. "-" .. tostring(distId) .. ")"
-- table.insert(membersList, listEntry)
-- count = count + 1
-- end
-- end
-- end
-- end
-- table.sort(membersList)
-- local header = "'''" .. targetCaucus .. " (" .. count .. " members)'''\n"
-- if count == 0 then
-- return header .. "* None"
-- end
-- return header .. "* " .. table.concat(membersList, "\n* ")
-- end
-- -- ==========================================
-- -- Function: Get Committee/Subcommittee Roster & Tally
-- -- ==========================================
-- function p.getCommitteeRoster(frame)
-- local args = getArgs(frame)
-- local committee = args.committee or args[1]
-- local subcommittee = args.subcommittee
-- if not committee then return "Error: Committee name required." end
-- local targetCode
-- for code, meta in pairs(committees.metadata) do
-- if subcommittee then
-- if meta.name == subcommittee then
-- local parentCode = code:sub(1, 2) .. "00"
-- if committees.metadata[parentCode] and committees.metadata[parentCode].name == committee then
-- targetCode = code
-- break
-- end
-- end
-- else
-- if meta.name == committee and code:sub(-2) == "00" then
-- targetCode = code
-- break
-- end
-- end
-- end
-- if not targetCode then return "Error: Committee or Subcommittee not found." end
-- local repToDist = {}
-- for state, dists in pairs(seats) do
-- for distId, data in pairs(dists) do
-- if data.rep_id ~= "VACANT" then
-- repToDist[data.rep_id] = state .. "-" .. tostring(distId)
-- end
-- end
-- end
-- local roster = {}
-- local tallies = { Democratic = 0, Republican = 0, Vacant = 0 }
-- for _, mem in ipairs(committees.rosters[targetCode] or {}) do
-- if mem.rep_id == "VACANT" then
-- tallies.Vacant = tallies.Vacant + 1
-- else
-- local rep = members[mem.rep_id]
-- if rep then
-- local party = rep.party
-- tallies[party] = (tallies[party] or 0) + 1
-- local role = ""
-- if mem.rank == 1 and mem.party_status == "majority" then
-- role = " (Chair)"
-- elseif mem.rank == 1 and mem.party_status == "minority" then
-- role = " (Ranking Member)"
-- end
-- local distStr = repToDist[mem.rep_id] or "Unknown"
-- table.insert(roster, rep.rep_name .. " (" .. distStr .. " - " .. party .. ")" .. role)
-- end
-- end
-- end
-- table.sort(roster)
-- local title = committee .. (subcommittee and (" — " .. subcommittee) or "")
-- local summary = string.format("'''%s''' (D: %d, R: %d, Vacant: %d)\n", title, tallies.Democratic, tallies.Republican, tallies.Vacant)
-- return summary .. "* " .. table.concat(roster, "\n* ")
-- end
-- -- ==========================================
-- -- Function: Get Representative of a District (O(1) Direct Lookup)
-- -- ==========================================
-- function p.getDistrictRep(frame)
-- local args = getArgs(frame)
-- local targetDistrict = args.district or args[1]
-- if not targetDistrict then return "Error: District required (e.g., CA-12)." end
-- local state, dist = targetDistrict:match("^(%u%u)%-(.+)$")
-- if not state or not dist then return "Error: Invalid district format." end
-- local dKey = tonumber(dist) or dist
-- local seatData = seats[state] and seats[state][dKey]
-- if not seatData then return "Error: District not found." end
-- if seatData.rep_id == "VACANT" then
-- return "The seat for " .. targetDistrict .. " is currently vacant."
-- end
-- local rep = members[seatData.rep_id]
-- if not rep then return "Error: Member data missing." end
-- return rep.rep_name .. " (" .. rep.party .. ")"
-- end
-- ============================================================================
-- TEST SUITE
-- ============================================================================
function p.runTests()
local out = {}
local function executeTest(testName, result)
table.insert(out, "=== " .. testName .. " ===")
table.insert(out, mw.dumpObject(result))
end
executeTest("1. Balance of Power", p.balanceOfPower())
-- executeTest("2. Roster by State ('LA')", p.stateToRoster("LA"))
executeTest("3. Rep to District ('Mike Johnson')", p.repToSeatID("Mike Johnson"))
executeTest("4. District to Rep ('LA', 4)", p.seatToRep("LA-4"))
executeTest("5. Enumerate All Caucuses", p.listAllCaucuses())
executeTest("6. Enumerate All Committees", p.listAllCommittees())
executeTest("7. Rep Committees ('Maxwell Frost')", p.repToCommittees("Maxwell Frost"))
executeTest("8. Rep Caucuses ('Mike Johnson')", p.repToCaucuses("Mike Johnson"))
executeTest("9. Committee Members ('Committee on Armed Services')", p.committeeToMembers("Committee on Armed Services"))
executeTest("10. Caucus Members ('Blue Dog Coalition')", p.caucusToMembers("Blue Dog Coalition"))
executeTest("11. Caucus tally ('Blue Dog Coalition')", p.caucusTally("Blue Dog Coalition"))
return table.concat(out, "\n\n")
end
return p