File Coverage

blib/lib/Vote/Count/Borda.pm
Criterion Covered Total %
statement 88 88 100.0
branch 16 16 100.0
condition n/a
subroutine 17 18 94.4
pod 1 2 50.0
total 122 124 98.3


line stmt bran cond sub pod time code
1 39     39   19989 use strict;
  39         92  
  39         1211  
2 39     39   203 use warnings;
  39         84  
  39         904  
3 39     39   918 use 5.024;
  39         148  
4 39     39   206 use feature qw /postderef signatures/;
  39         76  
  39         3729  
5              
6              
7             use Moose::Role;
8 39     39   281  
  39         89  
  39         279  
9             our $VERSION='2.02';
10              
11             =head1 NAME
12              
13             Vote::Count::Borda
14              
15             =head1 VERSION 2.02
16              
17             =cut
18              
19             # ABSTRACT: Provides Borda Count to Vote::Count objects
20              
21             no warnings 'experimental';
22 39     39   205328 use List::Util qw( min max );
  39         95  
  39         1702  
23 39     39   240 use Vote::Count::RankCount;
  39         87  
  39         2800  
24 39     39   279 use Try::Tiny;
  39         114  
  39         1204  
25 39     39   256 use Data::Dumper;
  39         92  
  39         2748  
26 39     39   264  
  39         78  
  39         44209  
27             has 'bordaweight' => (
28             is => 'rw',
29             isa => 'CodeRef',
30             builder => '_buildbordaweight',
31             lazy => 1,
32             );
33              
34             has 'bordadepth' => (
35             is => 'rw',
36             isa => 'Int',
37             default => 0,
38             );
39              
40             # Many real world Borda implmentations use 1
41             # for unranked default. The way unranked choices are valued
42             # relies on NonApproval (from Approval), which does not
43             # support overriding the Active Set. Because this is a low
44             # priority function the limitation is acceptable.
45             has 'unrankdefault' => (
46             is => 'rw',
47             isa => 'Int',
48             default => 0,
49             );
50              
51             =pod
52              
53             =head1 Synopsis
54              
55             my $RCV = Vote::Count->new(
56             BallotSet => read_ballots('t/data/data1.txt'),
57             bordadepth => 5
58             );
59             my $bordacount = $RCV->Borda();
60              
61             =head1 Borda Count
62              
63             Scores Choices based on their position on the Ballot. The first choice candidate gets a score equal to the number of choices, each lower choice receives 1 less.
64              
65             The Borda Count is trying to Cardinally value Preferential choices, for this reason where the Borda Count is an appropriate method it is a better to use a Range Ballot instead of Preferential so that the voters may assign the Cardinal values.
66              
67             =head1 Variations on the Borda Count
68              
69             One major criticism of the count is that when there are many choices the difference between a first and second choice becomes negligible. A large number of alternative weightings have been used to address this.
70              
71             =head2 Borda Depth (bordadepth parameter)
72              
73             One of the simpler variations is to fix the depth, when the depth is set to a certain number the weighting is as if the ballot had that many choices, and choices ranked lower than the depth are scored 0. If there are eight choices and a depth of 3, a first choice is worth 3, a 3rd 1, and later choices are ignored
74              
75             =head2 Borda Weight (bordaweight parameter)
76              
77             Some of the popular alternate weighting systems include:
78              
79             =over
80              
81             =item * different scaling such as 1/x where x is the position of the choice (1 is worth 1, 3 is 1/3).
82              
83             =item * Another popular alternative is to score for one less than the number of choices -- in a five choice race first is worth 4 and last is worth 0.
84              
85             =back
86              
87             When Creating a VoteCount object a custom Borda weight may be set by passing a coderef for bordaweight. The coderef takes two arguments. The first argument is the position of the choice in question. The second argument is optional for passing the depth of the ballot to the coderef. Some popular options such inversion (where choice $c becomes $c/1 then inverted to 1/$c) don't need to know the depth. In such cases the coderef should just ignore the second argument.
88              
89             my $testweight = sub {
90             my $x = int shift @_;
91             return $x ? 1/$x : 0 ;
92             };
93              
94             my $VC2 = Vote::Count->new(
95             BallotSet => read_ballots('t/data/data2.txt'),
96             bordaweight => $testweight,
97             );
98              
99             =head2 unrankdefault
100              
101             Jean-Charles de Borda expected voters to rank all available choices. When they fail to do this the unranked choices need to be handled. The default in Vote::Count is to score unranked choices as 0. However, it is also common to score them as 1. Vote::Count permits using any Integer for this valuation.
102              
103             my $VC2 = Vote::Count->new(
104             BallotSet => read_ballots('t/data/data2.txt'),
105             unrankdefault => 1,
106             );
107              
108             =head1 Method Borda
109              
110             Returns a RankCount Object with the scores per the weighting rule, for Ranked Choice Ballots. Optional Parameter is a hashref defining an active set.
111              
112             =cut
113              
114             return sub {
115             my ( $x, $y ) = @_;
116             return ( $y + 1 - $x );
117 217     217   314 }
118 217         477 }
119              
120 11     11   349 # Private Method _bordashrinkballot( $BallotSet, $active )
121              
122             # Takes a BallotSet and active list and returns a
123             # BallotSet reduced to only the active choices. When
124             # choices are removed later choices are promoted.
125              
126             my $newballots = {};
127             my %ballots = $BallotSet->{'ballots'}->%*;
128 25     25   43 for my $b ( keys %ballots ) {
  25         45  
  25         33  
  25         41  
129 25         57 my @newballot = ();
130 25         129 for my $item ( $ballots{$b}{'votes'}->@* ) {
131 25         93 try { if ( $active->{$item} ) { push @newballot, $item } }
132 174         354 catch {};
133 174         351 }
134 408 100   408   17554 if ( scalar(@newballot) ) {
  310         660  
135 408     0   3782 $newballots->{$b}{'votes'} = \@newballot;
136             $newballots->{$b}{'count'} =
137 174 100       1950 $ballots{$b}->{'count'};
138 162         413 }
139             }
140 162         453 return $newballots;
141             }
142              
143 25         172 my $BordaCount = {};
144             my $weight = $self->bordaweight;
145             my $depth =
146 24     24   43 $self->bordadepth
  24         43  
  24         36  
  24         43  
  24         34  
147 24         42 ? $self->bordadepth
148 24         794 : scalar( keys %{$active} );
149             for my $c ( keys $BordaTable->%* ) {
150             for my $rank ( keys $BordaTable->{$c}->%* ) {
151             $BordaCount->{$c} = 0 unless defined $BordaCount->{$c};
152 24 100       618 $BordaCount->{$c} +=
  23         62  
153 24         88 $BordaTable->{$c}{$rank} * $weight->( $rank, $depth );
154 148         400 }
155 235 100       542 }
156             return $BordaCount;
157 235         421 }
158              
159             my %BallotSet = $self->BallotSet()->%*;
160 24         76 my %ballots = ();
161             if ( defined $active ) {
162             die q/unrankdefault other than 0 is not compatible with overriding the
163 24     24 1 639 Active Set. To fix this use the SetActive method to update the active
  24         43  
  24         43  
  24         38  
164 24         664 set, then call this (Borda) method without passing an active set./
165 24         82 if $self->unrankdefault();
166 24 100       68 }
167 21 100       595 $active = $self->Active() unless defined $active;
168             %ballots = %{ _bordashrinkballot( \%BallotSet, $active ) };
169             my %BordaTable = ( map { $_ => {} } keys( $active->%* ) );
170             BORDALOOPACTIVE:
171             for my $b ( keys %ballots ) {
172 23 100       136 my @votes = $ballots{$b}->{'votes'}->@* ;
173 23         40 my $bcount = $ballots{$b}->{'count'};
  23         71  
174 23         112 for ( my $i = 0 ; $i < scalar(@votes) ; $i++ ) {
  145         290  
175             my $c = $votes[$i];
176 23         86 $BordaTable{$c}->{ $i + 1 } += $bcount;
177 155         306 }
178 155         237 }
179 155         308 my $BordaCounted = _dobordacount( $self, \%BordaTable, $active );
180 302         403 if ( $self->unrankdefault() ) {
181 302         815 my $unranked = $self->NonApproval()->RawCount();
182             for my $u ( keys $unranked->%* ) {
183             $BordaCounted->{$u} += $unranked->{$u} * $self->unrankdefault()
184 23         109 }
185 23 100       636 }
186 2         10 return Vote::Count::RankCount->Rank($BordaCounted);
187 2         14 }
188 16         386  
189              
190             1;
191 23         120  
192             #FOOTER
193              
194 3     3 0 12 =pod
195              
196             BUG TRACKER
197              
198             L<https://github.com/brainbuz/Vote-Count/issues>
199              
200             AUTHOR
201              
202             John Karr (BRAINBUZ) brainbuz@cpan.org
203              
204             CONTRIBUTORS
205              
206             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
207              
208             LICENSE
209              
210             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>.
211              
212             SUPPORT
213              
214             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
215              
216             =cut
217