schulze-vote 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)