schulze-vote 2.0.3 → 2.0.4
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/lib/vote/condorcet.rb +6 -0
- data/lib/vote/condorcet/schulze.rb +8 -0
- data/lib/vote/condorcet/schulze/basic.rb +188 -0
- data/lib/vote/condorcet/schulze/input.rb +81 -0
- data/lib/vote/matrix.rb +12 -0
- metadata +6 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f1a89377c52c746f04598f5af4202130d6276e4
|
4
|
+
data.tar.gz: 55fab398da85310889bee1304c3b099d9a6a6554
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ee33a934d33321ea22e2f35bad75fa1c9b462b0b4e9c141b7e0709eabe8a3d547fba852526b70a9de395afa12fa55a30b3e4b60dc553b3aeaf95cb2e96a8fee
|
7
|
+
data.tar.gz: b7475ff5206ebd7f19166aaf89a4cb88c3e37bd7a2ae15e60d3b4b7b35bc91cab5da1e56541b7feba5b1aebf568c805bbf2e9864d2bd7bb0acfbf7912aa2e289
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Vote
|
2
|
+
module Condorcet
|
3
|
+
module Schulze
|
4
|
+
class Basic
|
5
|
+
# All-in-One class method to get a calculated SchulzeBasic object
|
6
|
+
def self.do(vote_matrix, candidate_count = nil)
|
7
|
+
instance = new
|
8
|
+
instance.load vote_matrix, candidate_count
|
9
|
+
instance.run
|
10
|
+
instance
|
11
|
+
end
|
12
|
+
|
13
|
+
def load(vote_matrix, candidate_count = nil)
|
14
|
+
input = if vote_matrix.is_a?(Vote::Condorcet::Schulze::Input)
|
15
|
+
vote_matrix
|
16
|
+
else
|
17
|
+
Vote::Condorcet::Schulze::Input.new(
|
18
|
+
vote_matrix,
|
19
|
+
candidate_count
|
20
|
+
)
|
21
|
+
end
|
22
|
+
@vote_matrix = input.matrix
|
23
|
+
@candidate_count = input.candidates
|
24
|
+
@vote_count = input.voters
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
play
|
30
|
+
result
|
31
|
+
calculate_winners
|
32
|
+
rank
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :vote_matrix
|
36
|
+
|
37
|
+
attr_reader :play_matrix
|
38
|
+
|
39
|
+
attr_reader :result_matrix
|
40
|
+
|
41
|
+
def ranks
|
42
|
+
@ranking
|
43
|
+
end
|
44
|
+
|
45
|
+
def voters
|
46
|
+
@vote_count
|
47
|
+
end
|
48
|
+
|
49
|
+
# return all possible solutions to the votation
|
50
|
+
attr_reader :winners_array
|
51
|
+
|
52
|
+
def classifications
|
53
|
+
@classifications ||= calculate_classifications
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def play
|
59
|
+
@play_matrix = ::Matrix.scalar(@candidate_count, 0).extend(Vote::Matrix)
|
60
|
+
# step 1: find matches with wins
|
61
|
+
@candidate_count.times do |i|
|
62
|
+
@candidate_count.times do |j|
|
63
|
+
next if i == j
|
64
|
+
if @vote_matrix[i, j] > @vote_matrix[j, i]
|
65
|
+
@play_matrix[i, j] = @vote_matrix[i, j]
|
66
|
+
else
|
67
|
+
@play_matrix[i, j] = 0
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# step 2: find strongest paths
|
73
|
+
@candidate_count.times do |i|
|
74
|
+
@candidate_count.times do |j|
|
75
|
+
next if i == j
|
76
|
+
@candidate_count.times do |k|
|
77
|
+
next if (i == k) || (j == k)
|
78
|
+
@play_matrix[j, k] = [
|
79
|
+
@play_matrix[j, k],
|
80
|
+
[@play_matrix[j, i], @play_matrix[i, k]].min
|
81
|
+
].max
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def result
|
88
|
+
@result_matrix = ::Matrix.scalar(@candidate_count, 0).extend(Vote::Matrix)
|
89
|
+
@result_matrix.each_with_index do |e, x, y|
|
90
|
+
next if x == y
|
91
|
+
@result_matrix[x, y] = e + 1 if @play_matrix[x, y] > @play_matrix[y, x]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def calculate_winners
|
96
|
+
@winners_array = Array.new(@candidate_count, 0)
|
97
|
+
@winners_array.each_with_index do |_el, idx|
|
98
|
+
row = @play_matrix.row(idx)
|
99
|
+
column = @play_matrix.column(idx)
|
100
|
+
if row.each_with_index.all? { |r, index| r >= column[index] }
|
101
|
+
@winners_array[idx] = 1
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def rank
|
107
|
+
@ranking = @result_matrix.
|
108
|
+
row_vectors.map { |e| e.inject(0) { |s, v| s += v } }
|
109
|
+
end
|
110
|
+
|
111
|
+
# you should call calculate_winners first
|
112
|
+
def calculate_potential_winners
|
113
|
+
@potential_winners = []
|
114
|
+
winners_array.each_with_index do |val, idx|
|
115
|
+
@potential_winners << idx if val > 0
|
116
|
+
end
|
117
|
+
@potential_winners
|
118
|
+
end
|
119
|
+
|
120
|
+
def calculate_beat_couples
|
121
|
+
@beat_couples = []
|
122
|
+
ranks.each_with_index do |_val, idx|
|
123
|
+
ranks.each_with_index do |_val2, idx2|
|
124
|
+
next if idx == idx2
|
125
|
+
if play_matrix[idx, idx2] > play_matrix[idx2, idx]
|
126
|
+
@beat_couples << [idx, idx2]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
@beat_couples
|
131
|
+
end
|
132
|
+
|
133
|
+
def rank_element(el)
|
134
|
+
rank = 0
|
135
|
+
rank -= 100 if @potential_winners.include?(el)
|
136
|
+
@beat_couples.each do |b|
|
137
|
+
rank -= 1 if b[0] == el
|
138
|
+
end
|
139
|
+
rank
|
140
|
+
end
|
141
|
+
|
142
|
+
def calculate_classifications
|
143
|
+
calculate_potential_winners
|
144
|
+
calculate_beat_couples
|
145
|
+
|
146
|
+
start_list = (0..ranks.length - 1).to_a
|
147
|
+
start_list.sort! { |e1, e2| rank_element(e1) <=> rank_element(e2) }
|
148
|
+
|
149
|
+
classifications = []
|
150
|
+
compute_classifications(classifications, [], @potential_winners, @beat_couples, start_list)
|
151
|
+
classifications
|
152
|
+
end
|
153
|
+
|
154
|
+
def compute_classifications(classifications, classif = [], potential_winners, beated_list, start_list)
|
155
|
+
if beated_list.empty?
|
156
|
+
start_list.permutation.each do |array|
|
157
|
+
classifications << classif + array
|
158
|
+
end
|
159
|
+
else
|
160
|
+
if classif.empty? && potential_winners.any?
|
161
|
+
potential_winners.each do |element|
|
162
|
+
add_element(classifications, classif, nil, beated_list, start_list, element)
|
163
|
+
end
|
164
|
+
else
|
165
|
+
start_list.each do |element|
|
166
|
+
add_element(classifications, classif, nil, beated_list, start_list, element)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def add_element(classifications, classif, _potential_winners, beated_list, start_list, element)
|
173
|
+
return if beated_list.any? { |c| c[1] == element }
|
174
|
+
classification = classif.clone
|
175
|
+
classification << element
|
176
|
+
next_beated_list = beated_list.clone.delete_if { |c| c[0] == element }
|
177
|
+
next_start_list = start_list.clone
|
178
|
+
next_start_list.delete(element)
|
179
|
+
if next_start_list.empty?
|
180
|
+
classifications << classification
|
181
|
+
else
|
182
|
+
compute_classifications(classifications, classification, nil, next_beated_list, next_start_list)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Vote
|
2
|
+
module Condorcet
|
3
|
+
module Schulze
|
4
|
+
class Input
|
5
|
+
def initialize(vote_list, candidate_count = nil)
|
6
|
+
@vote_list = vote_list
|
7
|
+
@candidate_count = candidate_count
|
8
|
+
|
9
|
+
if @candidate_count.nil?
|
10
|
+
insert_vote_file(@vote_list) if vote_list.is_a?(File)
|
11
|
+
|
12
|
+
else
|
13
|
+
@vote_matrix = ::Matrix.scalar(@candidate_count, 0).extend(Vote::Matrix)
|
14
|
+
insert_vote_array(@vote_list) if vote_list.is_a?(Array)
|
15
|
+
insert_vote_string(@vote_list) if vote_list.is_a?(String)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def insert_vote_array(va)
|
20
|
+
va.each do |vote|
|
21
|
+
@vote_matrix.each_with_index do |_e, x, y|
|
22
|
+
next if x == y
|
23
|
+
@vote_matrix[x, y] += 1 if vote[x] > vote[y]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
@vote_count = va.size
|
27
|
+
end
|
28
|
+
|
29
|
+
def insert_vote_string(vs)
|
30
|
+
vote_array = []
|
31
|
+
|
32
|
+
vs.split(/\n|\n\r|\r/).each do |voter|
|
33
|
+
voter = voter.split(/=/)
|
34
|
+
vcount = (voter.size == 1) ? 1 : voter[0].to_i
|
35
|
+
|
36
|
+
vcount.times do
|
37
|
+
tmp = voter.last.split(/;/)
|
38
|
+
tmp2 = []
|
39
|
+
|
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
|
51
|
+
|
52
|
+
vote_array << (tmp2.sort.map { |e| e = e[1] }) # order, strip & add
|
53
|
+
end # vcount.times
|
54
|
+
end # vs.split.each
|
55
|
+
|
56
|
+
insert_vote_array vote_array
|
57
|
+
end
|
58
|
+
|
59
|
+
def insert_vote_file(vf)
|
60
|
+
vf.rewind
|
61
|
+
@candidate_count = vf.first.strip.to_i # reads first line for count
|
62
|
+
@vote_matrix = ::Matrix.scalar(@candidate_count, 0).extend(Vote::Matrix)
|
63
|
+
insert_vote_string vf.read # reads rest of file (w/o line 1)
|
64
|
+
vf.close
|
65
|
+
end
|
66
|
+
|
67
|
+
def matrix
|
68
|
+
@vote_matrix
|
69
|
+
end
|
70
|
+
|
71
|
+
def candidates
|
72
|
+
@candidate_count
|
73
|
+
end
|
74
|
+
|
75
|
+
def voters
|
76
|
+
@vote_count
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/vote/matrix.rb
ADDED
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.0.
|
4
|
+
version: 2.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alessandro Rodi
|
@@ -23,6 +23,11 @@ files:
|
|
23
23
|
- README.md
|
24
24
|
- lib/schulze_vote.rb
|
25
25
|
- lib/vote.rb
|
26
|
+
- lib/vote/condorcet.rb
|
27
|
+
- lib/vote/condorcet/schulze.rb
|
28
|
+
- lib/vote/condorcet/schulze/basic.rb
|
29
|
+
- lib/vote/condorcet/schulze/input.rb
|
30
|
+
- lib/vote/matrix.rb
|
26
31
|
homepage: http://github.com/coorasse/schulze-vote
|
27
32
|
licenses:
|
28
33
|
- MIT
|