Module:Sandbox/Blippy1998/Legislature
Appearance
local p = {}
local seats
local members
local committees
local base_module_name = "Module:Sandbox/Blippy1998/Legislature"
-- ============================================================================
-- 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
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
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.getMember(rep_id)
return members[rep_id]
end
-- ============================================================================
-- BALANCE OF POWER
-- Returns: { Democratic = int, Republican = int, Vacant = int, [Other] = int }
-- or similar for other countries
-- ============================================================================
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
-- ============================================================================
-- 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
-- ============================================================================
-- 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
-- ============================================================================
-- 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 ALL COMMITTEES AND SUBCOMMITTEES (NESTED)
-- Returns: {[committeeName] = {[subcommitteeName] = true}}
-- ============================================================================
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
-- ============================================================================
-- 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,
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
-- ============================================================================
-- LIST A COMMITTEE'S MAJORITY AND MINORITY MEMBERS
-- Returns: { majority = {{name=str, dist=str, rank=int}}, minority = {{...}} }
-- ============================================================================
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)
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
-- 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: check party/bloc of each member by looking up each member if this becomes desired
-- TODO: add flag for nonvoting
function p.committeeToTallies(committee_name)
local targetCommittee = committee_name
local membersList = p.committeeToMembers(targetCommittee)
-- 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.committeeTally(frame)
local args = p.getArgs(frame)
-- local country_code = p.initEnvironment(args)
p.initEnvironment(args)
local targetCommittee = args.committee or args[1]
local bloc = args.bloc or args.party or nil
local tallies = p.committeeToTallies(targetCommittee)
if bloc ~= nil then
return tallies[bloc]
end
return (function(t) local sum = 0 for _, value in pairs(t) do sum = sum + value end return sum end)(tallies)
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]
-- if not targetCaucus then return 0 end
local doCountVotingRepsOnly = (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