Benutzer:RhoTP/schulze-simple 2 3
Hier der Source-Code für die Auswertung mit schulze-simple für 2/3 Mehrheiten (schulze-simple_2_3).
(Anlage für diesen Antrag : BE:Parteitag/2014.1/Antragskommission/Antragsportal/Sonstiger_Antrag_-_004 )
Das Tool schulze-simple benutzen wir in Berlin bisher zur Auswertung von Wahlen für Kandidatenlisten nach Anlage A der GO.
Das Orginal für 1/2-Mehrheiten gibt es hier: preftools.
Das wiki erlaubt es mir nicht Skripte hochzuladen, deshalb hier der Source-Code zum copy/pasten.
Das Tool ist in Lua geschrieben.
Diff von schulze-simple und schulze-simple_2_3: Diff
Lizenz (preftools)
Copyright (c) 2010 Public Software Group e. V., Berlin, Germany
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Quellcode
#!/usr/bin/env lua
----------------------
-- Helper functions --
----------------------
function message(...)
io.stderr:write(...)
end
------------------------------------------
-- Print notice related to tie-breaking --
------------------------------------------
message("NOTICE: This simplified version of the program does not perform tie-breaking.\n\n")
--------------------------------------
-- Command line argument processing --
--------------------------------------
settings = {}
do
local next_arg
do
local argv = {...}
local i = 0
next_arg = function()
i = i + 1
return argv[i]
end
end
local function command_line_error()
message("Get help with -h or --help.\n")
os.exit(1)
end
for option in next_arg do
local argument, lower_argument
local function require_argument()
argument = next_arg()
if argument == nil then
message('Command line option "', option, '" requires an argument.\n')
command_line_error()
end
lower_argument = string.lower(argument)
end
local function set_setting_once(key, value)
if settings[key] ~= nil then
message('Command line option "', option, '" occurred multiple times.\n')
command_line_error()
end
settings[key] = value
end
if option == "-h" or option == "--help" then
io.stdout:write("Usage:\n")
io.stdout:write("schuool -c|--candidates <candidates_file>\n")
io.stdout:write(" -b|--ballots <ballots_file>\n")
io.stdout:write(" [ -d|--default n|neutral|f|first|l|last ]\n")
io.stdout:write(" [ -o|--output <output_file> ]\n")
os.exit(0)
elseif option == "-c" or option == "--candidates" then
require_argument()
set_setting_once("candidates_filename", argument)
elseif option == "-b" or option == "--ballots" then
require_argument()
set_setting_once("ballots_filename", argument)
elseif option == "-d" or option == "--default" then
require_argument()
if lower_argument == "n" or lower_argument == "neutral" then
set_setting_once("default", "n")
elseif lower_argument == "f" or lower_argument == "first" then
set_setting_once("default", "f")
elseif lower_argument == "l" or lower_argument == "last" then
set_setting_once("default", "l")
else
message('Unknown default position "', argument, '" specified after ', option, ' switch.\n')
command_line_error()
end
elseif option == "-o" or option == "--output" then
require_argument()
set_setting_once("output_filename", argument)
else
message('Illegal command line option "', option, '"\n')
command_line_error()
end
end
if settings.candidates_filename == nil then
message("Use -c or --candidates to specify file containing candidate information.\n")
command_line_error()
end
if settings.ballots_filename == nil then
message("Use -b or --ballots to specify file containing all ballot data.\n")
command_line_error()
end
end
--------------------------
-- I/O helper functions --
--------------------------
function strip(str)
return string.match(str, "^%s*(.-)%s*$")
end
function stripped_lines(filename)
local file, errmsg = io.open(filename, "r")
if not file then
message(errmsg, "\n")
os.exit(2)
end
local get_next_line = file:lines(filename)
return function()
if not file then return nil end
local line
repeat
line = get_next_line()
if line == nil then
file:close()
file = nil
return nil
end
line = strip(string.match(line, "^[^#]*"))
until line ~= ""
return line
end
end
function stripped_gmatch(str, pattern)
local next_entry = string.gmatch(str, pattern)
return function()
local entry
repeat
entry = next_entry()
if entry then entry = strip(entry) end
until entry ~= ""
return entry
end
end
do
local output_file
if settings.output_filename == nil then
output_file = io.stdout
else
local errmsg
output_file, errmsg = io.open(settings.output_filename, "w")
if not output_file then
message(errmsg, "\n")
os.exit(2)
end
end
function output(...)
output_file:write(...)
end
end
function padded_number(number, maximum)
local str = tostring(number)
local max_digits = 1
local tmp = maximum
while tmp >= 10 do
tmp = math.floor(tmp / 10)
max_digits = max_digits + 1
end
for i = 1, max_digits - #str do
str = " " .. str
end
return str
end
---------------------
-- Read candidates --
---------------------
candidates = {} -- mapping string to candidate number and vice versa
do
for line in stripped_lines(settings.candidates_filename) do
for candidate in stripped_gmatch(line, "[^;,]+") do
if candidates[candidate] then
message('Duplicate candidate in "', settings.candidates_filename, '": "', candidate, '".\n')
os.exit(2)
end
candidates[#candidates+1] = candidate
candidates[candidate] = #candidates
end
end
end
------------------------------
-- Read and process ballots --
------------------------------
ballots = {}
approval_counts = {}
disapproval_counts = {}
for i = 1, #candidates do
approval_counts[candidates[i]] = 0
disapproval_counts[candidates[i]] = 0
end
for line in stripped_lines(settings.ballots_filename) do
local ballot = {}
local rank
local processed = {}
local approvals, neutrals, disapprovals = string.match(line, "^([^/]*)/([^/]*)/([^/]*)$")
if not approvals then
approvals, neutrals, disapprovals = string.match(line, "^([^/]*)$"), "", ""
end
if not approvals then
message('Ill formatted ballot: "', line, '".\n')
os.exit(2)
end
local function process_lists(candidate_lists, count_table)
for candidate_list in string.gmatch(candidate_lists, "[^;]+") do
if rank == -1 then
-- only happens when there are different rankings in the neutral section
message('Different rankings (semicolon) found in neutral section of ballot "', line, '".\n')
os.exit(2)
end
local empty = true
for candidate in stripped_gmatch(candidate_list, "[^,]+") do
empty = false
if not candidates[candidate] then
message('Unknown candidate "', candidate, '" contained in ballot "', line, '".\n')
os.exit(2)
end
if processed[candidate] then
message('Duplicate candidate "', candidate, '" in ballot "', line, '".\n')
os.exit(2)
end
ballot[candidate] = rank
if count_table then
count_table[candidate] = count_table[candidate] + 1
end
processed[candidate] = true
end
if not empty then
-- It is important to only decrease rank, when candidates have been processed.
rank = rank - 1
end
end
end
rank = #candidates
process_lists(approvals, approval_counts)
rank = 0
process_lists(neutrals)
rank = -2 -- rank -1 is reserved for default=first
process_lists(disapprovals, disapproval_counts)
for i = 1, #candidates do
local candidate = candidates[i]
if not processed[candidate] then
if settings.default == "n" then
ballot[candidate] = 0
elseif settings.default == "f" then
ballot[candidate] = -1
disapproval_counts[candidate] = disapproval_counts[candidate] + 1
elseif settings.default == "l" then
ballot[candidate] = -#candidates - 2
disapproval_counts[candidate] = disapproval_counts[candidate] + 1
else
message('Candidate "', candidate, '" missing in ballot "', line, '".\n')
os.exit(2)
end
end
end
ballots[#ballots+1] = ballot
end
-------------------------------------------------------------------------
-- Select approved candidates, who passed the hard-coded quota of 2/3+ --
-------------------------------------------------------------------------
local approved_candidates = {}
do
local max_approval = 0
local max_disapproval = 0
local max_neutral = 0
for i = 1, #candidates do
local candidate = candidates[i]
local approval_count = approval_counts[candidate]
local disapproval_count = disapproval_counts[candidate]
local neutral_count = #ballots - approval_count - disapproval_count
max_approval = math.max(max_approval, approval_count)
max_disapproval = math.max(max_disapproval, disapproval_count)
max_neutral = math.max(max_neutral, neutral_count)
end
output("Candidates:\n")
for i = 1, #candidates do
local candidate = candidates[i]
local approval_count = approval_counts[candidate]
local disapproval_count = disapproval_counts[candidate]
local neutral_count = #ballots - approval_count - disapproval_count
local approved = approval_count > 2*disapproval_count
output(padded_number(i, #candidates), ". ", padded_number(approval_count, max_approval), ':', padded_number(disapproval_count, max_disapproval), '(:', padded_number(neutral_count, max_neutral), ') 2/3+ ', approved and "APPROVED" or "FAILED ", ' ', candidates[i], "\n")
if approved then
approved_candidates[#approved_candidates+1] = candidate
approved_candidates[candidate] = #approved_candidates
end
end
output("\n")
end
----------------------------------------------------------------
-- Calculate battles and rankings according to Schulze method --
----------------------------------------------------------------
battles = {} -- two dimensional array
max_pro_contra = 0
for i = 1, #approved_candidates do
battles[i] = {}
end
ranking = {} -- mapping candidate name to ranking number and ranking number to list of candidates
function beating_weight(pro, contra)
if pro > contra then
return pro
else
return 0
end
end
do
local matrix = {}
for i = 1, #approved_candidates do
matrix[i] = {}
end
for i = 1, #approved_candidates do
for j = i+1, #approved_candidates do
local pro, contra = 0, 0
for k = 1, #ballots do
local ballot = ballots[k]
local rank1 = ballot[approved_candidates[i]]
local rank2 = ballot[approved_candidates[j]]
if rank1 > rank2 then
pro = pro + 1
elseif rank2 > rank1 then
contra = contra + 1
end
end
battles[i][j] = pro
battles[j][i] = contra
max_pro_contra = math.max(max_pro_contra, pro)
matrix[i][j] = beating_weight(pro, contra)
matrix[j][i] = beating_weight(contra, pro)
end
end
for i = 1, #approved_candidates do
for j = 1, #approved_candidates do
if i ~= j then
for k = 1, #approved_candidates do
if i ~= k and j ~= k then
matrix[j][k] = math.max(matrix[j][k], math.min(matrix[j][i], matrix[i][k]))
end
end
end
end
end
local count = 0
repeat
local winners = {}
for i = 1, #approved_candidates do
local candidate = approved_candidates[i]
if ranking[candidate] == nil then
local best = true
for j = 1, #approved_candidates do
if i ~= j then
local other_candidate = approved_candidates[j]
if ranking[other_candidate] == nil and matrix[j][i] > matrix[i][j] then
best = false
break
end
end
end
if best then
winners[#winners+1] = candidate
end
end
end
ranking[#ranking+1] = winners
for i = 1, #winners do
ranking[winners[i]] = #ranking
count = count + 1
end
until count == #approved_candidates
end
--------------------
-- Output ranking --
--------------------
if #approved_candidates > 0 then
output("Ranking:\n")
for rank = 1, #ranking do
local list = ranking[rank]
for i = 1, #list do
local candidate = list[i]
output(padded_number(rank, #ranking), ". ", candidate, "\n")
end
end
output("\n")
end
---------------------------
-- Output battle results --
---------------------------
if #approved_candidates > 1 then
for rank = 1, #ranking do
local list = ranking[rank]
for i = 1, #list do
local candidate = list[i]
output("Direct comparison of: ", padded_number(rank, #ranking), ". ", candidate, "\n")
for other_rank = 1, #ranking do
local other_list = ranking[other_rank]
for j = 1, #other_list do
local other_candidate = other_list[j]
if candidate ~= other_candidate then
local pro = battles[approved_candidates[candidate]][approved_candidates[other_candidate]]
local contra = battles[approved_candidates[other_candidate]][approved_candidates[candidate]]
output(padded_number(pro, max_pro_contra), ":", padded_number(contra, max_pro_contra), " ", padded_number(other_rank, #ranking), ". ", other_candidate, "\n")
end
end
end
output("\n")
end
end
end