模組:ACGaward
外观
| 此模块使用Lua语言: |
本模块用于处理維基ACG專題獎分数相关内容。请在Module:ACGaward/list登记用户得分。
用法
[编辑]本模组包括若干函数:
score- 根据页面名獲得用戶专题奖得分。亦可用|user=调取指定用户得分,或透过|mapping=从其他模组调取分数(如Module:ACGaward/list/old)。level- 獲得特定用戶的专题奖等级。特性同上。ranking- 获取用户专题奖等级列表,效果见WikiProject:ACG/維基ACG專題獎/分數排名榜。可透过|mapping=从其他模组调取分数生成名单。diff- 和Module:ACGaward/list/old搭配使用,比較兩筆記錄間的用戶分数變化情況。亦可使用|mapping1=和|mapping2=指定两个页面来对比。
備註
[编辑]以下子模组和本模組功能無關,係爲避免「浪費」而借用本模組空間:
- /nominee check — 用於
{{ACG提名/check}}。
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 Rank number
--- @alias RecordMapping table<Username, Score>
--- @alias Wikitext string
--- @class UserScore
--- @field user Username
--- @field score Score
--- @field rank Rank
--- @class GroupItem
--- @field text string
--- @field value? Rank
--- @class UserScoreDelta
--- @field user Username
--- @field delta Score
--- @field current Score
--- @class RankingGroup
--- @field kind string
--- @field lbound? Level
--- @field ubound? Level
--- @field smin? Score
--- @field smax? Score
--- @field title? string
--- @type string
local AWARD_NAME = "維基ACG專題獎"
--- @type RankingGroup[]
local RANKING_GROUPS = {
-- 40级是因为[[:Category:ACG特別貢獻]]
{kind = "user_score", lbound = 40, ubound = math.huge},
-- 保持视觉效果平衡,因此没有细分
{kind = "user_score", lbound = 10, ubound = 40},
{kind = "user_score", lbound = 5, ubound = 10},
{kind = "user_score", lbound = 4, ubound = 5},
{kind = "user_score", lbound = 3, ubound = 4},
{kind = "user_score", lbound = 2, ubound = 3},
{kind = "user_score", lbound = 1, ubound = 2},
{
kind = "score_count",
smin = 1,
smax = 10,
title = "'''未滿1級ACG專題獎'''",
},
}
--- @type string
local CURRENT_RECORD_MODULE = "Module:ACGaward/list"
--- @type string
local OLD_RECORD_MODULE = "Module:ACGaward/list/old"
--- Return true when value is nil or an empty string.
---
--- @param value any Value to check.
--- @return boolean Result of the check.
local function is_empty(value)
return value == nil or value == ""
end
--- Load a record map from the given module name.
---
--- @param module string | nil Module name, "OLD", or empty.
--- @return RecordMapping Loaded record map.
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.
---
--- @param user Username | nil Optional username.
--- @return Username Normalized username.
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 a username and its recorded score from a map.
---
--- @param mapping RecordMapping Name-to-score map.
--- @param user Username | nil Optional username to look up.
--- @return Username Username used for lookup.
--- @return Score Recorded score, or 0 if not found.
local function get_record(mapping, user)
local name = fetch_username(user)
return name, (mapping[name] or 0)
end
--- Convert an internal score to a public integer score.
---
--- @param score Score Score that may have decimals.
--- @return Score Public score.
local function calculate_public_score(score)
return math.floor(score)
end
--- Convert a score to its ACG-award level.
---
--- @param score Score Score to convert.
--- @return Level Award level.
local function calculate_level(score)
return math.floor(score / 10)
end
--- Return a user's public score from a record map.
---
--- @param mapping RecordMapping Name-to-score map.
--- @param user Username | nil Optional username to look up.
--- @return Score Public score.
local function get_score(mapping, user)
local _, score = get_record(mapping, user)
return calculate_public_score(score)
end
--- Return a user's award level from a record map.
---
--- @param mapping RecordMapping Name-to-score map.
--- @param user Username | nil Optional username to look up.
--- @return Level Award level.
local function get_level(mapping, user)
local _, score = get_record(mapping, user)
return calculate_level(score)
end
--- Expand one template with the current frame.
---
--- @param frame table Scribunto frame object.
--- @param title string Template title.
--- @param args table | nil Template arguments.
--- @return string Expanded template output.
local function expand_template(frame, title, args)
return frame:expandTemplate {
title = title,
args = args or {},
}
end
--- Return the title text for one ranking group.
---
--- @param group RankingGroup Group config.
--- @return string Group title.
local function make_group_title(group)
if not is_empty(group.title) then
return group.title
end
local lbound = group.lbound
local ubound = group.ubound
if ubound == math.huge then
return strf("'''%d+級%s'''", lbound, AWARD_NAME)
end
if ubound - lbound == 1 then
return strf("'''%d級%s'''", lbound, AWARD_NAME)
end
return strf("'''%d–%d級%s'''", lbound, ubound - 1, AWARD_NAME)
end
--- Return all ranked users with their public scores.
---
--- @param mapping RecordMapping Name-to-score map.
--- @return UserScore[] Ranked user entries.
local function collect_ranked_users(mapping)
local list = {}
for user, score in pairs(mapping) do
table.insert(list, {
user = user,
score = calculate_public_score(score),
rank = 0,
})
end
table.sort(list, function(a, b)
if a.score == b.score then
return a.user < b.user
end
return a.score > b.score
end)
local current_rank = 0
local previous_score = nil
for index, item in ipairs(list) do
if item.score ~= previous_score then
current_rank = index
previous_score = item.score
end
item.rank = current_rank
end
return list
end
--- Return users that belong to one user-score group.
---
--- @param ranked_users UserScore[] Globally ranked user entries.
--- @param group RankingGroup Group config.
--- @return UserScore[] Group entries.
local function collect_user_score_group(ranked_users, group)
local list = {}
for _, item in ipairs(ranked_users) do
local level = calculate_level(item.score)
if level >= group.lbound and level < group.ubound then
table.insert(list, item)
end
end
return list
end
--- Return score counts that belong to one score-count group.
---
--- @param mapping RecordMapping Name-to-score map.
--- @param group RankingGroup Group config.
--- @return GroupItem[] Group entries.
local function collect_score_count_group(mapping, group)
local counts = {}
local list = {}
for _, score in pairs(mapping) do
local public_score = calculate_public_score(score)
if public_score >= group.smin and public_score < group.smax then
counts[public_score] = (counts[public_score] or 0) + 1
end
end
for score = group.smax - 1, group.smin, -1 do
local count = counts[score]
if count and count > 0 then
table.insert(list, {
text = strf("%d分:%d名維基人", score, count),
})
end
end
if #list == 0 then
table.insert(list, {text = "無"})
end
return list
end
--- Return rendered list items for one ranking group.
---
--- @param mapping RecordMapping Name-to-score map.
--- @param ranked_users UserScore[] Globally ranked user entries.
--- @param group RankingGroup Group config.
--- @return GroupItem[] Rendered group entries.
local function collect_group_items(mapping, ranked_users, group)
if group.kind == "user_score" then
local users = collect_user_score_group(ranked_users, group)
local list = {}
for _, item in ipairs(users) do
table.insert(list, {
text = strf(
"[[User:%s|%s]]:%d分",
item.user,
item.user,
item.score
),
value = item.rank,
})
end
return list
end
if group.kind == "score_count" then
return collect_score_count_group(mapping, group)
end
error("Unknown ranking group kind: " .. tostring(group.kind))
end
--- Build wikicode for one ranking group.
---
--- @param frame table Scribunto frame object.
--- @param group RankingGroup Group config.
--- @param items GroupItem[] Group entries.
--- @return Wikitext? Group wikicode, or nil if empty.
local function create_group_ranking(frame, group, items)
if #items == 0 then
return nil
end
local lines = {make_group_title(group)}
table.insert(
lines,
expand_template(frame, "div col", {colwidth = "18em"})
)
if group.kind == "score_count" then
for _, item in ipairs(items) do
table.insert(lines, strf("* %s", item.text))
end
table.insert(lines, expand_template(frame, "div col end"))
return table.concat(lines, "\n")
end
table.insert(lines, "<ol>")
for _, item in ipairs(items) do
table.insert(
lines,
strf('<li value="%d">%s</li>', item.value, item.text)
)
end
table.insert(lines, "</ol>")
table.insert(lines, expand_template(frame, "div col end"))
return table.concat(lines, "\n")
end
--- Build grouped ranking wikicode for all users.
---
--- @param frame table Scribunto frame object.
--- @param mapping RecordMapping Name-to-score map.
--- @return Wikitext Full ranking wikicode.
local function build_ranking(frame, mapping)
local parts = {}
local ranked_users = collect_ranked_users(mapping)
for _, group in ipairs(RANKING_GROUPS) do
local items = collect_group_items(mapping, ranked_users, group)
local section = create_group_ranking(frame, group, items)
if section then
table.insert(parts, section)
end
end
return table.concat(parts, "\n\n")
end
--- Build a report of score changes between two maps.
---
--- @param mapping1 RecordMapping Old record map.
--- @param mapping2 RecordMapping New record map.
--- @return Wikitext Difference report wikicode.
local function build_difference(mapping1, mapping2)
local users = {}
for user, _ in pairs(mapping1) do
table.insert(users, user)
end
for user, _ in pairs(mapping2) do
if mapping1[user] == nil then
table.insert(users, user)
end
end
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
table.insert(diff_score_list, {
user = user,
delta = score_delta,
current = score2,
})
end
end
table.sort(diff_score_list, function(a, b)
if a.delta == b.delta then
return a.current > b.current
end
return a.delta > b.delta
end)
local lines = {}
local ptn = "# <code%s>* [[User:%s|%s]]:共得%d分"
.. "<small>(累計分數%d分)</small>%s</code>"
for _, item in ipairs(diff_score_list) do
local style = ""
local tag = ""
if item.delta < 0 then
style = ' style="color: red;"'
elseif item.delta == item.current then
style = ' style="color: green;"'
tag = " {{color|green|N}}"
end
table.insert(lines, strf(
ptn,
style,
item.user,
item.user,
item.delta,
item.current,
tag
))
end
return table.concat(lines, "\n")
end
local p = {}
--- Return the public score for one user.
---
--- @param args table Invoke arguments.
--- @return string Public score as text.
function p._score(args)
local mapping = load_record_module(args.module)
return tostring(get_score(mapping, args.user))
end
--- Return the award level for one user.
---
--- @param args table Invoke arguments.
--- @return string Award level as text.
function p._level(args)
local mapping = load_record_module(args.module)
return tostring(get_level(mapping, args.user))
end
--- Return grouped ranking wikicode.
---
--- @param args table Invoke arguments.
--- @param frame table Scribunto frame object.
--- @return Wikitext Ranking wikicode.
function p._ranking(args, frame)
local mapping = load_record_module(args.module)
return build_ranking(frame, mapping)
end
--- Return score differences between two record maps.
---
--- @param args table Invoke arguments.
--- @return Wikitext Difference report wikicode.
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
--- Build an invoke wrapper for a private function.
---
--- @param func_name string Private function name.
--- @return function Invoke wrapper.
local function makeInvokeFunc(func_name)
return function(frame)
local args = getArgs(frame)
return p[func_name](args, frame)
end
end
for _, func_name in ipairs(__all__) do
p[func_name] = makeInvokeFunc("_" .. func_name)
end
return p