File Coverage

blib/lib/Vote/Count/RankCount.pm
Criterion Covered Total %
statement 141 142 99.3
branch 12 14 92.8
condition n/a
subroutine 22 22 100.0
pod 12 13 92.3
total 187 191 98.4


line stmt bran cond sub pod time code
1 39     39   294 use strict;
  39         84  
  39         1302  
2 39     39   194 use warnings;
  39         75  
  39         996  
3 39     39   735 use 5.024;
  39         358  
4              
5             package Vote::Count::RankCount;
6              
7 39     39   205 use feature qw /postderef signatures/;
  39         79  
  39         3653  
8 39     39   256 no warnings 'experimental';
  39         75  
  39         1907  
9 39     39   473 use List::Util qw( min max sum);
  39         94  
  39         3461  
10 39     39   15390 use Vote::Count::TextTableTiny qw/generate_table/;
  39         118  
  39         2615  
11 39     39   13158 use Sort::Hash;
  39         20866  
  39         57465  
12              
13             our $VERSION='2.01';
14              
15             =head1 NAME
16              
17             Vote::Count::RankCount
18              
19             =head1 VERSION 2.01
20              
21             =cut
22              
23             # ABSTRACT: RankCount object for Vote::Count. Toolkit for vote counting.
24              
25 795     795   1245 sub _RankResult ( $rawcount ) {
  795         1177  
  795         1111  
26 795         3497 my %rc = ( $rawcount->%* ); # destructive process needs to use a copy.
27 795         1700 my %ordered = ();
28 795         1155 my %byrank = ();
29 795         1289 my $pos = 0;
30 795         1405 my $maxpos = scalar( keys %rc );
31 795         1978 while ( 0 < scalar( keys %rc ) ) {
32 3749         4631 $pos++;
33 3749         6934 my @vrc = values %rc;
34 3749         7164 my $max = max @vrc;
35 3749         7188 for my $k ( keys %rc ) {
36 19632 100       30276 if ( $rc{$k} == $max ) {
37 5213         7669 $ordered{$k} = $pos;
38 5213         6812 delete $rc{$k};
39 5213 100       8480 if ( defined $byrank{$pos} ) {
40 1464         1808 push @{ $byrank{$pos} }, $k;
  1464         2971  
41             }
42             else {
43 3749         8704 $byrank{$pos} = [$k];
44             }
45             }
46             }
47             # uncoverable branch true
48 3749 50       9249 die "Vote::Count::RankCount::Rank in infinite loop\n"
49             if $pos > $maxpos;
50             }
51             # %byrank[1] is arrayref of 1st position,
52             # $pos still has last position filled, %byrank{$pos} is the last place.
53             # sometimes byranks came in as var{byrank...} deref and reref fixes this
54             # although it would be better if I understood why it happened.
55             # It is useful to sort the arrays anyway, for display they would likely be
56             # sorted anyway. For testing it makes the element order predictable.
57              
58 795         2151 for my $O ( keys %byrank ) {
59 3749         8685 $byrank{$O} = [ sort $byrank{$O}->@* ];
60             }
61 795         1392 my @top = @{ $byrank{1} };
  795         1905  
62 795         1250 my @bottom = @{ $byrank{$pos} };
  795         1611  
63 795 100       1835 my $tie = scalar(@top) > 1 ? 1 : 0;
64             return {
65 795         5117 'rawcount' => $rawcount,
66             'ordered' => \%ordered,
67             'byrank' => \%byrank,
68             'top' => \@top,
69             'bottom' => \@bottom,
70             'tie' => $tie,
71             };
72             }
73              
74             =head2 Rank
75              
76             Takes a single argument of a hashref containing Choices as Keys and Votes as Values. Returns an Object. This method is also aliased as new.
77              
78             =head2 newFromList
79              
80             Takes an ordered list and returns a RankCount Object where the RawCount values are zero minus the position: Item 3 in the list will have -3 votes while Item 1 will have -1.
81              
82             my $ordered_rank_count = Vote::Count::RankCount->newFromList( @ordered_list );
83              
84             =cut
85              
86 794     794 1 1992 sub Rank ( $class, $rawcount ) {
  794         1276  
  794         1175  
  794         1011  
87 794         1906 my $I = _RankResult($rawcount);
88 794         8626 return bless $I, $class;
89             }
90              
91 1     1 0 6 sub new ( $class, $rawcount ) {
  1         2  
  1         1  
  1         2  
92 1         3 my $I = _RankResult($rawcount);
93 1         3 return bless $I, $class;
94             }
95              
96 39     39 1 659 sub newFromList ( @list ) {
  39         106  
  39         62  
97 39         62 shift @list;
98 39         126 my $pos = 0;
99             my $newobj = Vote::Count::RankCount->Rank({
100 39         82 map { $_ => --$pos } @list} );
  233         461  
101 39         122 $newobj->{'orderedlist'} = \@list;
102 39         198 return $newobj;
103             }
104              
105             =head2 Methods
106              
107             The following Methods are available from RankCount Objects.
108              
109             =head3 RawCount
110              
111             Returns the original HashRef used for Object Creation.
112              
113             =head3 HashWithOrder
114              
115             Returns a HashRef with the Choices as Keys and the position of the choice, the value for the Leader would be 1 and the Third Place Choice would be 3. If choices are tied they will share the same value for their position.
116              
117             =head3 HashByRank
118              
119             Returns a HashRef where the keys are numbers and the values an ArrayRef of the Choices in that position. The ArrayRefs are sorted alphanumerically.
120              
121             =head3 ArrayTop, ArrayBottom
122              
123             Returns an ArrayRef of the Choices in the Top or Bottom Positions.
124              
125             =head3 OrderedList
126              
127             Returns the array that was to create the RankCount object if it was created from a List. Returns an exception if the object was created from a HashRef, because RankCount does not deal with ties. Returning a list with ties resolved by randomness or a sort would not be correct.
128              
129             =head3 CountVotes
130              
131             Returns the number of votes in the RawCount. This is not the same as the votes in the BallotSet from which that was derived. For TopCount it is the number of non-exhausted ballots in the round that generated RawCount, for Approval and Boorda it is probably not useful.
132              
133             =head3 Leader
134              
135             Returns a HashRef with the keys tie, tied, winner where winner is the winner, tie is true or false and tied is an array ref of the choices in the tie.
136              
137             =head3 RankTable
138              
139             Generates a MarkDown formatted table.
140              
141             say $Election->TopCount->RankTable;
142              
143             | Rank | Choice | Votes |
144             |------|------------|-------|
145             | 1 | VANILLA | 7 |
146             | 2 | MINTCHIP | 5 |
147              
148             =head3 RankTableWeighted ($votevalue)
149              
150             Ranktable for use with weighted votes. Displays both the Vote Value and the Vote Total (rounded to two places). Requires Vote Value as an argument.
151              
152             say $WeightedElection->TopCount->RankTableWeighted( 100 );
153              
154             | Rank | Choice | Votes | VoteValue |
155             |:-----|:-----------|------:|----------:|
156             | 1 | VANILLA | 7.00 | 700 |
157             | 2 | MINTCHIP | 5.00 | 500 |
158              
159             =cut
160              
161 502     502 1 6792 sub RawCount ( $I ) { return $I->{'rawcount'} }
  502         835  
  502         690  
  502         1421  
162 51     51 1 1616 sub HashWithOrder ( $I ) { return $I->{'ordered'} }
  51         91  
  51         76  
  51         194  
163 58     58 1 1048 sub HashByRank ( $I ) { return $I->{'byrank'} }
  58         126  
  58         102  
  58         257  
164 35     35 1 5117 sub ArrayTop ( $I ) { return $I->{'top'} }
  35         56  
  35         54  
  35         110  
165 194     194 1 336 sub ArrayBottom ( $I ) { return $I->{'bottom'} }
  194         265  
  194         262  
  194         726  
166 318     318 1 993 sub CountVotes ($I) { return sum( values $I->{'rawcount'}->%* ) }
  318         510  
  318         493  
  318         1926  
167              
168 5     5 1 1018 sub OrderedList ($I) {
  5         8  
  5         7  
169 5 100       44 return $I->{'orderedlist'}->@* if defined $I->{'orderedlist'};
170 1         8 die "OrderedList may only be used if the RankCount object was created from an ordered list.\n";
171             }
172              
173 27     27 1 59 sub Leader ( $I ) {
  27         48  
  27         36  
174 27         81 my @leaders = $I->ArrayTop()->@*;
175 27         122 my %return = ( 'tie' => 0, 'winner' => '', 'tied' => [] );
176 27 100       106 if ( 1 == @leaders ) { $return{'winner'} = $leaders[0] }
  22 50       53  
177 5         11 elsif ( 1 < @leaders ) { $return{'tie'} = 1; $return{'tied'} = \@leaders }
  5         9  
178 0         0 else { die "Does not compute in sub RankCount->Leader\n" }
179 27         92 return \%return;
180             }
181              
182 395     395 1 33295 sub RankTable( $self ) {
  395         601  
  395         506  
183 395         1140 my @rows = ( [ 'Rank', 'Choice', 'Votes' ] );
184 395         1535 my %rc = $self->{'rawcount'}->%*;
185 395         1379 my %byrank = $self->{'byrank'}->%*;
186 395         1835 for my $r ( sort { $a <=> $b } ( keys %byrank ) ) {
  1659         2946  
187 1335         2742 my @choice = sort $byrank{$r}->@*;
188 1335         2095 for my $choice (@choice) {
189 1734         2441 my $votes = $rc{$choice};
190 1734         2953 my @row = ( $r, $choice, $votes );
191 1734         3559 push @rows, ( \@row );
192             }
193             }
194 395         1527 return generate_table( rows => \@rows, style => 'markdown' ) . "\n";
195             }
196              
197 1     1 1 1 sub RankTableWeighted( $self, $votevalue ) {
  1         2  
  1         1  
  1         2  
198 1         3 my @rows = ( [ 'Rank', 'Choice', 'Votes', 'VoteValue' ] );
199 1         5 my %rc = $self->{'rawcount'}->%*;
200 1         4 my %byrank = $self->{'byrank'}->%*;
201 1         4 for my $r ( sort { $a <=> $b } ( keys %byrank ) ) {
  7         11  
202 5         10 my @choice = sort $byrank{$r}->@*;
203 5         8 for my $choice (@choice) {
204 8         10 my $votes = $rc{$choice};
205 8         34 my @row = ( $r, $choice, sprintf("%.2f", $votes/$votevalue), $votes );
206 8         15 push @rows, ( \@row );
207             }
208             }
209 1         5 return generate_table(
210             rows => \@rows,
211             style => 'markdown',
212             align => [qw/ l l r r /]
213             ) . "\n";
214             }
215             1;
216              
217             #FOOTER
218              
219             =pod
220              
221             BUG TRACKER
222              
223             L<https://github.com/brainbuz/Vote-Count/issues>
224              
225             AUTHOR
226              
227             John Karr (BRAINBUZ) brainbuz@cpan.org
228              
229             CONTRIBUTORS
230              
231             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
232              
233             LICENSE
234              
235             This module is released under the GNU Public License Version 3. See license file for details. For more information on this license visit L<http://fsf.org>.
236              
237             SUPPORT
238              
239             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
240              
241             =cut
242