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   1384175 use 5.024;
  40         187  
4 40     40   269 use feature qw/postderef signatures/;
  40         91  
  40         4619  
5 40     40   295 use strict;
  40         101  
  40         1038  
6 40     40   243 use warnings;
  40         111  
  40         1681  
7 40     40   250 no warnings qw/experimental/;
  40         118  
  40         2316  
8 40     40   1176 use Path::Tiny 0.108;
  40         12881  
  40         2805  
9 40     40   314 use Carp;
  40         91  
  40         2567  
10 40     40   15986 use JSON::MaybeXS;
  40         183721  
  40         2541  
11 40     40   1956 use YAML::XS;
  40         9209  
  40         3515  
12             # use Data::Dumper;
13              
14             # ABSTRACT: Read Ballots for Vote::Count. Toolkit for vote counting.
15              
16             our $VERSION='2.00';
17              
18             =head1 NAME
19              
20             Vote::Count::ReadBallots
21              
22             =head1 VERSION 2.00
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         395 use Exporter::Easy ( EXPORT =>
122             [qw( read_ballots write_ballots read_range_ballots write_range_ballots)],
123 40     40   20766 );
  40         54562  
124              
125             my $coder = Cpanel::JSON::XS->new->ascii->pretty;
126              
127 137     137   1717 sub _choices ( $choices ) {
  137         319  
  137         230  
128 137         341 my %C = ();
129 137         519 $choices =~ m/^\:CHOICES\:(.*)/;
130 137         1027 for my $choice ( split /:/, $1 ) {
131 1202         2428 $C{$choice} = 1;
132             }
133 137         531 return \%C;
134             }
135              
136 136     136 1 278283 sub read_ballots( $filename ) {
  136         353  
  136         245  
137 136         973 my %data = (
138             'choices' => undef,
139             'ballots' => {},
140             'options' => { 'rcv' => 1 },
141             'votescast' => 0,
142             'comment' => ''
143             );
144             BALLOTREADLINES:
145 136         673 for my $line_raw ( path($filename)->lines({chomp => 1}) ) {
146 9401 100       98291 if ( $line_raw =~ m/^\#/ ) {
147 100         296 $data{'comment'} .= $line_raw;
148 100         243 next BALLOTREADLINES;
149             }
150 9301 100       16142 if ( $line_raw =~ m/^\:CHOICES\:/ ) {
151 138 100       517 if ( $data{'choices'} ) {
152 2         33 croak("File $filename redefines CHOICES \n$line_raw\n");
153             }
154 136         482 else { $data{'choices'} = _choices($line_raw); }
155 136         337 next;
156             }
157 9163         14147 my $line = $line_raw;
158 9163 100       22959 next unless ( $line =~ /\w/ );
159 9148         28015 $line =~ s/^(\d+)\://;
160 9148 100       22981 my $numbals = $1 ? $1 : 1;
161 9148         14981 $data{'votescast'} += $numbals;
162 9148 100       16526 if ( $data{'ballots'}{$line} ) {
163             $data{'ballots'}{$line}{'count'} =
164 127         379 $data{'ballots'}{$line}{'count'} + $numbals;
165             }
166             else {
167 9021         12743 my @votes = ();
168 9021         27529 for my $choice ( split( /:/, $line ) ) {
169 42756 100       76309 unless ( $data{'choices'}{$choice} ) {
170             die "Choice: $choice is not in defined choice list: "
171 2         38 . join( ", ", keys( $data{'choices'}->%* ) )
172             . "\n -- $line\n";
173             }
174 42754         70968 push @votes, $choice;
175             }
176 9019         33533 $data{'ballots'}{$line}{'count'} = $numbals;
177 9019         23108 $data{'ballots'}{$line}{'votes'} = \@votes;
178             }
179             }
180 132         3130 for my $K ( keys $data{'ballots'}->%* ) { $data{'ballots'}{$K}{'votevalue'} = 1 }
  8999         15254  
181 132         4111 return \%data;
182             }
183              
184 1     1 1 5 sub write_ballots ( $BallotSet, $destination ) {
  1         2  
  1         2  
  1         2  
185 1         3 my @data = ('# Data rewritten in compressed form.');
186 1         8 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         10 my $cnt = $BallotSet->{'ballots'}{$k}{'count'};
190 5         12 push @data, "$cnt:$k";
191             }
192 1 50       3 for my $D (@data) { $D .= "\n" if $D !~ /\n$/ }
  7         17  
193 1         4 path($destination)->spew(@data);
194             }
195              
196 1     1 1 6 sub write_range_ballots ( $BallotSet, $destination, $format = 'json' ) {
  1         2  
  1         2  
  1         2  
  1         3  
197 1         8 $BallotSet->{'choices'} = [ sort keys $BallotSet->{'choices'}->%* ];
198 1 50       6 if ( $format eq 'json' ) {
    0          
199 1         5 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 19     19 1 13438 sub read_range_ballots ( $source, $format = 'json' ) {
  19         47  
  19         41  
  19         33  
209 19         40 my $BallotSet = undef;
210 19 100       72 if ( $format eq 'json' ) {
    50          
211 16         133 $BallotSet = $coder->decode( path($source)->slurp );
212             }
213             elsif ( $format eq 'yaml' ) {
214 3         11 $BallotSet = Load path($source)->slurp;
215             }
216 0         0 else { die "invalid score ballot format $format." }
217 19         6459 $BallotSet->{'votescast'} = 0;
218 19         85 $BallotSet->{'options'} = { 'range' => 1, 'rcv' => 0 };
219 19         90 my @choices = $BallotSet->{'choices'}->@*;
220 19         51 $BallotSet->{'choices'} = { map { $_ => 1 } @choices };
  140         292  
221 19         79 for my $ballot ( $BallotSet->{'ballots'}->@* ) {
222 92         153 $BallotSet->{'votescast'} += $ballot->{'count'};
223             }
224 19         573 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