File Coverage

blib/lib/Vote/Count/Floor.pm
Criterion Covered Total %
statement 102 102 100.0
branch 27 28 96.4
condition n/a
subroutine 14 14 100.0
pod 4 4 100.0
total 147 148 99.3


line stmt bran cond sub pod time code
1 39     39   24198 use strict;
  39         106  
  39         1272  
2 39     39   289 use warnings;
  39         91  
  39         1343  
3 39     39   621 use 5.024;
  39         144  
4 39     39   301 use feature qw /postderef signatures/;
  39         90  
  39         3325  
5              
6             package Vote::Count::Floor;
7 39     39   282 use namespace::autoclean;
  39         79  
  39         352  
8 39     39   3739 use Moose::Role;
  39         85  
  39         380  
9             # use Data::Dumper;
10              
11 39     39   210745 no warnings 'experimental';
  39         119  
  39         49223  
12              
13             our $VERSION='2.00';
14              
15             =head1 NAME
16              
17             Vote::Count::Floor
18              
19             =head1 VERSION 2.00
20              
21             =cut
22              
23             # ABSTRACT: Floor Rules for RCV elections.
24              
25             # load the roles providing the underlying ops.
26             with 'Vote::Count::Approval', 'Vote::Count::TopCount',;
27              
28             has 'FloorRounding' => (
29             is => 'rw',
30             isa => 'Str',
31             default => 'up'
32             );
33              
34 32     32   110 sub _FloorRnd ( $I, $num ) {
  32         50  
  32         50  
  32         52  
35 32 100       934 if ( $I->FloorRounding eq 'down' ) {
    100          
    100          
    100          
36 7         59 return int($num);
37             }
38             elsif ( $I->FloorRounding eq 'up' ) {
39 17 100       82 return int($num) if ( $num == int($num) );
40 10         57 return int( $num + 1 );
41             }
42             elsif ( $I->FloorRounding eq 'round' ) {
43 4         24 return int( $num + 0.5 );
44             }
45             elsif ( $I->FloorRounding eq 'nextint' ) {
46 3         18 return int( $num + 1 );
47             }
48 1         28 else { die 'unknown FloorRounding method requested: ' . $I->FloorRounding }
49             }
50              
51 15     15   29 sub _FloorMin ( $I, $floorpct ) {
  15         24  
  15         29  
  15         20  
52 15 100       72 my $pct = $floorpct >= 1 ? $floorpct / 100 : $floorpct;
53             # warn "floorpct = $floorpct $pct cast = ${\ $I->VotesCast() }";
54             # warn "FloorMin = ${\ $I->_FloorRnd( $I->VotesCast() * $pct )}";
55 15         52 return $I->_FloorRnd( $I->VotesCast() * $pct );
56             }
57              
58 17     17   81 sub _DoFloor ( $I, $ranked, $cutoff ) {
  17         29  
  17         28  
  17         34  
  17         24  
59 17         38 my @active = ();
60 17         34 my @remove = ();
61 17         73 for my $s ( keys $ranked->%* ) {
62 179 100       341 if ( $ranked->{$s} >= $cutoff ) { push @active, $s }
  98         173  
63             else {
64 81         117 push @remove, $s;
65 81         317 $I->logv("Removing: $s: $ranked->{$s}, minimum is $cutoff.");
66             }
67             }
68 17 100       62 if (@remove) {
69 16         110 $I->logt(
70             "Floor Rule Eliminated: ",
71             join( ', ', @remove ),
72             "Remaining: ", join( ', ', @active ),
73             );
74             }
75             else {
76 1         7 $I->logt('None Eliminated');
77             }
78 17         60 return { map { $_ => 1 } @active };
  98         290  
79             }
80              
81             # Approval Floor is Approval votes vs total
82             # votes cast -- not total of approval votes.
83 8     8 1 54 sub ApprovalFloor ( $self, $floorpct = 5, $rangecutoff = 0 ) {
  8         18  
  8         16  
  8         16  
  8         12  
84 8         38 my $votescast = $self->VotesCast();
85 8         76 $self->logt( "Applying Floor Rule of $floorpct\% "
86             . "Approval Count. vs Ballots Cast of $votescast." );
87             my $raw =
88             $self->BallotSetType() eq 'rcv'
89 8 100       36 ? do { $self->Approval(); $self->LastApprovalBallots() }
  5         34  
  5         171  
90             : $self->Approval( undef, $rangecutoff )->RawCount();
91 8         74 return $self->_DoFloor( $raw, $self->_FloorMin($floorpct) );
92             }
93              
94 4     4 1 1826 sub TopCountFloor ( $self, $floorpct = 2 ) {
  4         9  
  4         9  
  4         7  
95 4         48 $self->logt("Applying Floor Rule of $floorpct\% First Choice Votes.");
96             my $raw =
97             $self->BallotSetType() eq 'rcv'
98 4 50       29 ? do { $self->TopCount(); $self->LastTopCountUnWeighted() }
  4         36  
  4         136  
99             : $self->TopCount();
100 4         24 return $self->_DoFloor( $raw, $self->_FloorMin($floorpct) );
101             }
102              
103 6     6 1 1130 sub TCA ( $self, $floor = .5 ) {
  6         18  
  6         12  
  6         13  
104 6 100       31 if ( $floor > 1 ) {
105 1         9 my $m = "Floor value $floor is greater than 1";
106 1         7 $self->logt($m);
107 1         8 die "$m\n";
108             }
109             $self->logt(
110 5         69 'Applying Floor Rule: Approval Must at least ',
111             "$floor times the Most First Choice votes. "
112             );
113 5         26 my $tc = $self->TopCount();
114             # arraytop returns a list in case of tie.
115 5         21 my $winner = shift( $tc->ArrayTop->@* );
116 5         18 my $tcraw = $tc->RawCount()->{$winner};
117 5         52 my $cutoff = $self->_FloorRnd( $tcraw * $floor );
118 5         39 $self->logv( "The most first choice votes for any choice is $tcraw.",
119             "Cutoff will be $cutoff" );
120 5         34 return $self->_DoFloor( $self->Approval()->RawCount(), $cutoff );
121             }
122              
123 6     6 1 106 sub ApplyFloor ( $self, $rule, @args ) {
  6         16  
  6         10  
  6         11  
  6         8  
124 6         14 my $newset = {};
125 6 100       37 if ( $rule eq 'ApprovalFloor' ) {
    100          
    100          
126 2         19 $newset = $self->ApprovalFloor(@args);
127             }
128             elsif ( $rule eq 'TopCountFloor' ) {
129 2         16 $newset = $self->TopCountFloor(@args);
130             }
131             elsif ( $rule eq 'TCA' ) {
132 1         4 $newset = $self->TCA(@args);
133             }
134             else {
135 1         10 die "Bad rule provided to ApplyFloor, $rule";
136             }
137 5         38 $self->SetActive($newset);
138 5         36 return $newset;
139             }
140              
141             =head1 Floor Rules
142              
143             In real elections it is common to have choices with very little support, with write-ins there can be a large number of these choices, with iterative dropping like IRV it can take many rounds to work through them. A Floor Rule sets a criteria to remove the weakly supported choices early in a single operation.
144              
145             =head1 SYNOPSIS
146              
147             my $Election = Vote::Count->new( BallotSet => $someballotset );
148             my $ChoicesAfterFloor = $Election->ApprovalFloor();
149             $Election->SetActive( $ChoicesAfterFloor ); # To apply the floor
150             $Election->ApplyFloor( 'TopCountFloor', @options ); # One Step
151              
152             =head1 Rounding
153              
154             The default rounding is up. If a calculated cutoff is 11.2, the cutoff will become greater than or equal to 12. Set FloorRounding to 'down' to change this to round down for 11.9 to become 11. Set FloorRounding to 'round' to change this to round .5 or greater up. If the comparison needs to be Greater than, a FloorRounding of 'nextint' will use the next higher integer.
155              
156             # When creating the Election.
157             my $Election = Vote::Count->new( FloorRounding => 'round', ... );
158             # Before applying the floor.
159             $Election->FloorRounding( 'down');
160              
161             =head1 The Floor Methods
162              
163             All Methods in this Module apply a floor rule, log the eliminations and return the set of remaining choices as a HashRef. Use the ApplyFloor Method to immediately apply the results.
164              
165             =head2 ApplyFloor
166              
167             Takes as an argument the Method Name as a string of the rule to apply ( ApprovalFloor, TopCountFloor, TCA), followed by any optional arguments for the rule. Sets the Active Set as defined by that rule and returns the new Active Set as a hashref.
168              
169             # Apply a TopCount Floor of 10%.
170             my $newactive = $Election->ApplyFloor( 'TopCountFloor', 10);
171              
172             =head2 ApprovalFloor, TopCountFloor
173              
174             Requires a percent of votes cast in Approval or TopCount. The default is 5% for Approval and 2% for TopCount.
175              
176             # TopCountFloor with 3% threshold.
177             my $Floored = $Election->TopCountFloor( 3 );
178              
179             Both of these methods take an optional parameter which is the percentage for the floor. If the parameter is 1 or greater the parameter will be interpreted as a percentage, if it is less than 1 it will be interpreted as a decimal fraction, .1 and 10 will both result in a 10% floor.
180              
181             For Range Ballots using ApprovalFloor there is an additional optional value for cutoff that sets the score below which choices are not considered approved of.
182              
183             # Applies 5% floor with cutoff 5 (appropriate for Range 0-10)
184             my $active = $Range->ApprovalFloor( 5, 5 );
185              
186             =head2 TCA (TopCount-Approval)
187              
188             Aggressive but (effectively) safe for Condorcet Methods. It requires the Approval for a choice be at least half of the leading Top Count Vote.
189              
190             This rule takes an optional argument to change the floor from .5.
191              
192             # uses default of 1/2
193             my $active = $Election->TCA();
194             # requires approval equal leader
195             my $active = $Election->TCA( 1 );
196              
197             =head3 TCA Rule Validation and Implication
198              
199             If there is a Loop or Condorcet Winner, either it will be/include the Top Count Leader or it must be a choice which defeats the Top Count leader. To defeat the Top Count Leader a Choice's Approval must be greater than the Lead Top Count. To be able to defeat a choice it is necessary to have more than half of the approval of that choice. Thus to be able to defeat a choice which can defeat the Top Count Leader it will be necessary to have more than half of the Approval of a choice with an Approval greater than the lead Top Count.
200              
201             There is a small possibility for a situation with a deeply nested knotted result that this rule could eliminate a member of the Dominant Set. For the common simple dropping rules (Approval, Top Count, Greatest Loss, Borda) this choice would never win.
202              
203             For IRV any choice with an Approval that is not greater than the current TopCount of any other choice will always be eliminated prior to that choice. Unfortunately, with IRV any change to dropping order can alter the result. If it is used in IRV the Election Rules must specify it. Also because it is a high Approval based Floor, it can be construed as adding a small risk of Later Harm violation. If the reason for choosing IRV was Later Harm, then the only safe floor is a TopCount floor.
204              
205             =cut
206              
207             1;
208              
209             #FOOTER
210              
211             =pod
212              
213             BUG TRACKER
214              
215             L<https://github.com/brainbuz/Vote-Count/issues>
216              
217             AUTHOR
218              
219             John Karr (BRAINBUZ) brainbuz@cpan.org
220              
221             CONTRIBUTORS
222              
223             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
224              
225             LICENSE
226              
227             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>.
228              
229             SUPPORT
230              
231             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
232              
233             =cut
234