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 +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
|
+
[![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
|
|
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)
|