File Coverage

blib/lib/Voting/Condorcet/RankedPairs.pm
Criterion Covered Total %
statement 97 97 100.0
branch 28 28 100.0
condition 3 3 100.0
subroutine 21 21 100.0
pod 10 10 100.0
total 159 159 100.0


line stmt bran cond sub pod time code
1             package Voting::Condorcet::RankedPairs;
2              
3 4     4   64030 use strict;
  4         10  
  4         236  
4 4     4   23 use warnings;
  4         8  
  4         248  
5 4     4   5477 use Graph;
  4         525814  
  4         146  
6 4     4   30 use Carp qw(croak);
  4         7  
  4         281  
7              
8             our $VERSION = '1.01';
9              
10             # Our majorities are in positon 2 of our stored pairs array.
11 4     4   19 use constant INDEX_MAJORITY => 2;
  4         8  
  4         226  
12              
13 4     4   21 use constant RANGE_MIN => 0;
  4         6  
  4         143  
14 4     4   17 use constant RANGE_MAX => 1;
  4         8  
  4         219  
15 4     4   18 use constant HALF_RANGE => (RANGE_MIN + RANGE_MAX) / 2;
  4         8  
  4         4026  
16              
17             =head1 NAME
18              
19             Voting::Condorcet::RankedPairs - Ranked Pairs voting resolution.
20              
21             =head1 SYNOPSIS
22              
23             use Voting::Condorcet::RankedPairs;
24              
25             my $rp = Voting::Condorcet::RankedPairs->new();
26              
27             $rp->add('Alice', 'Bob', 0.7); # Alice got 70% votes, Bob 30%
28             $rp->add('Alice', 'Eve', 0.4); # Alice got 40% votes, Eve 60%
29              
30             my $winner = $rp->winner; # The winner, ignores ties.
31             my @winners = $rp->strict_winners; # All winners, allows ties.
32              
33             my @rankings = $rp->rankings; # All entries, best to worst.
34             my @rankings2 = $rp->strict_rankings; # All entries, allowing ties.
35              
36             my @better = $rp->better_than('Alice'); # Entries significantly better
37             # than Alice.
38              
39             my @worse = $rp->worse_than('Alice'); # Entries significantly worse
40             # than Alice.
41              
42             my $graph = $rp->graph; # Underlying Graph object used.
43             # (Advanced users only)
44              
45             $rp->compile; # Force graph compilation.
46             # (Advanced users only)
47              
48             =head1 DESCRIPTION
49              
50             This module implements a I Condorcet voting system,
51             as described at L.
52              
53             Ranked pairs uses a directed graph to determine the winner and
54             rankings from a series of pairwise comparisons.
55              
56             =cut
57              
58             my %DEFAULTS = (
59             ordered_input => 0,
60             );
61              
62             =head2 new
63              
64             my $rp = Voting::Condorcet::RankedPairs->new();
65             my $rp2 = Voting::Condorcet::RankedPairs->new(ordered_input => 1);
66              
67             This method creates a new Ranked Pairs object. The C
68             option, if set, allows the module to perform a number of time and
69             space optimisations, but requires that data be added in strict
70             most-significant to least-significant order.
71              
72             =cut
73              
74             sub new {
75 6     6 1 2397 my ($class, @args) = @_;
76              
77 6         23 my $this = bless({},$class);
78              
79 6         27 $this->_init(@args);
80              
81 6         16 return $this;
82             }
83              
84             sub _init {
85 6     6   14 my $this = shift;
86 6         31 my %args = (%DEFAULTS,@_);
87              
88 6         29 $this->{ordered_input} = $args{ordered_input};
89 6         47 $this->{graph} = Graph->new;
90 6         1476 $this->{pairs} = [];
91 6         16 $this->{max_dist} = HALF_RANGE;
92              
93 6         13 return $this;
94             }
95              
96             =head2 add
97              
98             $rp->add('Alice','Bob',0.7); # Alice vs Bob, Alice gets 70% votes
99             $rp->add('Bob','Eve',0.4); # Bob vs Eve, Bob gets only 40% votes
100              
101             This method adds the results of a pairwise contest. It always
102             takes exactly three arguments: the two contestants, and a fractional
103             number between 0 and 1 indicating the number of votes in favour
104             of the first contestant.
105              
106             A score of 0.5 indicates a tie, a score of 1.00 would indicate all
107             votes fell to the first contestant, and a score of 0.00 would indicate
108             all votes fell to the second.
109              
110             If C was set when the object was created, then
111             contests must be added in order of most relevance (scores furthest
112             from 0.50) to least relevance (scores closest to 0.50). Adding
113             scores out of order when C is set will result in
114             an exception.
115              
116             Scores of exactly 0.5 result in the contestants being added to
117             the graph, but no edge being drawn.
118              
119             =cut
120              
121             sub add {
122 22     22 1 1567 my ($this, $winner, $loser, $result) = @_;
123              
124 22 100       60 if (@_ != 4) {
125 1         171 croak("add() must be given two nodes and a result (received:".join(",",@_));
126             }
127              
128 21 100 100     118 if ($result < RANGE_MIN or $result > RANGE_MAX) {
129 2         184 croak "add() must be given a fractional result between 0 and 1. Received $result";
130             }
131              
132             # If it's an exact draw, then add the nodes to the graph,
133             # but no edge. We can do this immediately.
134              
135 19 100       41 if ($result == HALF_RANGE) {
136 3         7 $this->_graph->add_vertex($winner)->add_vertex($loser);
137 3         132 return;
138             }
139              
140             # We assume that $winner beats $loser. Swap them
141             # around if this is not currently correct.
142            
143 16 100       38 ($winner,$loser) = ($loser,$winner) if ($result < HALF_RANGE);
144              
145 16 100       61 if ($this->{ordered_input}) {
146              
147             # Results must be fed to us in most-significant
148             # to least significant order. We check that here.
149             # If we're given results out of order, then an
150             # exception is thrown.
151              
152 5         12 my $distance = abs(HALF_RANGE - $result);
153 5         10 my $max_dist = $this->{max_dist};
154 5 100       10 if ($distance > $max_dist) {
155 1         202 croak "Out of order pair detected in ordered_input mode. ($winner,$loser) has a majority of $distance, where it must be less than $max_dist";
156             }
157 4         8 $this->{max_dist} = $distance;
158              
159 4         10 $this->_add($winner,$loser);
160             } else {
161 11         15 push(@{$this->{pairs}},[$winner,$loser,$result]);
  11         37  
162             }
163              
164 15         36 return;
165             }
166              
167             # This actually inserts an edge into our voting graph.
168             # It assumes it's being called with the correct arguments.
169              
170             sub _add {
171 13     13   23 my ($this,$winner,$loser) = @_;
172              
173 13         30 my $graph = $this->_graph;
174              
175 13         43 $graph->add_edge($winner,$loser);
176              
177             # If we just made a cycle, then reverse our action.
178 13 100       973 if ($graph->is_cyclic) {
179 2         6613 $graph->delete_edge($winner,$loser);
180             }
181              
182 13         14591 return;
183             }
184              
185             =head2 winner
186              
187             my $winner = $rp->winner;
188              
189             This returns the 'winner' of the competition. This always returns
190             a single result, and does not check for draws. Use L
191             (below) if a draw may exist.
192              
193             =cut
194              
195             sub winner {
196 4 100   4 1 286 croak "Useless call to winner in void context" if not defined wantarray;
197 3         14 return ($_[0]->strict_winners)[0];
198             }
199              
200             =head2 strict_winners
201              
202             my @winners = $rp->strict_winners;
203              
204             In some cirumstances two or more entries can be considered a draw. This
205             method returns an array to all the winners of a contest. In
206             most circumstances this will be a single entry.
207              
208             =cut
209              
210             sub strict_winners {
211 5 100   5 1 313 croak "Useless call to strict_winners in void context" if not defined wantarray;
212 4         18 return $_[0]->graph->predecessorless_vertices;
213             }
214              
215             =head2 rankings
216              
217             my @results = $rp->rankings;
218              
219             This method returns an ordered list of contestents, with the winner in
220             position 0. Ties are ignored; if two or more entries are tied they
221             will be returned adjacent to each other, but in an indeterminate
222             sequence. Use L if tie detection is required.
223              
224             =cut
225              
226             sub rankings {
227 4     4 1 4664 my ($this) = @_;
228              
229 4 100       109 croak "Useless call to rankings in void context" if not defined wantarray;
230              
231 3         17 return map { @$_ } $this->strict_rankings;
  10         43  
232             }
233              
234             =head2 strict_rankings
235              
236             my @results = $rp->strict_rankings;
237              
238             This method returns an ordered list of lists. Each element contains
239             a reference to all contestants at that position. This will usually
240             be a single element, but may contain multiple entries in the case
241             of draws.
242              
243             =cut
244              
245             sub strict_rankings {
246 4     4 1 220 my ($this) = @_;
247              
248 4 100       83 croak "Useless call to strict_rankings in void context" if not defined wantarray;
249              
250             # Take a copy of the graph. Don't hurt our original.
251 3         14 my $graph = $this->graph->copy;
252 3         2493 my @rankings;
253              
254             # Iteratively find and remove the winners from the graph.
255              
256 3         13 while (my @contestants = $graph->predecessorless_vertices) {
257 10         1843 push(@rankings, \@contestants);
258 10         16 foreach my $vertex (@contestants) {
259 10         36 $graph->delete_vertex($vertex);
260             }
261             }
262              
263 3         249 return @rankings;
264             }
265              
266             =head2 better_than
267              
268             my @higher_ranked = $rp->better_than("Alice");
269              
270             This function returns all the nodes that I beat the
271             given node with significance. In terms of graphs, these are all
272             the nodes that have the given node as its destination (ie, its
273             predecessors).
274              
275             =cut
276              
277             sub better_than {
278 11 100   11 1 4245 croak "Useless call to better_than in void context" if not defined wantarray;
279 10         18 my ($this, $node) = @_;
280              
281 10         24 return $this->graph->predecessors($node);
282             }
283              
284             =head2 worse_than
285              
286             my @lower_ranked = $rp->worse_than("Alice");
287              
288             This function returns all nodes that are I beaten
289             by the given node. In terms of graphs, these are the
290             nodes successors.
291              
292             =cut
293              
294             sub worse_than {
295 7 100   7 1 4063 croak "Useless call to worse_than in void context" if not defined wantarray;
296 6         12 my ($this, $node) = @_;
297              
298 6         49 return $this->graph->successors($node);
299             }
300              
301             =head2 compile
302              
303             $rp->compile;
304              
305             This method will construct the underlying graph needed to find results.
306             This method has no effect if the object was created with
307             C set to true.
308              
309             Normally there is no need to call this method by hand. It is
310             automatically from any function that needs a compiled graph.
311              
312             =cut
313              
314             sub compile {
315 23     23 1 29 my ($this) = @_;
316              
317 23 100       39 return unless @{$this->{pairs}};
  23         94  
318              
319             # Sort our pairs from largest marjority to smallest majority.
320             # In this case, our majority is the distance from the center-point
321             # of our range (0.5 by default).
322              
323 12         50 my @pairs = sort {
324 2         13 abs($b->[INDEX_MAJORITY] - HALF_RANGE) <=>
325             abs($a->[INDEX_MAJORITY] - HALF_RANGE)
326 2         5 } @{$this->{pairs}};
327              
328 2         6 foreach my $pairwise (@pairs) {
329 9         26 $this->_add(@$pairwise);
330             }
331              
332 2         8 $this->{pairs} = [];
333            
334 2         10 return;
335             }
336              
337             =head2 graph
338              
339             my $graph = $rp->graph;
340              
341             Returns the underlying L object used. This isn't a copy of
342             the object, it I the object, so be careful if you plan on
343             making changes to it.
344              
345             =cut
346              
347             sub graph {
348 23     23 1 38 my ($this) = @_;
349              
350 23         56 $this->compile;
351              
352 23         86 return $this->_graph;
353             }
354              
355             # This fetches our graph without attempting a compile. It's required
356             # for operations such as _add that actually do the work of compiling
357             # the graph in the first place.
358              
359             sub _graph {
360 39     39   190 return $_[0]->{graph};
361             }
362              
363             1;
364             __END__