File Coverage

blib/lib/Vote/Count/ReadBallots.pm
Criterion Covered Total %
statement 104 108 96.3
branch 19 24 79.1
condition n/a
subroutine 15 15 100.0
pod 4 4 100.0
total 142 151 94.0


line stmt bran cond sub pod time code
1             package Vote::Count::ReadBallots;
2              
3 40     40   1183911 use 5.024;
  40         168  
4 40     40   237 use feature qw/postderef signatures/;
  40         86  
  40         4049  
5 40     40   248 use strict;
  40         86  
  40         860  
6 40     40   197 use warnings;
  40         78  
  40         1386  
7 40     40   197 no warnings qw/experimental/;
  40         83  
  40         1743  
8 40     40   967 use Path::Tiny 0.108;
  40         11059  
  40         2264  
9 40     40   253 use Carp;
  40         84  
  40         2089  
10 40     40   17233 use JSON::MaybeXS;
  40         187468  
  40         2262  
11 40     40   1724 use YAML::XS;
  40         7548  
  40         3065  
12             # use Data::Dumper;
13              
14             # ABSTRACT: Read Ballots for Vote::Count. Toolkit for vote counting.
15              
16             our $VERSION='2.01';
17              
18             =head1 NAME
19              
20             Vote::Count::ReadBallots
21              
22             =head1 VERSION 2.01
23              
24             =head1 SYNOPSIS
25              
26             use Vote::Count::ReadBallots;
27              
28             my $data1 = read_ballots('t/data/data1.txt');
29              
30             =head1 Description
31              
32             Reads a file containing vote data. Retruns a HashRef of a Vote::Count BallotSet.
33              
34             All public methods are exported.
35              
36             =head1 BallotSet Data Structure
37              
38             ballots {
39             CHOCOLATE:MINTCHIP:VANILLA {
40             count 1,
41             votevalue 1, # needed for STV support
42             votes [
43             [0] "CHOCOLATE",
44             [1] "MINTCHIP",
45             [2] "VANILLA"
46             ]
47             },
48             },
49             choices {
50             CHOCOLATE 1,
51             MINTCHIP 1,
52             VANILLA 1
53             },
54             votescast 1,
55             comment "# Optional Comment",
56             options {
57             rcv 1
58             }
59              
60             =head1 Data File Format
61              
62             # This is a comment, optional.
63             :CHOICES:VANILLA:CHOCOLATE:STRAWBERRY:MINTCHIP:CARAMEL:RUMRAISIN
64             5:VANILLA:CHOCOLATE:STRAWBERRY
65             RUMRAISIN
66              
67             CHOICES must be defined before any vote lines. or an error will be thrown. CHOICES must only be defined once. These two rules are to protect against errors in manually prepared files.
68              
69             A data line may begin with a number or a choice. When there is no number the line is counted as being a single ballot. The number represents the number of ballots identical to that one; this notation will both dramatically shrink the data files and improve performance.
70              
71             =head2 read_ballots
72              
73             Reads a data file in the standard Vote::Count format and returns a BallotSet.
74              
75             =head2 write_ballots
76              
77             write_ballots( $BallotSet, $newfile);
78              
79             Write out a ballotset. Useful for creating a compressed version of a raw file.
80              
81             =head1 Range Ballots
82              
83             Range Ballots are supported in both JSON and YAML format. The read method doesn't perform validation like B<read_ballots> does.
84              
85             =head2 Range Ballot Format in JSON
86              
87             {
88             "choices": [
89             "TWEEDLEDEE",
90             "TWEEDLEDUM",
91             "HUMPTYDUMPTY"
92             ],
93             "ballots": [
94             {
95             "votes": {
96             "TWEEDLEDEE": 1,
97             "TWEEDLEDUM": 1,
98             "HUMPTYDUMPTY": 3
99             },
100             "count": 3
101             }
102             ],
103             "depth": 3
104             }
105              
106             =head2 read_range_ballots
107              
108             Requires a parameter of a JSON or YAML file. The second parameter may be either 'json' or 'yaml', defaulting to 'json'.
109              
110             my $BestFastFood = read_range_ballots('t/data/fastfood.range.json');
111             my $BestFastFood = read_range_ballots('t/data/fastfood.range.yml', 'yaml');
112              
113             =head2 write_range_ballots
114              
115             Takes three parameters, a BallotSet, a file location, and a value of 'json' or 'yaml'. The first two parameters are required, the third defaults to 'json'.
116              
117             write_range_ballots( $BestFastFood, '/tmp/fast.json', 'json' );
118              
119             =cut
120              
121 40         309 use Exporter::Easy ( EXPORT =>
122             [qw( read_ballots write_ballots read_range_ballots write_range_ballots)],
123 40     40   17589 );
  40         47856  
124              
125             my $coder = Cpanel::JSON::XS->new->ascii->pretty;
126              
127 151     151   1695 sub _choices ( $choices ) {
  151         303  
  151         257  
128 151         353 my %C = ();
129 151         580 $choices =~ m/^\:CHOICES\:(.*)/;
130 151         1099 for my $choice ( split /:/, $1 ) {
131 1360         2427 $C{$choice} = 1;
132             }
133 151         557 return \%C;
134             }
135              
136 150     150 1 269624 sub read_ballots( $filename ) {
  150         369  
  150         261  
137 150         1041 my %data = (
138             'choices' => undef,
139             'ballots' => {},
140             'options' => { 'rcv' => 1 },
141             'votescast' => 0,
142             'comment' => ''
143             );
144             BALLOTREADLINES:
145 150         719 for my $line_raw ( path($filename)->lines({chomp => 1}) ) {
146 9568 100       95833 if ( $line_raw =~ m/^\#/ ) {
147 114         337 $data{'comment'} .= $line_raw;
148 114         256 next BALLOTREADLINES;
149             }
150 9454 100       15257 if ( $line_raw =~ m/^\:CHOICES\:/ ) {
151 152 100       548 if ( $data{'choices'} ) {
152 2         30 croak("File $filename redefines CHOICES \n$line_raw\n");
153             }
154 150         514 else { $data{'choices'} = _choices($line_raw); }
155 150         336 next;
156             }
157 9302         13088 my $line = $line_raw;
158 9302 100       21323 next unless ( $line =~ /\w/ );
159 9287         26607 $line =~ s/^(\d+)\://;
160 9287 100       22210 my $numbals = $1 ? $1 : 1;
161 9287         14473 $data{'votescast'} += $numbals;
162 9287 100       16014 if ( $data{'ballots'}{$line} ) {
163             $data{'ballots'}{$line}{'count'} =
164 129         336 $data{'ballots'}{$line}{'count'} + $numbals;
165             }
166             else {
167 9158         12590 my @votes = ();
168 9158         25868 for my $choice ( split( /:/, $line ) ) {
169 43022 100       72712 unless ( $data{'choices'}{$choice} ) {
170             die "Choice: $choice is not in defined choice list: "
171 2         51 . join( ", ", keys( $data{'choices'}->%* ) )
172             . "\n -- $line\n";
173             }
174 43020         66086 push @votes, $choice;
175             }
176 9156         31267 $data{'ballots'}{$line}{'count'} = $numbals;
177 9156         21558 $data{'ballots'}{$line}{'votes'} = \@votes;
178             }
179             }
180 146         2898 for my $K ( keys $data{'ballots'}->%* ) { $data{'ballots'}{$K}{'votevalue'} = 1 }
  9136         14516  
181 146         4270 return \%data;
182             }
183              
184 1     1 1 5 sub write_ballots ( $BallotSet, $destination ) {
  1         2  
  1         2  
  1         3  
185 1         3 my @data = ('# Data rewritten in compressed form.');
186 1         10 my $choicelist = join( ':', sort keys( $BallotSet->{'choices'}->%* ) );
187 1         5 push @data, "CHOICES:$choicelist";
188 1         5 for my $k ( sort keys $BallotSet->{'ballots'}->%* ) {
189 5         7 my $cnt = $BallotSet->{'ballots'}{$k}{'count'};
190 5         11 push @data, "$cnt:$k";
191             }
192 1 50       2 for my $D (@data) { $D .= "\n" if $D !~ /\n$/ }
  7         15  
193 1         4 path($destination)->spew(@data);
194             }
195              
196 1     1 1 6 sub write_range_ballots ( $BallotSet, $destination, $format = 'json' ) {
  1         3  
  1         2  
  1         2  
  1         1  
197 1         7 $BallotSet->{'choices'} = [ sort keys $BallotSet->{'choices'}->%* ];
198 1 50       4 if ( $format eq 'json' ) {
    0          
199 1         4 path($destination)->spew( $coder->encode($BallotSet) );
200             }
201             elsif ( $format eq 'yaml' ) {
202 0         0 $BallotSet = Load path->($destination)->slurp;
203 0         0 path($destination)->spew( Dump $BallotSet);
204             }
205 0         0 else { die "invalid score ballot format $format." }
206             }
207              
208 20     20 1 13644 sub read_range_ballots ( $source, $format = 'json' ) {
  20         52  
  20         38  
  20         32  
209 20         46 my $BallotSet = undef;
210 20 100       74 if ( $format eq 'json' ) {
    50          
211 17         83 $BallotSet = $coder->decode( path($source)->slurp );
212             }
213             elsif ( $format eq 'yaml' ) {
214 3         10 $BallotSet = Load path($source)->slurp;
215             }
216 0         0 else { die "invalid score ballot format $format." }
217 20         6038 $BallotSet->{'votescast'} = 0;
218 20         89 $BallotSet->{'options'} = { 'range' => 1, 'rcv' => 0 };
219 20         84 my @choices = $BallotSet->{'choices'}->@*;
220 20         53 $BallotSet->{'choices'} = { map { $_ => 1 } @choices };
  144         260  
221 20         77 for my $ballot ( $BallotSet->{'ballots'}->@* ) {
222 96         143 $BallotSet->{'votescast'} += $ballot->{'count'};
223             }
224 20         595 return $BallotSet;
225             }
226              
227             1;
228              
229             #buildpod
230              
231             #FOOTER
232              
233             =pod
234              
235             BUG TRACKER
236              
237             L<https://github.com/brainbuz/Vote-Count/issues>
238              
239             AUTHOR
240              
241             John Karr (BRAINBUZ) brainbuz@cpan.org
242              
243             CONTRIBUTORS
244              
245             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
246              
247             LICENSE
248              
249             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>.
250              
251             SUPPORT
252              
253             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
254              
255             =cut
256