![]() | This module is rated as ready for general use. It has reached a mature form and is thought to be relatively bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing. |
implements:
local p = {}
-- Load the flagicon data module used to map years to host country flags
local flagicon_data = require("Module:Medals table country/data/Olympic Games host flagicons")
function p.render(frame)
local maxRows = 50
-- Get arguments passed from the parent template
local args = frame:getParent().args
local country = args["country"] or "Country"
local show_games_flag = args["show_games_flag"] == "yes" -- Control flags
local show_dual_ranks = args["show_dual_ranks"] == "yes" -- Dual ranks (Gold medal table and Medal total table)
local total_col_bold = (args["total_col_bold"] or "yes"):lower() == "yes" -- Setting total column bold
local total_athletes = (args["total_athletes"] or "no"):lower() == "yes"
-- Determine if the table is for Summer or Winter Olympics
local season = (args["season"] or "summer"):lower()
local is_winter = (season == "winter")
local season_name = is_winter and "Winter" or "Summer"
-- Dynamically require ranking data and games held based on season
local ranking_data
if is_winter then
ranking_data = require("Module:Medals table country/data/Winter Olympics ranking")
else
ranking_data = require("Module:Medals table country/data/Summer Olympics ranking")
end
-------------------------------------------------------------------
-- Helper functions
-------------------------------------------------------------------
-- Helper: border-style for a purple border around the row (hosted games).
local function get_border_style(position)
local base = 'border-top:3px solid purple; border-bottom:3px solid purple;'
if position == "left" then
return base .. ' border-left:3px solid purple;'
elseif position == "right" then
return base .. ' border-right:3px solid purple;'
else
return base
end
end
-- Helper: bold value if it’s the max
local function bold_if_max(val, max)
return val > 0 and val == max and ("'''" .. val .. "'''") or tostring(val)
end
-- Helper: extract leading numeric value for calculations while preserving full notext for display of added notes
local function extract_number_and_text(val)
val = tostring(val or "")
local num = val:match("^%s*(%d+)")
return tonumber(num) or 0, val
end
-- Helper: split number and text to extract the number (to compute things like max athletes), but separate the number from the trailing wikitext (like references)
local function split_number_and_text(val)
val = tostring(val or ""):match("^%s*(.-)%s*$") -- trim outer spaces
local num_str = val:match("^(%d+)")
local num = tonumber(num_str) or 0
local note = val:match("^%d+%s*(.*)") or ""
return num, note
end
-- Helper: process flag icon with optional flagicon
local function format_game_with_flag(game)
local expanded_game = frame:preprocess(game)
local year = expanded_game:match("(%d%d%d%d)")
if show_games_flag and year then
local flag_name = flagicon_data[season] and flagicon_data[season][year]
if flag_name then
return string.format("{{flagicon|%s}} %s", flag_name, expanded_game)
end
end
return expanded_game
end
-- Helper to find all-time rank by country from lists in external modules
-- ("Module:Medals table country/data/Winter Olympics ranking" and "Module:Medals table country/data/Summer Olympics ranking")
local function find_rank(list, name)
for _, entry in ipairs(list) do
if entry.country == name then
return entry.rank
end
end
return nil
end
-- Helper function to check if a participation text indicates a future event
local function is_future_event(participation_text)
-- Strip common wiki formatting for comparison, removes whitespaces and sets it to lower case
-- Removes ''' and ''
local stripped_text = mw.ustring.lower(mw.text.trim(participation_text:gsub("'''", ""):gsub("''", "")))
return stripped_text == "future event"
end
-------------------------------------------------------------------
-- Fetch or derive all-time rank values
local alltime_gold_rank = args["alltime_gold_rank"]
if not alltime_gold_rank or alltime_gold_rank == "" then
local g = find_rank(ranking_data.gold_ranking, country)
alltime_gold_rank = g and tostring(g) or "–"
end
local alltime_medal_rank = args["alltime_medal_rank"]
if show_dual_ranks then
if not alltime_medal_rank or alltime_medal_rank == "" then
local t = find_rank(ranking_data.total_ranking, country)
alltime_medal_rank = t and tostring(t) or "–"
end
end
-------------------------------------------------------------------
-- Step 1: Collect raw input rows from arguments into a raw_rows list
-------------------------------------------------------------------
local raw_rows = {}
for i = 1, maxRows do
local games = args["row"..i.."_games"]
if games and games ~= "" then
if games == "note" then
local participation_text = args["row"..i.."_participation"] or ""
table.insert(raw_rows, {
is_note = true,
note_text = participation_text,
})
else
local participation = args["row"..i.."_participation"]
local is_host = args["row"..i.."_host"] == "yes"
-- Parse athletes and medals
local athletes_val, athletes_text = split_number_and_text(args["row"..i.."_athletes"])
local gold_num, gold_text = extract_number_and_text(args["row"..i.."_gold"])
local silver_num, silver_text = extract_number_and_text(args["row"..i.."_silver"])
local bronze_num, bronze_text = extract_number_and_text(args["row"..i.."_bronze"])
local rank_raw = args["row"..i.."_rank"] or ""
local medal_rank_raw = args["row"..i.."_medal_rank"] or ""
-- Add row to list
table.insert(raw_rows, {
games = games,
athletes_val = athletes_val,
athletes_text = athletes_text,
participation = participation,
gold = gold_num,
silver = silver_num,
bronze = bronze_num,
gold_display = gold_text,
silver_display = silver_text,
bronze_display = bronze_text,
rank_raw = rank_raw,
medal_rank_raw = medal_rank_raw,
is_host = is_host,
})
end
end
end
-------------------------------------------------------------------
-- Step 2: Process rows and merge participation rows with rowspan
-------------------------------------------------------------------
local rows = {}
local i = 1
-- Setting total and max paramaters --> (Step 2a: Compute max athletes, max medals and totals)
local max_athletes, max_gold, max_silver, max_bronze, max_total = 0, 0, 0, 0, 0
local total_games, total_athletes_num, total_gold, total_silver, total_bronze, total_medals = 0, 0, 0, 0, 0, 0
-- prepared initialization --> (Step 3: Olympic Games held calculation)
local current_year = tonumber(os.date("%Y"))
local games_in_progress = false
while i <= #raw_rows do
local raw_row = raw_rows[i]
-- Safely check if raw_row is nil before proceeding with its properties
if raw_row then
-- Determine the year for the current raw_row to check against current_year
local row_year = nil
if raw_row.games then
row_year = raw_row.games:match("(%d%d%d%d)")
end
-- This logic mimics the original loop's 'break' behavior by only setting the flag once.
if row_year == tostring(current_year) then
if raw_row.athletes_val and raw_row.athletes_val > 0 then
games_in_progress = true
elseif raw_row.participation then
if is_future_event(raw_row.participation) then
games_in_progress = false
else -- not is_future_event(raw_row.participation)
games_in_progress = true
end
end
end
if raw_row.is_note then
table.insert(rows, raw_row)
i = i + 1
-- Merge rows that have identical participation notes
elseif raw_row.participation and raw_row.participation ~= "" and not raw_row.is_host then
local rowspan = 1
for j = i + 1, #raw_rows do
if raw_rows[j].participation == raw_row.participation and not raw_rows[j].is_host then
rowspan = rowspan + 1
else
break
end
end
local merged_games = {}
for k = i, i + rowspan - 1 do
table.insert(merged_games, raw_rows[k].games)
end
table.insert(rows, {
participation = raw_row.participation,
rowspan = rowspan,
games_list = merged_games,
merged = true,
is_host = false,
year = merged_games[1] and merged_games[1]:match("(%d%d%d%d)"),
})
i = i + rowspan
elseif raw_row.participation and raw_row.participation ~= "" then
-- These rows should always be treated as individual participation rows, not medal rows
table.insert(rows, {
is_participation_row = true, -- flag to identify these rows
games = raw_row.games,
year = raw_row.games and raw_row.games:match("(%d%d%d%d)"),
participation = raw_row.participation,
is_host = raw_row.is_host,
merged = false, -- These are not part of a rowspan merge
})
i = i + 1
else
-- Regular medal row: build structured row with medals and rankings
local year = raw_row.games and raw_row.games:match("(%d%d%d%d)") or ""
-- Athletes number and text extracted and splited (number to calc max value; text for wiki-linking)
local athletes_num = tonumber(raw_row.athletes_val) or 0
local athletes_note = tostring(raw_row.athletes_text) or ""
-- Formatted athletes display (in Step 5: Rendering) after max athletes calc (Step 2a)
local athletes_cell = string.format("[[%s at the %s %s Olympics|%d]] %s", country, year, season_name, athletes_num, athletes_note)
-------------------------------------------------------------------
-- Step 2a: Compute max athletes, max medals and totals
-------------------------------------------------------------------
-- Medal totals calculation
local total = raw_row.gold + raw_row.silver + raw_row.bronze
total_games = total_games + 1
total_athletes_num = total_athletes_num + raw_row.athletes_val
total_gold = total_gold + raw_row.gold
total_silver = total_silver + raw_row.silver
total_bronze = total_bronze + raw_row.bronze
max_athletes = math.max(max_athletes, athletes_num)
max_gold = math.max(max_gold, raw_row.gold)
max_silver = math.max(max_silver, raw_row.silver)
max_bronze = math.max(max_bronze, raw_row.bronze)
max_total = math.max(max_total, total)
-- Final totals
total_medals = total_gold + total_silver + total_bronze
-------------------------------------------------------------------
-- Step 2b: Rank links and color set for top 3 ranks
-------------------------------------------------------------------
-- Helper: build medal table rank links
local function make_rank_link(rank_raw)
local rank_num = tonumber(rank_raw)
local medal_table_title = string.format("%s %s Olympics medal table", year, season_name)
if rank_num then
return string.format("[[%s|%d]]", medal_table_title, rank_num), rank_num
elseif rank_raw == "" then
return string.format("[[%s|–]]", medal_table_title), nil
elseif rank_raw ~= "" then
return rank_raw, nil
else
return "", nil
end
end
-- Rank links per year ( --> all-time rank links outside loop)
local gold_rank_link, gold_rank_num = make_rank_link(raw_row.rank_raw)
local medal_rank_link, medal_rank_num = make_rank_link(raw_row.medal_rank_raw)
-- Background color for top 3 ranks
local function get_rank_color(rank)
return (rank == 1 and "#F7F6A8") or (rank == 2 and "#dce5e5") or (rank == 3 and "#ffdab9") or ""
end
local bgcolor_gold_ranking = get_rank_color(gold_rank_num)
local bgcolor_medal_ranking = get_rank_color(medal_rank_num)
-------------------------------------------------------------------
-- Add full row to output list
table.insert(rows, {
games = raw_row.games,
athletes_num = athletes_num,
athletes = athletes_cell,
participation = raw_row.participation,
gold = raw_row.gold,
silver = raw_row.silver,
bronze = raw_row.bronze,
gold_display = raw_row.gold_display,
silver_display = raw_row.silver_display,
bronze_display = raw_row.bronze_display,
total = total,
total_medals = total_medals,
gold_rank = gold_rank_link,
total_rank = medal_rank_link,
bgcolor_gold_ranking = bgcolor_gold_ranking,
bgcolor_medal_ranking = bgcolor_medal_ranking,
is_host = raw_row.is_host,
merged = false,
})
i = i + 1
end
else -- raw_row is nil. Increment i to prevent an infinite loop if this happens
i = i + 1
end
end
-- All-time rank links (goldrank and alltime medal rank link with the same expression)
local function make_alltime_rank_link(rank)
return rank ~= "" and string.format("[[All-time Olympic Games medal table#Complete ranked medals (excluding precursors)|%s]]", rank) or ""
end
alltime_gold_rank = make_alltime_rank_link(alltime_gold_rank) -- all-time rank (set above from args or module)
if show_dual_ranks then
alltime_medal_rank = make_alltime_rank_link(alltime_medal_rank)
end
-------------------------------------------------------------------
-- Step 3 Number of Olympic Games held calculation
--(Check for 'games_in_progress' --> (see Step 2)) --
-------------------------------------------------------------------
-- Configuration for Olympic Games history
local OLYMPIC_CONFIG = {
SUMMER = {
start_year = 1896,
canceled_games = {1916, 1940, 1944}, -- Years of canceled Summer Olympics
split_year = 1944, -- Year of last canceled game during World War II
games_before_last_canceled_game = 10 -- Number of Summer Games held until 1944, if counted with 4-
-- if counted with 4-year interval from 1944-adjusted start.
-- 1896 (1), 1900 (2), ..., 1936 (10), 1948 (11)
},
WINTER = {
start_year = 1924, -- First Winter Olympics
canceled_games = {1940, 1944}, -- Years of canceled Summer Olympics
split_year = 1994, -- Year Winter Olympics moved to offset
games_before_split_offset = 17 -- Number of Winter Games held until 1994 inclusive,
-- if counted with 4-year interval from 1994-adjusted start.
-- 1924 (1), 1928 (2), ..., 1992 (16), 1994 (17)
}
}
local function get_games_held(games_in_progress)
if is_winter then
-- Winter Olympics logic
local years_since = current_year - OLYMPIC_CONFIG.WINTER.split_year
if years_since % 4 ~= 0 then -- Modulo for no Olympic years
return math.floor(years_since / 4) + OLYMPIC_CONFIG.WINTER.games_before_split_offset
elseif games_in_progress then
-- games in current year and in progress
return math.floor(years_since / 4) + OLYMPIC_CONFIG.games_before_split_offset
else -- games in current year, but not not in progress, hence the "-1"
return math.floor(years_since / 4) + OLYMPIC_CONFIG.games_before_split_offset -1
end
else
-- Summer Olympics logic
local years_since = current_year - OLYMPIC_CONFIG.SUMMER.split_year
if years_since % 4 ~= 0 then -- Modulo for no Olympic years
return math.floor(years_since / 4) + OLYMPIC_CONFIG.SUMMER.games_before_last_canceled_game
elseif games_in_progress then
-- games in current year and in progress
return math.floor(years_since / 4) + OLYMPIC_CONFIG.SUMMER.games_before_last_canceled_game
else -- games in current year, but not not in progress, hence the "-1"
return math.floor(years_since / 4) + OLYMPIC_CONFIG.SUMMER.games_before_last_canceled_game - 1
end
end
end
-- Check for 'games_in_progress' --> (see Step 2)
local games_held_num = get_games_held(games_in_progress)
-------------------------------------------------------------------
-- Step 4: Build the wikitable header
-------------------------------------------------------------------
local sticky_header = frame:expandTemplate{ title = "sticky-header" }
local wikitext = '{| class="wikitable sticky-header" style="text-align:center; font-size:90%;"\n'
wikitext = wikitext .. "|-\n! Games !! Athletes"
wikitext = wikitext .. ' !! style="background:gold; width:3.2em; font-weight:bold;"| Gold'
wikitext = wikitext .. ' !! style="background:silver; width:3.2em; font-weight:bold;"| Silver'
wikitext = wikitext .. ' !! style="background:#c96; width:3.2em; font-weight:bold;"| Bronze'
wikitext = wikitext .. ' !! style="width:3.1em; font-weight:bold;"| Total'
-- Column headers for rank (1 or 2 columns depending on config)
if show_dual_ranks then
wikitext = wikitext .. ' !! style="width:3em; font-weight:bold;"| [[Olympic medal table|<small>{{abbr|Gold medal|Gold medal table rank }}</small>]]'
wikitext = wikitext .. ' !! style="width:3em; font-weight:bold;"| [[Olympic medal table|<small>{{abbr|Total medal|Total medal table rank}}</small>]]\n'
else
wikitext = wikitext .. ' !! style="width:3em; font-weight:bold;"| Rank\n'
end
-------------------------------------------------------------------
-- Step 5: Render each table row
-------------------------------------------------------------------
local colspan_for_note = show_dual_ranks and 8 or 7
local colspan_for_participation = show_dual_ranks and 7 or 6
local function matchesOlympicsPattern(games)
return games and games:match("–%s") or games:match("Olympics|%d%d%d%d]]")
end
for _, row in ipairs(rows) do
if row.is_note then
wikitext = wikitext .. string.format(
"|-\n| colspan=%d style=\"text-align:center;\" | %s\n",
colspan_for_note,
row.note_text
)
elseif row.merged then
-- Participation row with rowspan (only for non-future event and non-host merges)
local align = matchesOlympicsPattern(row.games_list[1]) and "center" or "left"
wikitext = wikitext .. string.format(
"|-\n| align=%s | %s || colspan=%d rowspan=%d | %s\n",
align,
format_game_with_flag(row.games_list[1]),
colspan_for_participation,
row.rowspan,
row.participation
)
for k = 2, row.rowspan do
wikitext = wikitext .. string.format(
"|-\n| align=left | %s\n",
format_game_with_flag(row.games_list[k])
)
end
elseif row.is_participation_row then
-- Handle individual participation rows (including host future events)
wikitext = wikitext .. string.format(
"|-\n| align=left %s | %s || colspan=%d %s | %s\n",
row.is_host and ('style="' .. get_border_style("left") .. '"') or "",
format_game_with_flag(row.games),
colspan_for_participation,
row.is_host and ('style="' .. get_border_style("right") .. '"') or "",
row.participation
)
else
-- Regular medal row
--------------------------------------------
local line = "|-\n"
local game_display = format_game_with_flag(row.games)
-- Formatted athletes display after max athletes calc
local athletes_display = (row.athletes_num == max_athletes) and ("'''" .. row.athletes .. "'''") or row.athletes
if row.is_host then
line = line .. string.format('| align=left style="%s" | %s', get_border_style("left"), game_display)
line = line .. string.format(' || style="%s" | %s', get_border_style(), athletes_display)
line = line .. string.format(' || style="%s" | %s', get_border_style(), bold_if_max(row.gold, max_gold))
line = line .. string.format(' || style="%s" | %s', get_border_style(), bold_if_max(row.silver, max_silver))
line = line .. string.format(' || style="%s" | %s', get_border_style(), bold_if_max(row.bronze, max_bronze))
if total_col_bold then
line = line .. string.format(' || style="%s" | \'\'\'%s\'\'\'', get_border_style(), tostring(row.total))
else
line = line .. string.format(' || style="%s" | %s', get_border_style(), bold_if_max(row.total, max_total))
end
if show_dual_ranks then
line = line .. string.format(' || style="%s%s" | %s', row.bgcolor_gold_ranking ~= "" and 'background-color:' .. row.bgcolor_gold_ranking .. '; ' or "", get_border_style(), row.gold_rank)
line = line .. string.format(' || style="%s%s" | %s', row.bgcolor_medal_ranking ~= "" and 'background-color:' .. row.bgcolor_medal_ranking .. '; ' or "", get_border_style("right"), row.total_rank)
else
line = line .. string.format(' || style="%s%s" | %s', row.bgcolor_gold_ranking ~= "" and 'background-color:' .. row.bgcolor_gold_ranking .. '; ' or "", get_border_style("right"), row.gold_rank)
end
else
-- Regular non-hosted row
line = line .. "| align=left | " .. game_display
line = line .. " || " .. athletes_display
line = line .. " || " .. (max_gold > 0 and row.gold == max_gold and ("'''" .. row.gold_display .. "'''") or row.gold_display)
line = line .. " || " .. (max_silver > 0 and row.silver == max_silver and ("'''" .. row.silver_display .. "'''") or row.silver_display)
line = line .. " || " .. (max_bronze > 0 and row.bronze == max_bronze and ("'''" .. row.bronze_display .. "'''") or row.bronze_display)
if total_col_bold then
line = line .. " || " .. "'''" .. tostring(row.total) .. "'''"
else
line = line .. " || " .. bold_if_max(row.total, max_total)
end
if show_dual_ranks then
line = line .. (row.bgcolor_gold_ranking ~= "" and (' || style="background-color:' .. row.bgcolor_gold_ranking .. '" | ' .. row.gold_rank) or (" || " .. row.gold_rank))
line = line .. (row.bgcolor_medal_ranking ~= "" and (' || style="background-color:' .. row.bgcolor_medal_ranking .. '" | ' .. row.total_rank) or (" || " .. row.total_rank))
else
line = line .. (row.bgcolor_gold_ranking ~= "" and (' || style="background-color:' .. row.bgcolor_gold_ranking .. '" | ' .. row.gold_rank) or (" || " .. row.gold_rank))
end
end
wikitext = wikitext .. line .. "\n"
end
end
-------------------------------------------------------------------
-- Step 6: Add total row at bottom of table
-------------------------------------------------------------------
local function format_num(n) -- format numbers with thousands separator
local s = tostring(math.floor(n)) -- Ensure integer part
local formatted = s:reverse():gsub("(...)(%d)", "%1,%2"):reverse()
return formatted:gsub("^(-?),", "%1")
end
if total_athletes then
wikitext = wikitext .. "|-\n! Total".. string.format(" (%d/%s) || %s", total_games, games_held_num, format_num(total_athletes_num))
else
wikitext = wikitext .. "|-\n! colspan=2 | Total".. string.format(" (%d/%s)", total_games, games_held_num)
end
wikitext = wikitext .. string.format(" !! %s !! %d !! %d !! %s", format_num(total_gold), total_silver, total_bronze, format_num(total_medals))
if show_dual_ranks then
wikitext = wikitext .. " !! " .. alltime_gold_rank .. " !! " .. alltime_medal_rank .. "\n"
else
wikitext = wikitext .. " !! " .. alltime_gold_rank .. "\n"
end
wikitext = wikitext .. "|}"
-------------------------------------------------------------------
-- Final output
-------------------------------------------------------------------
local full_wikitext = sticky_header .. "\n" .. wikitext
return frame:preprocess(full_wikitext)
end
return p