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   289 use strict;
  39         94  
  39         1256  
2 39     39   223 use warnings;
  39         78  
  39         1054  
3 39     39   745 use 5.024;
  39         134  
4              
5             package Vote::Count::RankCount;
6              
7 39     39   218 use feature qw /postderef signatures/;
  39         83  
  39         3847  
8 39     39   282 no warnings 'experimental';
  39         426  
  39         1996  
9 39     39   261 use List::Util qw( min max sum);
  39         108  
  39         3824  
10 39     39   18649 use Vote::Count::TextTableTiny qw/generate_table/;
  39         103  
  39         2853  
11 39     39   13991 use Sort::Hash;
  39         23091  
  39         63481  
12              
13             our $VERSION='2.00';
14              
15             =head1 NAME
16              
17             Vote::Count::RankCount
18              
19             =head1 VERSION 2.00
20              
21             =cut
22              
23             # ABSTRACT: RankCount object for Vote::Count. Toolkit for vote counting.
24              
25 652     652   1095 sub _RankResult ( $rawcount ) {
  652         969  
  652         996  
26 652         3272 my %rc = ( $rawcount->%* ); # destructive process needs to use a copy.
27 652         1491 my %ordered = ();
28 652         1006 my %byrank = ();
29 652         1061 my $pos = 0;
30 652         1176 my $maxpos = scalar( keys %rc );
31 652         1582 while ( 0 < scalar( keys %rc ) ) {
32 3330         4531 $pos++;
33 3330         6567 my @vrc = values %rc;
34 3330         6790 my $max = max @vrc;
35 3330         6902 for my $k ( keys %rc ) {
36 17908 100       30298 if ( $rc{$k} == $max ) {
37 4467         7426 $ordered{$k} = $pos;
38 4467         6467 delete $rc{$k};
39 4467 100       7877 if ( defined $byrank{$pos} ) {
40 1137         1426 push @{ $byrank{$pos} }, $k;
  1137         2533  
41             }
42             else {
43 3330         8383 $byrank{$pos} = [$k];
44             }
45             }
46             }
47             # uncoverable branch true
48 3330 50       9075 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 652         1796 for my $O ( keys %byrank ) {
59 3330         8041 $byrank{$O} = [ sort $byrank{$O}->@* ];
60             }
61 652         1215 my @top = @{ $byrank{1} };
  652         1702  
62 652         1121 my @bottom = @{ $byrank{$pos} };
  652         1421  
63 652 100       1716 my $tie = scalar(@top) > 1 ? 1 : 0;
64             return {
65 652         4303 '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 651     651 1 1870 sub Rank ( $class, $rawcount ) {
  651         1141  
  651         1266  
  651         981  
87 651         1716 my $I = _RankResult($rawcount);
88 651         8515 return bless $I, $class;
89             }
90              
91 1     1 0 6 sub new ( $class, $rawcount ) {
  1         3  
  1         2  
  1         2  
92 1         3 my $I = _RankResult($rawcount);
93 1         4 return bless $I, $class;
94             }
95              
96 14     14 1 629 sub newFromList ( @list ) {
  14         35  
  14         19  
97 14         24 shift @list;
98 14         20 my $pos = 0;
99             my $newobj = Vote::Count::RankCount->Rank({
100 14         26 map { $_ => --$pos } @list} );
  109         187  
101 14         38 $newobj->{'orderedlist'} = \@list;
102 14         70 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 438     438 1 18685 sub RawCount ( $I ) { return $I->{'rawcount'} }
  438         722  
  438         671  
  438         1400  
162 4     4 1 1769 sub HashWithOrder ( $I ) { return $I->{'ordered'} }
  4         8  
  4         6  
  4         13  
163 40     40 1 1198 sub HashByRank ( $I ) { return $I->{'byrank'} }
  40         70  
  40         53  
  40         138  
164 35     35 1 5727 sub ArrayTop ( $I ) { return $I->{'top'} }
  35         63  
  35         64  
  35         139  
165 194     194 1 334 sub ArrayBottom ( $I ) { return $I->{'bottom'} }
  194         303  
  194         276  
  194         897  
166 286     286 1 862 sub CountVotes ($I) { return sum( values $I->{'rawcount'}->%* ) }
  286         461  
  286         402  
  286         1714  
167              
168 11     11 1 651 sub OrderedList ($I) {
  11         19  
  11         15  
169 11 100       58 return $I->{'orderedlist'}->@* if defined $I->{'orderedlist'};
170 1         9 die "OrderedList may only be used if the RankCount object was created from an ordered list.\n";
171             }
172              
173 27     27 1 69 sub Leader ( $I ) {
  27         65  
  27         45  
174 27         104 my @leaders = $I->ArrayTop()->@*;
175 27         151 my %return = ( 'tie' => 0, 'winner' => '', 'tied' => [] );
176 27 100       108 if ( 1 == @leaders ) { $return{'winner'} = $leaders[0] }
  22 50       66  
177 5         9 elsif ( 1 < @leaders ) { $return{'tie'} = 1; $return{'tied'} = \@leaders }
  5         15  
178 0         0 else { die "Does not compute in sub RankCount->Leader\n" }
179 27         121 return \%return;
180             }
181              
182 315     315 1 42221 sub RankTable( $self ) {
  315         535  
  315         477  
183 315         941 my @rows = ( [ 'Rank', 'Choice', 'Votes' ] );
184 315         1243 my %rc = $self->{'rawcount'}->%*;
185 315         1143 my %byrank = $self->{'byrank'}->%*;
186 315         1543 for my $r ( sort { $a <=> $b } ( keys %byrank ) ) {
  1564         2980  
187 1184         2469 my @choice = sort $byrank{$r}->@*;
188 1184         1828 for my $choice (@choice) {
189 1436         2063 my $votes = $rc{$choice};
190 1436         2674 my @row = ( $r, $choice, $votes );
191 1436         3303 push @rows, ( \@row );
192             }
193             }
194 315         1180 return generate_table( rows => \@rows, style => 'markdown' ) . "\n";
195             }
196              
197 1     1 1 2 sub RankTableWeighted( $self, $votevalue ) {
  1         1  
  1         3  
  1         51  
198 1         6 my @rows = ( [ 'Rank', 'Choice', 'Votes', 'VoteValue' ] );
199 1         7 my %rc = $self->{'rawcount'}->%*;
200 1         7 my %byrank = $self->{'byrank'}->%*;
201 1         6 for my $r ( sort { $a <=> $b } ( keys %byrank ) ) {
  8         14  
202 5         13 my @choice = sort $byrank{$r}->@*;
203 5         9 for my $choice (@choice) {
204 8         11 my $votes = $rc{$choice};
205 8         38 my @row = ( $r, $choice, sprintf("%.2f", $votes/$votevalue), $votes );
206 8         23 push @rows, ( \@row );
207             }
208             }
209 1         8 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