Module:Medals table country

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

-- MODIFY wikitable: Arguments for medal column widths

local medal_header_width = args["medal_header_width"] or "4em"

local rank_header_width = args["rank_header_width"] or "3.5em"

if show_dual_ranks or show_games_flag then

medal_header_width = "3.3em"

rank_header_width = "3em"

end

-- MODIFY wikitable: Arguments to set class sortable wikitable

local wikitable_sortable = args["sortable"] or "no" == "yes" -- sortable wiktable

local sortable = ""

if wikitable_sortable then sortable = "sortable" 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("%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("%d", medal_table_title, rank_num), rank_num

elseif rank_raw == "" then

return string.format("", 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("%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' .. sortable .. ' sticky-header" style="text-align:center; font-size:90%;"\n'

wikitext = wikitext .. '

style="height:2.5em"\n! Games !! Athletes'

wikitext = wikitext .. ' !! style="background:gold; width:' .. medal_header_width .. '; font-weight:bold;"| Gold'

wikitext = wikitext .. ' !! style="background:silver; width:' .. medal_header_width .. '; font-weight:bold;"| Silver'

wikitext = wikitext .. ' !! style="background:#c96; width:' .. medal_header_width .. '; font-weight:bold;"| Bronze'

wikitext = wikitext .. ' !! style="width:' .. medal_header_width .. '; font-weight:bold;"| Total'

-- Column headers for rank (1 or 2 columns depending on config)

if show_dual_ranks then

wikitext = wikitext .. ' !! style="width:' .. rank_header_width .. '; font-weight:bold;"| Olympic medal table'

wikitext = wikitext .. ' !! style="width:' .. rank_header_width .. '; font-weight:bold;"| Olympic medal table\n'

else

wikitext = wikitext .. ' !! style="width:' .. rank_header_width .. '; 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 | %scolspan=%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 | %scolspan=%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