Benutzer:RhoTP/schulze-simple 2 3

Aus Piratenwiki
Wechseln zu: Navigation, Suche

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