schulze-vote 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 53d7c71ae5e43fcd9a28bf01eaf4e7f16819f5b0
4
- data.tar.gz: 989df507edb37c37e043891c28224b643572b0ea
3
+ metadata.gz: 9866e914ebb502be11324a542dfb49de786a7408
4
+ data.tar.gz: 6ade5e377c2b4e1f01444fd110e535e54df71ac4
5
5
  SHA512:
6
- metadata.gz: 1082070bf51ba9aa979670a21240bc0a42efad17ee2eb385284ed2e5386f850dd537d4f837a04cdf766238cd70e691665615f478366d4735e357fa389da89c30
7
- data.tar.gz: c2b409116e6066aebc43250aae94081681d70d700e65738dc246beeacca6c313628d2c19570ed040276a7802ec5a9a50c46ce7df0604cda8bf4adae49a26924a
6
+ metadata.gz: 03f614f995678224821bc5d81fa8e5d86945aa0086f767ad96b35d6d577d2d9742177b2bad6fa23ad4e7fa0560e3f17638cf1113d1738e463d40f912d76a2a5a
7
+ data.tar.gz: 5088baad29d4959dee3df8d4aa232be97090dda1111648a9c388f65dc87e07f4ce6aac55b757e674bbd0f745866424d3eedeb7f69fa43a095ecd2764f89631d5
data/README.md CHANGED
@@ -3,6 +3,15 @@
3
3
  This gem is a Ruby implementation of the Schulze voting method (with help of the Floyd–Warshall algorithm),
4
4
  a type of the Condorcet voting methods.
5
5
 
6
+ ## Master
7
+
8
+ [![Build Status](https://travis-ci.org/coorasse/schulze-vote.svg?branch=master)](https://travis-ci.org/coorasse/schulze-vote)
9
+
10
+ ## Develop
11
+
12
+ [![Build Status](https://travis-ci.org/coorasse/schulze-vote.svg?branch=develop)](https://travis-ci.org/coorasse/schulze-vote)
13
+ [![Code Climate](https://codeclimate.com/github/coorasse/schulze-vote/badges/gpa.svg)](https://codeclimate.com/github/coorasse/schulze-vote)
14
+ [![Test Coverage](https://codeclimate.com/github/coorasse/schulze-vote/badges/coverage.svg)](https://codeclimate.com/github/coorasse/schulze-vote/coverage)
6
15
 
7
16
  Wikipedia:
8
17
 
@@ -6,3 +6,4 @@ autoload :Vote, 'vote'
6
6
 
7
7
  SchulzeInput = Vote::Condorcet::Schulze::Input
8
8
  SchulzeBasic = Vote::Condorcet::Schulze::Basic
9
+ SchulzeClassifications = Vote::Condorcet::Schulze::Classifications
@@ -3,6 +3,7 @@ module Vote
3
3
  module Schulze
4
4
  autoload :Input, 'vote/condorcet/schulze/input'
5
5
  autoload :Basic, 'vote/condorcet/schulze/basic'
6
+ autoload :Classifications, 'vote/condorcet/schulze/classifications'
6
7
  end
7
8
  end
8
9
  end
@@ -2,6 +2,14 @@ module Vote
2
2
  module Condorcet
3
3
  module Schulze
4
4
  class Basic
5
+ attr_reader :candidates, :vote_matrix, :play_matrix, :result_matrix, :ranking, :vote_count
6
+ attr_reader :winners_array, :potential_winners, :beat_couples, :ties
7
+
8
+ def initialize
9
+ @beat_couples = []
10
+ @ties = []
11
+ end
12
+
5
13
  # All-in-One class method to get a calculated SchulzeBasic object
6
14
  def self.do(vote_matrix, candidate_count = nil)
7
15
  instance = new
@@ -14,10 +22,7 @@ module Vote
14
22
  input = if vote_matrix.is_a?(Vote::Condorcet::Schulze::Input)
15
23
  vote_matrix
16
24
  else
17
- Vote::Condorcet::Schulze::Input.new(
18
- vote_matrix,
19
- candidate_count
20
- )
25
+ Vote::Condorcet::Schulze::Input.new(vote_matrix, candidate_count)
21
26
  end
22
27
  @vote_matrix = input.matrix
23
28
  @candidate_count = input.candidates
@@ -32,64 +37,22 @@ module Vote
32
37
  calculate_winners
33
38
  rank
34
39
  calculate_beat_couples
40
+ calculate_potential_winners
35
41
  end
36
42
 
37
- attr_reader :vote_matrix
38
-
39
- attr_reader :play_matrix
40
-
41
- attr_reader :result_matrix
42
-
43
- def ranks
44
- @ranking
45
- end
46
-
47
- def voters
48
- @vote_count
49
- end
50
-
51
- # return all possible solutions to the votation
52
- attr_reader :winners_array
43
+ private
53
44
 
54
- # compute all possible solutions
55
- # since this can take days, there is an option to limit the number of calculated classifications
56
- # the default is 10. if the system is calculating more then 10 possible classifications it will stop
57
- # raising a TooManyClassifications exception
58
- # you can set it to false to disable the limit
59
- def classifications(limit_results = false)
60
- @classifications ||= calculate_classifications(limit_results)
45
+ def play
46
+ @play_matrix = build_play_matrix
47
+ find_matches_with_wins
48
+ find_strongest_paths
61
49
  end
62
50
 
63
- attr_reader :beat_couples
64
-
65
- attr_reader :ties
66
-
67
- # compute the final classification with ties included
68
- # the result is an array of arrays. each position can contain one or more elements in tie
69
- # e.g. [[0,1], [2,3], [4], [5]]
70
- def classification_with_ties
71
- calculate_potential_winners
72
- result = []
73
- result << @potential_winners # add potential winners on first place
74
- result += @ties.clone.sort_by { |tie| -@ranking[tie[0]] } # add all the ties ordered by ranking
75
- result.uniq! # remove duplicates (potential winners are also ties)
76
- excludeds = (@candidates - result.flatten) # all remaining elements (not in a tie and not winners)
77
- excludeds.each do |excluded|
78
- result.each_with_index do |position, index|
79
- # insert before another element if they have a better ranking
80
- break result.insert(index, [excluded]) if has_better_ranking?(excluded, position[0])
81
- # insert at the end if it's the last possible position
82
- break result.insert(-1, [excluded]) if index == result.size - 1
83
- end
84
- end
85
- result
51
+ def build_play_matrix
52
+ ::Matrix.scalar(@candidate_count, 0).extend(Vote::Matrix)
86
53
  end
87
54
 
88
- private
89
-
90
- def play
91
- @play_matrix = ::Matrix.scalar(@candidate_count, 0).extend(Vote::Matrix)
92
- # step 1: find matches with wins
55
+ def find_matches_with_wins
93
56
  @candidate_count.times do |i|
94
57
  @candidate_count.times do |j|
95
58
  next if i == j
@@ -100,17 +63,15 @@ module Vote
100
63
  end
101
64
  end
102
65
  end
66
+ end
103
67
 
104
- # step 2: find strongest paths
68
+ def find_strongest_paths
105
69
  @candidate_count.times do |i|
106
70
  @candidate_count.times do |j|
107
71
  next if i == j
108
72
  @candidate_count.times do |k|
109
73
  next if (i == k) || (j == k)
110
- @play_matrix[j, k] = [
111
- @play_matrix[j, k],
112
- [@play_matrix[j, i], @play_matrix[i, k]].min
113
- ].max
74
+ @play_matrix[j, k] = [@play_matrix[j, k], [@play_matrix[j, i], @play_matrix[i, k]].min].max
114
75
  end
115
76
  end
116
77
  end
@@ -136,112 +97,48 @@ module Vote
136
97
  end
137
98
 
138
99
  def rank
139
- @ranking = @result_matrix.
140
- row_vectors.map { |e| e.inject(0) { |s, v| s += v } }
100
+ @ranking = @result_matrix.row_vectors.map { |rm| rm.inject(0) { |a, e| a + e } }
141
101
  end
142
102
 
143
103
  # you should call calculate_winners first
144
104
  def calculate_potential_winners
145
- @potential_winners = []
146
- winners_array.each_with_index do |val, idx|
147
- @potential_winners << idx if val > 0
148
- end
149
- @potential_winners
105
+ @potential_winners ||= winners_array.map.with_index { |val, idx| idx if val > 0 }.compact
150
106
  end
151
107
 
152
108
  # calculates @beat_couples and @ties in roder to display results afterward
153
109
  def calculate_beat_couples
154
110
  return if @calculated_beat_couples
155
- @beat_couples = []
156
- @ties = []
157
- ranks.each_with_index do |_val, idx|
158
- ranks.each_with_index do |_val2, idx2|
111
+
112
+ ranking.each_with_index do |_val, idx|
113
+ ranking.each_with_index do |_val2, idx2|
159
114
  next if idx == idx2
160
115
  next @beat_couples << [idx, idx2] if play_matrix[idx, idx2] > play_matrix[idx2, idx]
161
- next unless in_tie?(idx, idx2)
162
- next if @ties.any? { |tie| ([idx, idx2] - tie).empty? }
163
- tie = @ties.find { |tie| tie.any? { |el| el == idx } }
164
- next tie << idx2 if tie
165
- tie = @ties.find { |tie| tie.any? { |el| el == idx2 } }
166
- next tie << idx if tie
167
- @ties << [idx, idx2]
116
+ calculate_ties(idx, idx2)
168
117
  end
169
118
  end
170
119
  @calculated_beat_couples = true
171
120
  end
172
121
 
173
- def in_tie?(idx, idx2)
174
- play_matrix[idx, idx2] == play_matrix[idx2, idx] &&
175
- @ranking[idx] == @ranking[idx2] &&
176
- @winners_array[idx] == @winners_array[idx2]
177
- end
178
-
179
- def has_better_ranking?(a, b)
180
- @ranking[a] > @ranking[b]
181
- end
182
-
183
- def rank_element(el)
184
- rank = 0
185
- rank -= 100 if @potential_winners.include?(el)
186
- beat_couples.each do |b|
187
- rank -= 1 if b[0] == el
188
- end
189
- rank
190
- end
191
-
192
- def calculate_classifications(limit_results)
193
- calculate_potential_winners
194
-
195
- start_list = (0..ranks.length - 1).to_a
196
- start_list.sort! { |e1, e2| rank_element(e1) <=> rank_element(e2) }
197
-
198
- classifications = []
199
- compute_classifications(classifications, [], @potential_winners, beat_couples, start_list, limit_results)
200
- classifications
201
- end
202
-
203
- def compute_classifications(classifications, classif = [], potential_winners,
204
- beated_list, start_list, limit_results)
205
- if beated_list.empty?
206
- start_list.permutation.each do |array|
207
- classifications << classif + array
208
- check_limits(classifications, limit_results)
209
- end
210
- else
211
- if classif.empty? && potential_winners.any?
212
- potential_winners.each do |element|
213
- add_element(classifications, classif, nil, beated_list, start_list, element, limit_results)
214
- end
215
- else
216
- start_list.each do |element|
217
- add_element(classifications, classif, nil, beated_list, start_list, element, limit_results)
218
- end
219
- end
220
- end
122
+ def calculate_ties(idx, idx2)
123
+ return unless in_tie?(idx, idx2)
124
+ return if @ties.any? { |tie| ([idx, idx2] - tie).empty? }
125
+ found_tie = tie_by_idx(idx)
126
+ return found_tie << idx2 if found_tie
127
+ found_tie = tie_by_idx(idx2)
128
+ return found_tie << idx if found_tie
129
+ @ties << [idx, idx2]
221
130
  end
222
131
 
223
- def check_limits(classifications, limit_results)
224
- fail TooManyClassificationsException if limit_results && classifications.size > limit_results
132
+ def tie_by_idx(idx)
133
+ @ties.find { |tie| tie.any? { |el| el == idx } }
225
134
  end
226
135
 
227
- def add_element(classifications, classif, _potential_winners, beated_list, start_list, element, limit_results)
228
- return if beated_list.any? { |c| c[1] == element }
229
- classification = classif.clone
230
- classification << element
231
- next_beated_list = beated_list.clone.delete_if { |c| c[0] == element }
232
- next_start_list = start_list.clone
233
- next_start_list.delete(element)
234
- if next_start_list.empty?
235
- classifications << classification
236
- check_limits(classifications, limit_results)
237
- else
238
- compute_classifications(classifications, classification, nil, next_beated_list, next_start_list, limit_results)
239
- end
136
+ def in_tie?(idx, idx2)
137
+ @play_matrix[idx, idx2] == @play_matrix[idx2, idx] &&
138
+ @ranking[idx] == @ranking[idx2] &&
139
+ @winners_array[idx] == @winners_array[idx2]
240
140
  end
241
141
  end
242
142
  end
243
143
  end
244
144
  end
245
-
246
- class TooManyClassificationsException < StandardError
247
- end
@@ -0,0 +1,109 @@
1
+ module Vote
2
+ module Condorcet
3
+ module Schulze
4
+ class Classifications
5
+ def initialize(schulze_basic)
6
+ @schulze_basic = schulze_basic
7
+ end
8
+
9
+ # compute all possible solutions
10
+ # since this can take days, there is an option to limit the number of calculated classifications
11
+ # the default is 10. if the system is calculating more then 10 possible classifications it will stop
12
+ # raising a TooManyClassifications exception
13
+ # you can set it to false to disable the limit
14
+ def classifications(limit_results = false)
15
+ @classifications = []
16
+ @limit_results = limit_results
17
+ calculate_classifications
18
+ @classifications
19
+ end
20
+
21
+ # compute the final classification with ties included
22
+ # the result is an array of arrays. each position can contain one or more elements in tie
23
+ # e.g. [[0,1], [2,3], [4], [5]]
24
+ def classification_with_ties
25
+ result = []
26
+ result << @schulze_basic.potential_winners # add potential winners on first place
27
+ result += @schulze_basic.ties.clone.sort_by { |tie| -@schulze_basic.ranking[tie[0]] } # add ties by ranking
28
+ result.uniq! # remove duplicates (potential winners are also ties)
29
+ add_excludeds(result)
30
+ end
31
+
32
+ def add_excludeds(result)
33
+ excludeds = (@schulze_basic.candidates - result.flatten) # all remaining elements (not in tie, not winners)
34
+ excludeds.each do |excluded|
35
+ result.each_with_index do |position, index|
36
+ # insert before another element if they have a better ranking
37
+ break result.insert(index, [excluded]) if better_ranking?(excluded, position[0])
38
+ # insert at the end if it's the last possible position
39
+ break result.insert(-1, [excluded]) if index == result.size - 1
40
+ end
41
+ end
42
+ result
43
+ end
44
+
45
+ private
46
+
47
+ def better_ranking?(a, b)
48
+ @schulze_basic.ranking[a] > @schulze_basic.ranking[b]
49
+ end
50
+
51
+ def rank_element(el)
52
+ rank = 0
53
+ rank -= 100 if @schulze_basic.potential_winners.include?(el)
54
+ @schulze_basic.beat_couples.each do |b|
55
+ rank -= 1 if b[0] == el
56
+ end
57
+ rank
58
+ end
59
+
60
+ def calculate_classifications
61
+ start_list = (0..@schulze_basic.ranking.length - 1).to_a
62
+ start_list.sort! { |e1, e2| rank_element(e1) <=> rank_element(e2) }
63
+ compute_classifications([], @schulze_basic.potential_winners, @schulze_basic.beat_couples, start_list)
64
+ end
65
+
66
+ def compute_classifications(classif = [], potential_winners, beated_list, start_list)
67
+ return compute_permutations(classif, start_list) if beated_list.empty?
68
+ next_list = (classif.empty? && potential_winners.any?) ? potential_winners : start_list
69
+ add_elements(beated_list, classif, next_list, start_list)
70
+ end
71
+
72
+ def add_elements(beated_list, classif, potential_winners, start_list)
73
+ potential_winners.each { |element| add_element(classif, beated_list, start_list, element) }
74
+ end
75
+
76
+ def compute_permutations(classif, start_list)
77
+ start_list.permutation.each do |array|
78
+ @classifications << classif + array
79
+ check_limits
80
+ end
81
+ end
82
+
83
+ def check_limits
84
+ fail TooManyClassificationsException if @limit_results && @classifications.size > @limit_results
85
+ end
86
+
87
+ def add_element(classif, beated_list, start_list, element)
88
+ return if beated_list.any? { |c| c[1] == element }
89
+ classification = classif.clone << element
90
+ next_start_list = clone_and_delete(start_list, element)
91
+ if next_start_list.empty?
92
+ @classifications << classification
93
+ check_limits
94
+ else
95
+ compute_classifications(classification, nil,
96
+ beated_list.clone.delete_if { |c| c[0] == element }, next_start_list)
97
+ end
98
+ end
99
+
100
+ def clone_and_delete(list, element)
101
+ list.clone.tap { |l| l.delete(element) }
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ class TooManyClassificationsException < StandardError
109
+ end
@@ -12,7 +12,7 @@ module Vote
12
12
  else
13
13
  @vote_matrix = ::Matrix.scalar(@candidate_count, 0).extend(Vote::Matrix)
14
14
  insert_vote_array(@vote_list) if vote_list.is_a?(Array)
15
- insert_vote_string(@vote_list) if vote_list.is_a?(String)
15
+ insert_vote_strings(@vote_list) if vote_list.is_a?(String)
16
16
  end
17
17
  end
18
18
 
@@ -26,7 +26,7 @@ module Vote
26
26
  @vote_count = va.size
27
27
  end
28
28
 
29
- def insert_vote_string(vs)
29
+ def insert_vote_strings(vs)
30
30
  vote_array = []
31
31
 
32
32
  vs.split(/\n|\n\r|\r/).each do |voter|
@@ -35,32 +35,35 @@ module Vote
35
35
 
36
36
  vcount.times do
37
37
  tmp = voter.last.split(/;/)
38
- tmp2 = []
38
+ vote_array << extract_vote_string(tmp)
39
+ end
40
+ end
39
41
 
40
- tmp.map! { |e| [e, @candidate_count - tmp.index(e)] }
41
- # find equal-weighted candidates
42
- tmp.map do |e|
43
- if e[0].size > 1
44
- e[0].split(/,/).each do |f|
45
- tmp2 << [f, e[1]]
46
- end # each
47
- else
48
- tmp2 << e
49
- end # if
50
- end # tmp.map
42
+ insert_vote_array vote_array
43
+ end
51
44
 
52
- vote_array << (tmp2.sort.map { |e| e = e[1] }) # order, strip & add
53
- end # vcount.times
54
- end # vs.split.each
45
+ def extract_vote_string(tmp) # array of preferences [['1, 2'], ['3']. ['4, 5']]
46
+ tmp2 = []
47
+ order_and_remap(tmp).
48
+ map do |e| # find equal-weighted candidates
49
+ if e[0].size > 1
50
+ e[0].split(/,/).each { |f| tmp2 << [f, e[1]] }
51
+ else
52
+ tmp2 << e
53
+ end
54
+ end
55
+ tmp2.sort.map { |e| e[1] } # order, strip & add
56
+ end
55
57
 
56
- insert_vote_array vote_array
58
+ def order_and_remap(tmp)
59
+ tmp.map { |e| [e, @candidate_count - tmp.index(e)] }
57
60
  end
58
61
 
59
62
  def insert_vote_file(vf)
60
63
  vf.rewind
61
64
  @candidate_count = vf.first.strip.to_i # reads first line for count
62
65
  @vote_matrix = ::Matrix.scalar(@candidate_count, 0).extend(Vote::Matrix)
63
- insert_vote_string vf.read # reads rest of file (w/o line 1)
66
+ insert_vote_strings vf.read # reads rest of file (w/o line 1)
64
67
  vf.close
65
68
  end
66
69
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schulze-vote
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi
@@ -26,6 +26,7 @@ files:
26
26
  - lib/vote/condorcet.rb
27
27
  - lib/vote/condorcet/schulze.rb
28
28
  - lib/vote/condorcet/schulze/basic.rb
29
+ - lib/vote/condorcet/schulze/classifications.rb
29
30
  - lib/vote/condorcet/schulze/input.rb
30
31
  - lib/vote/matrix.rb
31
32
  homepage: http://github.com/coorasse/schulze-vote
@@ -48,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
48
49
  version: '0'
49
50
  requirements: []
50
51
  rubyforge_project:
51
- rubygems_version: 2.2.0
52
+ rubygems_version: 2.5.1
52
53
  signing_key:
53
54
  specification_version: 4
54
55
  summary: Schulze method implementation in Ruby (Condorcet voting method)