local p = {}
--[[
Процесс работы:
1) Устанавливается список кандидатов
1.1) Список кандидатов ищется на разных страницах
1.2) В случае ошибок сообщения о них передаются в таблице и потом группируются
2) Устанавливается список избирателей
2.1) нужна заранее подготовленная таблица к выборам, без неё нет автоматической проверки критериев
2.1.1) TODO - если вообще нет таблицы, пропускать всех так
2.2) принимаются команды с комментариями от бюрократов о необходимости учесть кого-то
3) Установление списка голосов
3.1) vote_listing, грабит страницы с голосами при помощи pooling
3.2) compute переписывает голоса в таблицу допущенных, добавляя в таблицу err списки исключённых
3.3) сортировка кандидатов согласно ВП:ВАК (без отсечки по 20 голосам)
3.4) формируется итоговая таблица
3.4.1) записывается заранее заданная шапка
3.4.2) на основании valid_votes функция line_format записывает строки
3.4.3) сообщения об ошибках группируются и записываются самые важные вместе с подвалом таблицы
TODO - необходимо компактно отображать ещё больше информации
3.5) выдаётся итоговый результат
Таблицы
candidatrue[candidate] - (ключ - ник кандидата, значение true/nil в зависимости от того есть ли о нём запись)
candidates[N] - (без ключа, содержит ники кандидатов)
err[N] - (без ключа, {тип ошибки текстом; группа ошибки; серьёзность ошибки: 1 - критическая, 2 - серьёзная, 3 - мелкая, 4 - не ошибка; текст ошибки})
vote_table[candidate] = votes[voter] {{nick,bool_pro,bool_contra,time}, }
exceptions[user] = {is_exception,in_general,allow,prevent,comment,candidate}
--]]
-- local mw = mw or {} -- для уменьшения количества ошибок от локального дебаггера
-- local frame -- frame из p.open_vote, чтобы не волноваться о доступе из любой функции
-- вызов функцией не переданного в неё аргумента приводит к [[Замыкание (программирование)|замыканию]], которое тут не нужно
local monthlang = {"января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря"}
local month_to_num = {["января"]=1,["февраля"]=2,["марта"]=3,["апреля"]=4,["мая"]=5,["июня"]=6,
["июля"]=7,["августа"]=8,["сентября"]=9,["октября"]=10,["ноября"]=11,["декабря"]=12,["-"]=""}
local table_start, table_end = '<table class="wikitable sortable" width=33%><tr style="text-align:center"><th class="headerSortDown">#</th><th>Кандидат</th><th>+</th><th>−</th><th>Σ</th><th>%</th></tr>', "</table>"
local err_start, err_end = '<tr class="sortbottom" style="text-align:center"><td colspan=6><table class="mw-collapsible mw-collapsed"><tr><th>Дополнительная информация </th></tr><tr><td>','</td></tr></table></td></tr>'
local err_type = {["no data"] ="Нет данных:", ["clash"] ="Расхождения:", ["vot_ers"] ="Исключения:"}
-- функция для проверки, содержит ли массив запрашиваемое значение
local function is_in_list ( var, list )
for i=1, #list do
if var == list[i] then
return true
end
end
return false
end
-- функция для подсчёта элементов в массиве
local function n_list (tab)
local i = 0
for k,v in pairs(tab) do
i = i + 1
end
return i
end
-- граббинг данных со страниц с голосами
local function pooling (content, plus, votes)
if not content or #content < 2 then return end
votes = votes or {}
for line in string.gmatch(content, "[^\n]+") do
--при моментальном переголосовании в течении минуты модуль не может определить позднейший голос
local user, nick, h, m, d, mon, y = string.match(line, "^#%s%[%[user:(.-)|(.-)%]]%s(%d%d):(%d%d),%s(.-)%s(.-)%s(.-)%s%(UTC%)")
if user then
local date = os.time{year=y, month=month_to_num[mon], day=d, hour=h, min=m}
votes[user] = votes[user] or {}
table.insert(votes[user], {
nick, -- user == voter == nick
plus, -- pro
not plus, -- contra
date}) -- date
end
end
return votes
end
-- =p.dress("Кандидат",true,false,"причина")
local function dress (candidate,pro,contra,comm)
return "<span style='font-size:90%' class='ts-comment-commentedText' title='" .. candidate .. (comm and " (" or "") .. (comm or "") .. (comm and ")" or "") .. "'>" .. (pro and "+" or (contra and "−" or "?")) .. "</span>"
end
-- TODO подсчёт результатов на заданную дату
-- отмены голосов бюрократами будут вневременные и не повлияют на динамику
local function compute (err,vote_table,electoratrue,exceptions,date)
if not vote_table then return nil end
date = date or os.time()
local valid_votes = {}
for candidate, votes in pairs(vote_table) do
valid_votes[candidate] = valid_votes[candidate] or {}
mw.log("== " .. candidate .. " ==")
for voter, vote in pairs(votes) do
err[votes] = err[votes] or {}
err[votes][candidate] = err[votes][candidate] or {}
err[votes][candidate]["change"] = #vote - 1
valid_votes[candidate][voter] = valid_votes[candidate][voter] or {}
local min_date, pro, contra = 0, false, false
-- local max_date = 17179869184
table.sort(vote,function(a,b) return a[4]>b[4] end)
pro, contra = vote[1][2], vote[1][3]
if #vote ~= 1 then
table.insert(err,{"vot_ers",voter,"<small><i>change: </i></small>",dress(candidate,false,false,#vote)})
end
--[[ for i,subvote in ipairs(vote) do
-- subvote[N] {nick,bool_pro,bool_contra,time}
if subvote[1] ~= voter then
table.insert(err,{"clash",candidate,voter,"(" .. subvote[1] .. ")"})
break
end
if #vote == 1 then
pro, contra = subvote[2], subvote[3]
elseif subvote[4] > min_date then
table.insert(err,{"vot_ers",voter,"<small><i>change: </i></small>",dress(candidate,false,false,#vote)})
pro, contra, v_date, change = subvote[2], subvote[3], subvote[4], true
end
end
--]]
if not electoratrue[voter] then
table.insert(err,{"vot_ers",voter,"<small><i>activity: </i></small>",dress(candidate,pro,contra)})
end
valid_votes[candidate][voter] = {electoratrue[voter] or false, pro, contra}
if exceptions[voter] and exceptions[voter][1] then
if exceptions[voter][2] or exceptions[voter][7] == candidate then
if exceptions[voter][3] then
-- TODO - записывать комментарий бюрократа
-- понять как можно прятать в ref или comment
valid_votes[candidate][voter][1] = true
table.insert(err,{"vot_ers",voter,"<small><i>allowed: </i></small>",dress(candidate,pro,contra,exceptions[voter][5] and exceptions[voter][6] or nil)})
elseif exceptions[voter][4] then
valid_votes[candidate][voter][1] = false
table.insert(err,{"vot_ers",voter,"<small><i>restricted: </i></small>",dress(candidate,pro,contra,exceptions[voter][5] and exceptions[voter][6] or nil)})
end
end
end
-- exceptions[voter] {is_exception,in_general,allow,prevent,bool_comment,comment,candidate}
-- err[N] {txt_type; txt_group; txt_subgroup; txt_comment}
-- valid_votes[candidate][voter]{[1]= bool_valid, [2]=bool_pro, [3]=bool_contra}
end
end
return err, valid_votes
end
local function reform (err, valid_votes)
local pre_result, pre_sort = {},{}
for candidate, votes in pairs(valid_votes) do
local count_sup,count_opp = 0,0
for voter, vote in pairs(votes) do
if vote[1] and vote[2] then
count_sup = count_sup + 1
elseif vote[1] and vote[3] then
count_opp = count_opp + 1
else
-- mw.log(candidate .. ">" .. voter .. ">("..tostring(vote[1])..","..tostring(vote[2])..","..tostring(vote[3])..")")
end
end
local count_tot = count_sup + count_opp
local percent = count_tot == 0 and 0 or (count_sup * 100 / count_tot)
pre_result[candidate] = {count_sup,count_opp,count_tot,percent}
-- сортировка по голосам за, но те кто набирает процент - выше
local sort_index
-- mw.log(candidate .. " - " .. percent .. "(" .. tostring(percent > (200/3)).. ")")
if percent > (200/3) then
sort_index = 1000000 + count_sup
else
sort_index = 100 * (100/3 + percent) + count_sup
end
-- mw.log(sort_index)
table.insert(pre_sort,{candidate, sort_index})
end
table.sort(pre_sort, function(a,b) return a[2]>b[2] end)
return err, pre_result, pre_sort
end
-- valid_votes[candidate][voter]{[1]= bool_valid, [2]=bool_pro, [3]=bool_contra}
-- line_format(i, cand[1], pre_result[cand[1]], arb_page))
local function line_format (i, candidate, c_res, arb_page)
local count_sup,count_opp,count_tot,percent = c_res[1], c_res[2], c_res[3], c_res[4]
local passing = percent > 66.6
local tr = mw.html.create( 'tr' )
tr :css( 'background', passing and '#d5fdf4;' or '#fee7e6;' )
:tag( 'td' ):css('text-align','right'):wikitext( (passing and '' or '<small>') .. i .. (passing and '' or '</small>')):done()
:tag( 'td' ):css('text-align','center'):css('white-space','nowrap'):wikitext( table.concat{"[[".. arb_page .."/", candidate, '|', candidate, "]]"}):done()
:tag( 'td' ):css('text-align','right'):wikitext( count_sup ):done()
:tag( 'td' ):css('text-align','right'):wikitext( count_opp ):done()
:tag( 'td' ):css('text-align','right'):wikitext( count_tot ):done()
:tag( 'td' ):css('text-align','right'):wikitext( (string.gsub( mw.ustring.format("%.2f %%", percent), "%.", ","))):done()
return tostring( tr )
end
local function vote_listing (err,vote_table,arb_page,candidate)
local pattern = "\n#[^#*:][^\n]+"; -- подсчёт нумерованных списков
local pagepointer_sup=mw.title.new(arb_page .. '/+/' .. candidate, '')
local pagepointer_opp=mw.title.new(arb_page .. '/-/' .. candidate, '')
local text_sup=pagepointer_sup.getContent(pagepointer_sup)
local text_opp=pagepointer_opp.getContent(pagepointer_opp)
local votes = {}
votes = pooling(text_sup, true, votes)
local pro_votes = n_list(votes)
-- mw.log(candidate .. " + " .. pro_votes)
votes = pooling(text_opp, false, votes)
local opp_votes = 1+ n_list(votes) - pro_votes
-- mw.log(candidate .. " - " .. opp_votes)
vote_table[candidate] = votes
err[votes] = err[votes] or {}
err[votes][candidate] = err[votes][candidate] or {}
err[votes][candidate]["raw_pro"] = pro_votes
err[votes][candidate]["raw_opp"] = opp_votes
return err, vote_table
end
-- для сложения таблиц кандидатов
local vlist={}
function vlist.__add (tru1,tru2)
if not tru1 or not tru2
or type(tru1) ~= "table" or type(tru2) ~= "table"
then return end
local tru_list = {}
for key,bool in pairs(tru1) do
tru_list[key] = bool
end
for key,bool in pairs(tru2) do
tru_list[key] = bool
end
return tru_list
end
local function line_processor(raw_text_candid,patt_string,patt_candid,patt_candid_end,position)
local candidates, candidatrue = {}, {}
setmetatable(candidatrue,vlist)
for line in raw_text_candid:gmatch("[^\n]+") do
if string.match( line, patt_string ) then
local candidate_text = string.match( line, patt_candid)
local pos0, pos1 = string.find(candidate_text,patt_candid_end)
local candidate = string.sub(candidate_text, position, pos0 - 1)
if candidate ~= "" and candidatrue[candidate] ~= true then
candidatrue[candidate] = true
table.insert(candidates, candidate)
end
end
end
return candidates, candidatrue
end
-- =p.caret({},mw.title.new("Википедия:Выборы арбитров/Зима 2019—2020/Голосование/^"))
-- =p.caret({},mw.title.new("Википедия:Выборы арбитров/Зима 2019—2020/Голосование/^"))
local function caret (err, page)
local raw_text = mw.title.new(page):getContent() or ""
if #raw_text < 2 then
table.insert(err,{"no data","bureaucrat panel","",'[['.. page ..'|/^]]'})
return err, {}
end
local content, exceptions = false, {}
for line in raw_text:gmatch("[^\n]+") do
if content then
-- TODO exceptions[voter] {is_exception,in_general,allow,prevent,bool_comment,comment,candidate}
-- "*" true true true false ""
-- "*" true false false true "отказался" "Candid"
local line_data = string.match( line, "%*%s(.+)")
local _,n = line_data:gsub("|","")
if n == 0 then
exceptions[mw.text.trim(line_data)] = {true,true,true,false}
elseif n == 1 then
local voter, comment = mw.ustring.match(line_data,"([^|]+)|([^|]+)")
voter = mw.text.trim(voter)
comment = mw.text.trim(comment)
exceptions[voter] = {true,true,false,true,true,comment}
elseif n == 2 then
local voter, candidate, comment = mw.ustring.match(line_data,"([^|]+)%s|%s([^|]+)|([^|]+)")
voter = mw.text.trim(voter)
candidate = mw.text.trim(candidate)
comment = mw.text.trim(comment)
exceptions[voter] = {true,false,false,true,true,comment,candidate}
else
table.insert(err,{"clash","bureaucrat panel","",line_data})
end
elseif string.match( line, ".*</noinclude>" ) then
content = true
end
end
-- mw.logObject(exceptions)
return err, exceptions
end
-- =p.exclamat(mw.title.new("Википедия:Выборы арбитров/Зима 2019—2020/Голосование/!"))
local function exclamat (page)
return line_processor(page:getContent() or "","====.+}}","====.+}}","{{",8)
end
-- =p.ampersan(mw.title.new("Википедия:Выборы арбитров/Зима 2019—2020/Голосование/&"))
local function ampersan (page)
return line_processor(page:getContent() or "","%|[^|]+%|%|%[%[.*","%|[^|]+%|%|%[%[","||%[%[",2)
end
-- =p.asterisk(mw.title.new("Википедия:Выборы арбитров/Зима 2019—2020/Голосование/*"))
local function asterisk (page)
return line_processor(page:getContent() or "","%*%s%[%[#[^|]+|[^%]]+%]%]","%*%s%[%[#[^|]+|[^%]]+%]%]","|",6)
end
-- =p.w_quarry(mw.title.new("Википедия:Выборы арбитров/Зима 2021/Избиратели"))
-- =p.w_quarry(mw.title.new("Википедия:Выборы арбитров/Лето 2020/Избиратели"))
local function w_quarry (page)
local electorate, electoratrue = line_processor(page:getContent() or "","|[^|]+||%d+||%d+||%d+","^|[^|]+||","||",2)
if not electorate[1] then
local raw_text = page:getContent()
-- TODO сообщение об ошибке
if not raw_text then return {}, {} end
for line in raw_text:gmatch("[^\n]+") do
if line ~= "" and electoratrue[line] ~= true then
electoratrue[line] = true
table.insert(electorate, line)
end
end
end
-- mw.logObject(electoratrue)
-- mw.logObject(electorate)
return electorate, electoratrue
end
-- =p.elec_listing({},"Википедия:Выборы арбитров/Лето 2020/Голосование")
-- =p.elec_listing({},"Шаблон:Результат выборов арбитров")
local function elec_listing (err, arb_page)
local page_1, page_2, _ = mw.ustring.match(arb_page,"([^/]+)/([^/]+)/([^/]+)")
local page_name
if not page_1 then
page_name = table.concat({page_1 or arb_page,"Избиратели"},"/")
else
page_name = table.concat({page_1,page_2,"Избиратели"},"/")
end
local page = mw.title.new(page_name)
local electorate, electoratrue = w_quarry (page)
if not electorate[1] then
table.insert(err,{"no data","elec","","[[" .. page_name .. "|/Избиратели]]"})
end
return err, electoratrue
end
local function merge(candidates,err,candidatrue,mark,candidates_merge)
for key,bool in pairs(candidatrue) do
if not is_in_list(key,candidates_merge) then
table.insert(err,{"no data",mark,"",key})
elseif not is_in_list(key,candidates) then
table.insert(candidates,key)
end
end
return candidates,err
end
-- =p.cand_listing("","Википедия:Выборы арбитров/Зима 2019—2020/Голосование")
local function cand_listing (err, arb_page)
local candidates, candidatrue = {}, {}
local pagep_candid_excl = mw.title.new(arb_page .. '/!')
local pagep_candid_ampe = mw.title.new(arb_page .. '/&')
local pagep_candid_aste = mw.title.new(arb_page .. '/*')
local candidates_excl, candidatrue_excl = exclamat(pagep_candid_excl)
local candidates_ampe, candidatrue_ampe = ampersan(pagep_candid_ampe)
local candidates_aste, candidatrue_aste = asterisk(pagep_candid_aste)
-- if not candidates_excl[1] then
-- table.insert(err,{"no data","service page","",'[['.. tostring(pagep_candid_excl) ..'|/!]]'}) end
-- if not candidates_ampe[1] then
-- table.insert(err,{"no data","allpages changes","",'[['.. tostring(pagep_candid_ampe) ..'|/&]]'}) end
if not candidates_aste[1] then
table.insert(err,{"no data","all votes","",'[['.. tostring(pagep_candid_aste) ..'|/*]]'}) end
candidatrue = candidatrue_excl + candidatrue_ampe + candidatrue_aste
local err_spec = {}
candidates,err_spec = merge(candidates,err_spec,candidatrue,"service page",candidates_excl)
candidates,err_spec = merge(candidates,err_spec,candidatrue,"allpages changes",candidates_ampe)
candidates,err_spec = merge(candidates,err_spec,candidatrue,"all votes",candidates_aste)
-- TODO нужна более чёткая проверка наличия расхождений
if not candidates[1] then
for _, er in ipairs(err_spec) do
table.insert(err,er)
end
end
return err, candidates
end
-- todo - сообщения об ошибках, исключение голосов
-- функция для работы через {{Результат выборов арбитров}}
-- =p.open_vote(mw.getCurrentFrame():newChild{title="Википедия:Выборы арбитров/Лето 2020/Голосование",args={"Википедия:Выборы арбитров/Лето 2020/Голосование"}})
-- =p.open_vote(mw.getCurrentFrame():newChild{title="Википедия:Выборы арбитров/Лето 2020/Тестовое голосование",args={"Википедия:Выборы арбитров/Лето 2020/Тестовое голосование"}})
-- =p.open_vote(mw.getCurrentFrame():newChild{title="Википедия:Выборы арбитров/Зима 2021/Голосование",args={"Википедия:Выборы арбитров/Зима 2021/Голосование"}})
-- =p.open_vote(mw.getCurrentFrame():newChild{title="Википедия:Выборы арбитров/Зима 2021/Форум",args={"Википедия:Выборы арбитров/Зима 2021/Форум"}})
function p.open_vote(frame)
local parent = frame:getParent()
local args = parent.args
local ch_args = frame.args --для отладки
local arb_page = ch_args[1] or mw.title.getCurrentTitle().fullText
local err, vote_table = {}, {}
local candidates, electoratrue, exceptions, valid_votes, pre_result, pre_sort
arb_page = string.gsub( arb_page, "%/Форум", "/Голосование")
err, candidates = cand_listing (err, arb_page)
err, electoratrue = elec_listing (err, arb_page)
err, exceptions = caret(err, arb_page .. "/^")
for _, candidate in ipairs(candidates) do
err, vote_table = vote_listing (err,vote_table,arb_page,candidate)
end
-- TODO function under construction
err, valid_votes = compute (err,vote_table,electoratrue,exceptions) --,date
-- mw.logObject(err)
local result = {}
table.insert(result, table_start)
err, pre_result, pre_sort = reform (err, valid_votes)
-- TODO новая функция - недоделки
for i, cand in ipairs(pre_sort) do
table.insert(result, line_format(i, cand[1], pre_result[cand[1]], arb_page))
end
--]]
local err_notice = {}
for i, err in ipairs(err) do
err_notice[err[1]] = err_notice[err[1]] or {} -- err type
err_notice[err[1]][err[2]] = err_notice[err[1]][err[2]] or {} -- err group
err_notice[err[1]][err[2]][err[3]] = err_notice[err[1]][err[2]][err[3]] or {} -- err subgroup
table.insert(err_notice[err[1]][err[2]][err[3]],err[4]) -- err messaage
end
local err_mass = {}
for e_type, gr_errs in pairs(err_notice) do
table.insert(err_mass,err_type[e_type])
for e_group, subgr_errs in pairs(gr_errs) do
table.insert(err_mass,e_group)
for e_subgr,e_msgs in pairs(subgr_errs) do
table.insert(err_mass,e_subgr)
for i, e_msg in ipairs(e_msgs) do
table.insert(err_mass,e_msg)
end
end
end
end
-- mw.logObject(err_mass)
if err_mass[1] then
table.insert(err_mass,1,err_start)
table.insert(err_mass,err_end)
table.insert(result,table.concat(err_mass," "))
end
table.insert(result, table_end)
return table.concat(result)
end
return p