schulze 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ bin/autospec
20
+ bin/htmldiff
21
+ bin/ldiff
22
+ bin/rspec
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@schulze --create
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Bradley Grzesiak
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Schulze
2
+
3
+ Calculate the winner of a Schulze method election.
4
+
5
+ This gem is intended for command-line use, though it may work alright as a library.
6
+
7
+ ## Warning!
8
+
9
+ This program was designed against a particular use-case... specifically, candidates must have unique last names (where last name is defined as the last element of a `split`). This was done because data on first names tends to be unclean (e.g., "Tim" & "Timothy" are the same person).
10
+
11
+ ## Installation
12
+
13
+ Install it:
14
+
15
+ $ gem install schulze
16
+
17
+ ## Usage
18
+
19
+ Collect ballots in a directory. Each ballot should be a file in the form:
20
+
21
+ 1 A
22
+ 2 B
23
+ 3 C1
24
+ 3 C2
25
+ 4 D
26
+
27
+ Then, run the program:
28
+
29
+ $ schulze directory/to/ballots/*.txt
30
+
31
+ ## Contributing
32
+
33
+ 1. Fork it
34
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
35
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
36
+ 4. Push to the branch (`git push origin my-new-feature`)
37
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/schulze ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/schulze', __FILE__)
4
+
5
+ results = Schulze::Election.perform_for_dir_glob(ARGV[1...ARGV.count])
6
+
7
+ results.each_with_index do |line, idx|
8
+ puts("%3d %s" % [idx+1, line.inspect])
9
+ end
data/lib/schulze.rb ADDED
@@ -0,0 +1,3 @@
1
+ require File.expand_path('../schulze/version', __FILE__)
2
+
3
+ require File.expand_path('../schulze/election', __FILE__)
@@ -0,0 +1,49 @@
1
+ require 'matrix'
2
+
3
+ module Schulze
4
+ class Ballot
5
+ def self.new_from_filename filename
6
+ new.tap do |ballot|
7
+ File.open(filename, 'r') do |io|
8
+ io.each_line do |line|
9
+ chunks = line.split
10
+ ballot.add_choice(chunks.first.to_i, chunks.last)
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ def initialize
17
+ @choices = Hash.new(1e9)
18
+ end
19
+
20
+ def add_choice ordering, name
21
+ @choices[name] = ordering
22
+ end
23
+
24
+ def candidates
25
+ @choices.keys
26
+ end
27
+
28
+ def preference_matrix all_candidates
29
+ matrix = Matrix.zero(all_candidates.size)
30
+ Matrix.build(all_candidates.size) do |row, col|
31
+ if row == col
32
+ 0
33
+ else
34
+ a, b = all_candidates[row], all_candidates[col]
35
+ prefers?(a, b) ? 1 : 0
36
+ end
37
+ end
38
+ end
39
+
40
+ def prefers? a, b
41
+ @choices[a] < @choices[b]
42
+ end
43
+
44
+ def to_s
45
+ "Ballot:\n" + @choices.to_a.map(&:inspect).join("\n")
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,91 @@
1
+ require File.expand_path('../ballot', __FILE__)
2
+ require 'set'
3
+
4
+ module Schulze
5
+ class Election
6
+
7
+ def self.perform_for_dir_glob dir_glob
8
+ election = new
9
+ dir_glob.each do |filename|
10
+ election << Ballot.new_from_filename(filename)
11
+ end
12
+ election.results
13
+ end
14
+
15
+ def initialize
16
+ @ballots = []
17
+ @candidates = Set.new
18
+ end
19
+
20
+ def << ballot
21
+ @ballots << ballot
22
+ @candidates.merge ballot.candidates
23
+ end
24
+
25
+ def results
26
+ candidate_array = @candidates.to_a.sort
27
+ pairwise = generate_pairwise_preferences candidate_array
28
+ path_strengths = generate_path_strengths pairwise
29
+
30
+ if !(defined?(SILENCE) && SILENCE)
31
+ path_strengths.each_with_index do |row, idx|
32
+ puts("%20s %s" % [candidate_array[idx], row.inspect])
33
+ end
34
+ end
35
+
36
+ generate_orderings candidate_array, path_strengths
37
+ end
38
+
39
+ private
40
+
41
+ def generate_pairwise_preferences candidates
42
+ @ballots.inject(::Matrix.zero(@candidates.count)) do |memo, ballot|
43
+ memo + ballot.preference_matrix(candidates)
44
+ end
45
+ end
46
+
47
+ def generate_path_strengths d
48
+ size = d.row_size
49
+ paths = Matrix.build(size) do |row, col|
50
+ if row == col
51
+ 0
52
+ else
53
+ d[row,col] > d[col,row] ? d[row,col] : 0
54
+ end
55
+ end
56
+ ret = d.to_a
57
+ 0.upto(size-1) do |i|
58
+ 0.upto(size-1) do |j|
59
+ unless i == j
60
+ 0.upto(size-1) do |k|
61
+ if i != k && j != k
62
+ ret[j][k] = [ret[j][k], [ret[j][i], ret[i][k]].min].max
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ ret
69
+ end
70
+
71
+ def generate_orderings candidate_array, path_strengths
72
+ sorted = candidate_array.sort do |a, b|
73
+ i, j = candidate_array.index(a), candidate_array.index(b)
74
+ path_strengths[j][i] <=> path_strengths[i][j]
75
+ end
76
+ ret = [[sorted.shift]]
77
+ while not sorted.empty?
78
+ last_addition = ret.last.last
79
+ next_candidate = sorted.shift
80
+ i, j = candidate_array.index(last_addition), candidate_array.index(next_candidate)
81
+ if path_strengths[i][j] == path_strengths[j][i]
82
+ ret.last << next_candidate
83
+ else
84
+ ret << [next_candidate]
85
+ end
86
+ end
87
+ ret
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module Schulze
2
+ VERSION = "0.0.1"
3
+ end
data/schulze.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/schulze/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Bradley Grzesiak"]
6
+ gem.email = ["brad@bendyworks.com"]
7
+ gem.description = %q{The Schulze Method, for ruby}
8
+ gem.summary = %q{The Schulze Method is a Condorcet voting method that computes a list of candidates sorted by the electorate's preferences}
9
+ gem.homepage = "https://github.com/madisonium/schulze"
10
+
11
+ gem.executables = ['schulze']
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "schulze"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Schulze::VERSION
17
+
18
+ gem.add_development_dependency 'rspec'
19
+ end
@@ -0,0 +1,105 @@
1
+ # See: http://en.wikipedia.org/w/index.php?title=User:MarkusSchulze/Schulze_method_examples&oldid=383750275
2
+
3
+ require 'schulze'
4
+ require 'fileutils'
5
+
6
+ SILENCE = true
7
+
8
+ describe 'schulze voting method' do
9
+ let(:tmp_dir) { '/tmp/schulze' }
10
+ let(:candidate_count) { ballots.keys.first.length }
11
+
12
+ after do
13
+ FileUtils.rm_rf(tmp_dir)
14
+ end
15
+
16
+ before do
17
+ FileUtils.mkdir(tmp_dir)
18
+
19
+ ballots.each_pair do |ordering, count|
20
+ 1.upto(count) do |idx|
21
+ File.open("#{tmp_dir}/#{ordering}.#{idx}.txt", 'w') do |f|
22
+ pairs = (1..candidate_count).zip(ordering.split(''))
23
+ f.puts pairs.map{|x| "#{x[0]} #{x[1]}"}.join("\n")
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+
31
+
32
+
33
+ shared_examples_for 'standard schulze' do
34
+ it 'works' do
35
+ result = Schulze::Election.perform_for_dir_glob Dir["#{tmp_dir}/*.txt"]
36
+ result.should == expected
37
+ end
38
+ end
39
+
40
+
41
+
42
+
43
+ describe 'example 1' do
44
+ let(:ballots) { {
45
+ 'ACBED' => 5,
46
+ 'ADECB' => 5,
47
+ 'BEDAC' => 8,
48
+ 'CABED' => 3,
49
+ 'CAEBD' => 7,
50
+ 'CBADE' => 2,
51
+ 'DCEBA' => 7,
52
+ 'EBADC' => 8
53
+ }
54
+ }
55
+ let(:expected) { [['E'], ['A'], ['C'], ['B'], ['D']] }
56
+ it_should_behave_like 'standard schulze'
57
+ end
58
+
59
+ describe 'example 2' do
60
+ let(:ballots) { {
61
+ 'ACBD' => 5,
62
+ 'ACDB' => 2,
63
+ 'ADCB' => 3,
64
+ 'BACD' => 4,
65
+ 'CBDA' => 3,
66
+ 'CDBA' => 3,
67
+ 'DACB' => 1,
68
+ 'DBAC' => 5,
69
+ 'DCBA' => 4
70
+ }
71
+ }
72
+ let(:expected) { [['D'], ['A'], ['C'], ['B']] }
73
+ it_should_behave_like 'standard schulze'
74
+ end
75
+
76
+ describe 'example 3' do
77
+ let(:ballots) { {
78
+ 'ABDEC' => 3,
79
+ 'ADEBC' => 5,
80
+ 'ADECB' => 1,
81
+ 'BADEC' => 2,
82
+ 'BDECA' => 2,
83
+ 'CABDE' => 4,
84
+ 'CBADE' => 6,
85
+ 'DBECA' => 2,
86
+ 'DECAB' => 5
87
+ }
88
+ }
89
+ let(:expected) { [['B'], ['A'], ['D'], ['E'], ['C']] }
90
+ it_should_behave_like 'standard schulze'
91
+ end
92
+
93
+ describe 'example 4', :pending => 'does not handle degenerate cases well' do
94
+ let(:ballots) { {
95
+ 'ABCD' => 3,
96
+ 'DABC' => 2,
97
+ 'DBCA' => 2,
98
+ 'CBDA' => 2
99
+ }
100
+ }
101
+ let(:expected) { [['B'], ['C'], ['D'], ['A']] }
102
+ it_should_behave_like 'standard schulze'
103
+ end
104
+
105
+ end
@@ -0,0 +1,25 @@
1
+ require 'schulze/ballot'
2
+ require 'set'
3
+ require 'matrix'
4
+
5
+ describe Schulze::Ballot do
6
+ describe 'preference_matrix' do
7
+ example '4 candidates, 4 choices' do
8
+ expected = ::Matrix[[0, 1, 1, 1], [0, 0, 1, 1], [0, 0, 0, 1], [0, 0, 0, 0]]
9
+ ballot = Schulze::Ballot.new
10
+ ballot.add_choice 1, 'A'
11
+ ballot.add_choice 2, 'B'
12
+ ballot.add_choice 3, 'C'
13
+ ballot.add_choice 4, 'D'
14
+ result = ballot.preference_matrix %w(A B C D)
15
+ result.should == expected
16
+ end
17
+ example '4 candidates, 1 choice' do
18
+ expected = ::Matrix[[0, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
19
+ ballot = Schulze::Ballot.new
20
+ ballot.add_choice 1, 'A'
21
+ result = ballot.preference_matrix %w(A B C D)
22
+ result.should == expected
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schulze
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Bradley Grzesiak
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70262322232300 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70262322232300
25
+ description: The Schulze Method, for ruby
26
+ email:
27
+ - brad@bendyworks.com
28
+ executables:
29
+ - schulze
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - .rspec
35
+ - .rvmrc
36
+ - Gemfile
37
+ - LICENSE
38
+ - README.md
39
+ - Rakefile
40
+ - bin/schulze
41
+ - lib/schulze.rb
42
+ - lib/schulze/ballot.rb
43
+ - lib/schulze/election.rb
44
+ - lib/schulze/version.rb
45
+ - schulze.gemspec
46
+ - spec/acceptance/wikipedia_spec.rb
47
+ - spec/lib/schulze/ballot_spec.rb
48
+ homepage: https://github.com/madisonium/schulze
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.15
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: The Schulze Method is a Condorcet voting method that computes a list of candidates
72
+ sorted by the electorate's preferences
73
+ test_files:
74
+ - spec/acceptance/wikipedia_spec.rb
75
+ - spec/lib/schulze/ballot_spec.rb