跳转到内容

模組:ACGaward

维基百科,自由的百科全书

require("strict")

local __all__ = { "score", "level", "ranking", "diff" }

local strf = mw.ustring.format

local getArgs = require("Module:Arguments").getArgs

--- @alias Username string
--- @alias Score number
--- @alias Level number
--- @alias RecordMapping table<Username, Score>
--- @alias Wikitext string

--- @class UserScore
--- @field user Username
--- @field score Score

--- @class UserScoreDelta
--- @field user Username
--- @field delta Score
--- @field current Score

--- @type Level[]
local LEVEL_NODES = { math.huge, 40, 20, 10, 5, 4, 3, 2, 1, 0 }
--- @type string
local CURRENT_RECORD_MODULE = "Module:ACGaward/list"
--- @type string
local OLD_RECORD_MODULE = "Module:ACGaward/list/old"

--- Whether a value is empty (`nil` or "").
--- Other values, including 0, `false` and `{}`, are treated as falsy.
--- @param value any @ The value to be evaluated.
--- @return boolean @The result.
local function is_empty(value)
    if value == nil or value == "" then
        return true
    end
    return false
end

--- Load the record mapping from a module.
--- @param module string | nil @The module name, "OLD", or empty.
--- @return RecordMapping @The loaded record mapping.
local function load_record_module(module)
    if is_empty(module) then
        return mw.loadData(CURRENT_RECORD_MODULE)
    end
    if module == "OLD" then
        return mw.loadData(OLD_RECORD_MODULE)
    end
    return mw.loadData(module)
end

--- Return a normalized username or current page base name if nil.
--- @param user Username | nil @Optional username to normalize.
--- @return Username @The normalized username or current page base name.
local function fetch_username(user)
    if is_empty(user) then
        return mw.title.getCurrentTitle().baseText
    end
    local username = user  -- TODO: Implement normalization
    return username
end

--- Return name and recorded score from the given record mapping.
--- If the username is not recorded, the score will be 0.
--- @param mapping RecordMapping @Mapping of username to score.
--- @param user Username | nil @Optional username to look up.
--- @return Username, Score @Username and associated score.
local function get_record(mapping, user)
    local name = fetch_username(user)
    return name, (mapping[name] or 0)
end

--- Convert an internal score to an integer score.
--- @param score Score @The score that might have a decimal part.
--- @return Score @The score as an integer (floor value).
local function calculate_public_score(score)
    return math.floor(score)
end

--- Convert a score to its corresponding ACG-award level.
--- @param score Score @The score to convert to a level.
--- @return Level @The level as an integer.
local function calculate_level(score)
    return math.floor(score / 10)
end

--- Get a normalized score for a user from record mapping.
--- @param mapping RecordMapping @Mapping of username to score.
--- @param user Username | nil @Optional username to look up.
--- @return Score @The normalized score for the user.
local function get_score(mapping, user)
    local _, score = get_record(mapping, user)
    return calculate_public_score(score)
end

--- Get a normalized level for a user from record mapping.
--- @param mapping RecordMapping @Mapping of username to score.
--- @param user Username | nil @Optional username to look up.
--- @return Level @The level calculated from the user's score.
local function get_level(mapping, user)
    local _, score = get_record(mapping, user)
    return calculate_level(score)
end

--- Create wikicode ranking for users within level bounds.
--- @param list UserScore[] @Sequence of UserScore tuples to include.
--- @param lbound Level @The lower bound of level (included).
--- @param ubound Level @The upper bound of level (excluded).
--- @param start number @Starting number for the ordered list.
--- @return Wikitext @Formatted wikicode for the subset ranking.
local function create_subset_ranking(list, lbound, ubound, start)
    if #list == 0 then
        return
    end
    local lines = {}
    -- generate header based on level bounds
    local header
    if ubound == math.huge then
        header = tostring(lbound) .. "級或以上維基ACG獎"
    elseif ubound - lbound == 1 then
        header = tostring(lbound) .. "級維基ACG獎"
    else
        header = tostring(lbound) .. "至" .. tostring(ubound - 1) .. "級維基ACG獎"
    end
    header = "<b>" .. header .. "</b>"
    table.insert(lines, header)
    -- generate ordered list with the proper start number
    local line = strf('<ol start="%d">', start)
    table.insert(lines, line)
    for _, v in ipairs(list) do
        line = strf("<li>[[User:%s|%s]]:%d分", v.user, v.user, v.score)
        table.insert(lines, line)
    end
    table.insert(lines, "</ol>")
    -- return the result
    return table.concat(lines, "\n")
end

--- Build a complete ranking of users grouped by level in wikicode.
--- @param mapping RecordMapping @Mapping of username to score.
--- @return Wikitext @Formatted wikicode with the complete ranking.
local function build_ranking(mapping)
    local score_list = {}
    for user, score in pairs(mapping) do
        local item = { user = user, score = calculate_public_score(score) }
        table.insert(score_list, item)
    end
    local sorter = function(a, b)
        if a.score == b.score then
            return a.user < b.user
        end
        return a.score > b.score
    end
    table.sort(score_list, sorter)
    -- process users by level groups
    local subset_ranking_parts = {}
    local sub_score_list = {}
    local rank = 0
    local node_index = 1
    for i, user in ipairs(score_list) do
        local user_level = calculate_level(user.score)
        while user_level < LEVEL_NODES[node_index] do
            local subset_ranking = create_subset_ranking(
                    sub_score_list,
                    LEVEL_NODES[node_index],
                    LEVEL_NODES[node_index - 1],
                    rank
            )
            table.insert(subset_ranking_parts, subset_ranking)
            -- reset for the next level
            sub_score_list = {}
            rank = i
            node_index = node_index + 1
        end
        table.insert(sub_score_list, user)
    end
    return table.concat(subset_ranking_parts, "\n")
end

--- Build a difference report listing users with score change.
--- @param mapping1 RecordMapping @The record mapping with old data.
--- @param mapping2 RecordMapping @The record mapping with new data.
--- @return Wikitext @Formatted wikicode with the report.
local function build_difference(mapping1, mapping2)
    local users = {}
    for k, _ in pairs(mapping1) do
        table.insert(users, k)
    end
    for k, _ in pairs(mapping2) do
        if mapping1[k] == nil then
            table.insert(users, k)
        end
    end
    -- process data list
    local diff_score_list = {}
    for _, user in ipairs(users) do
        local score1 = get_score(mapping1, user)
        local score2 = get_score(mapping2, user)
        local score_delta = score2 - score1
        if score_delta ~= 0 then
            --- @type UserScoreDelta
            local item = { user = user, delta = score_delta, current = score2 }
            table.insert(diff_score_list, item)
        end
    end
    local sorter = function(a, b)
        if a.delta == b.delta then
            return a.current > b.current
        end
        return a.delta > b.delta
    end
    table.sort(diff_score_list, sorter)
    -- generate result
    local lines = {}
    local ptn = '# <code%s>* &#91;[User:%s|%s]]:共得%d分'
            .. '&#60;small>(累計分數%d分)&#60;/small>%s</code>'
    for _, v in ipairs(diff_score_list) do
        local user, delta, current = v.user, v.delta, v.current
        local style, tag = "", ""
        if delta < 0 then
            style = ' style="color: red;"'
        elseif delta == current then
            style = ' style="color: green;'
            tag = " {{color|green|N}}"
        end
        local line = strf(ptn, style, user, user, delta, current, tag)
        table.insert(lines, line)
    end
    return table.concat(lines, "\n")
end

local p = {}

function p._score(args)
    local module = args.module
    local user = args.user
    local mapping = load_record_module(module)
    return tostring(get_score(mapping, user))
end

function p._level(args)
    local module = args.module
    local user = args.user
    local mapping = load_record_module(module)
    return tostring(get_level(mapping, user))
end

function p._ranking(args)
    local module = args.module
    local mapping = load_record_module(module)
    return build_ranking(mapping)
end

function p._diff(args)
    local module1 = args[1] or "OLD"
    local module2 = args[2]
    local mapping1 = load_record_module(module1)
    local mapping2 = load_record_module(module2)
    return build_difference(mapping1, mapping2)
end

local function makeInvokeFunc(func_name)
    return function(frame)
        local args = getArgs(frame)
        return p[func_name](args)
    end
end

for _, func_name in ipairs(__all__) do
    p[func_name] = makeInvokeFunc("_" .. func_name)
end

return p