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 +4 -4
- data/README.md +9 -0
- data/lib/schulze_vote.rb +1 -0
- data/lib/vote/condorcet/schulze.rb +1 -0
- data/lib/vote/condorcet/schulze/basic.rb +41 -144
- data/lib/vote/condorcet/schulze/classifications.rb +109 -0
- data/lib/vote/condorcet/schulze/input.rb +22 -19
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9866e914ebb502be11324a542dfb49de786a7408
|
4
|
+
data.tar.gz: 6ade5e377c2b4e1f01444fd110e535e54df71ac4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
[](https://travis-ci.org/coorasse/schulze-vote)
|
9
|
+
|
10
|
+
## Develop
|
11
|
+
|
12
|
+
[](https://travis-ci.org/coorasse/schulze-vote)
|
13
|
+
[](https://codeclimate.com/github/coorasse/schulze-vote)
|
14
|
+
[](https://codeclimate.com/github/coorasse/schulze-vote/coverage)
|
6
15
|
|
7
16
|
Wikipedia:
|
8
17
|
|
data/lib/schulze_vote.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
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
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
@
|
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
|
224
|
-
|
132
|
+
def tie_by_idx(idx)
|
133
|
+
@ties.find { |tie| tie.any? { |el| el == idx } }
|
225
134
|
end
|
226
135
|
|
227
|
-
def
|
228
|
-
|
229
|
-
|
230
|
-
|
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
|
-
|
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
|
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
|
-
|
38
|
+
vote_array << extract_vote_string(tmp)
|
39
|
+
end
|
40
|
+
end
|
39
41
|
|
40
|
-
|
41
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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)
|