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              
2             use 5.024;
3 40     40   1152058 use feature qw/postderef signatures/;
  40         182  
4 40     40   267 use strict;
  40         431  
  40         8744  
5 40     40   296 use warnings;
  40         114  
  40         945  
6 40     40   2246 no warnings qw/experimental/;
  40         94  
  40         1580  
7 40     40   217 use Path::Tiny 0.108;
  40         91  
  40         5969  
8 40     40   935 use Carp;
  40         10443  
  40         2497  
9 40     40   270 use JSON::MaybeXS;
  40         92  
  40         2219  
10 40     40   16649 use YAML::XS;
  40         171801  
  40         2313  
11 40     40   1553 # use Data::Dumper;
  40         7573  
  40         3275  
12              
13             # ABSTRACT: Read Ballots for Vote::Count. Toolkit for vote counting.
14              
15             our $VERSION='2.02';
16              
17             =head1 NAME
18              
19             Vote::Count::ReadBallots
20              
21             =head1 VERSION 2.02
22              
23             =head1 SYNOPSIS
24              
25             use Vote::Count::ReadBallots;
26              
27             my $data1 = read_ballots('t/data/data1.txt');
28              
29             =head1 Description
30              
31             Reads a file containing vote data. Retruns a HashRef of a Vote::Count BallotSet.
32              
33             All public methods are exported.
34              
35             =head1 BallotSet Data Structure
36              
37             ballots {
38             CHOCOLATE:MINTCHIP:VANILLA {
39             count 1,
40             votevalue 1, # needed for STV support
41             votes [
42             [0] "CHOCOLATE",
43             [1] "MINTCHIP",
44             [2] "VANILLA"
45             ]
46             },
47             },
48             choices {
49             CHOCOLATE 1,
50             MINTCHIP 1,
51             VANILLA 1
52             },
53             votescast 1,
54             comment "# Optional Comment",
55             options {
56             rcv 1
57             }
58              
59             =head1 Data File Format
60              
61             # This is a comment, optional.
62             :CHOICES:VANILLA:CHOCOLATE:STRAWBERRY:MINTCHIP:CARAMEL:RUMRAISIN
63             5:VANILLA:CHOCOLATE:STRAWBERRY
64             RUMRAISIN
65              
66             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.
67              
68             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.
69              
70             =head2 read_ballots
71              
72             Reads a data file in the standard Vote::Count format and returns a BallotSet.
73              
74             =head2 write_ballots
75              
76             write_ballots( $BallotSet, $newfile);
77              
78             Write out a ballotset. Useful for creating a compressed version of a raw file.
79              
80             =head1 Range Ballots
81              
82             Range Ballots are supported in both JSON and YAML format. The read method doesn't perform validation like B<read_ballots> does.
83              
84             =head2 Range Ballot Format in JSON
85              
86             {
87             "choices": [
88             "TWEEDLEDEE",
89             "TWEEDLEDUM",
90             "HUMPTYDUMPTY"
91             ],
92             "ballots": [
93             {
94             "votes": {
95             "TWEEDLEDEE": 1,
96             "TWEEDLEDUM": 1,
97             "HUMPTYDUMPTY": 3
98             },
99             "count": 3
100             }
101             ],
102             "depth": 3
103             }
104              
105             =head2 read_range_ballots
106              
107             Requires a parameter of a JSON or YAML file. The second parameter may be either 'json' or 'yaml', defaulting to 'json'.
108              
109             my $BestFastFood = read_range_ballots('t/data/fastfood.range.json');
110             my $BestFastFood = read_range_ballots('t/data/fastfood.range.yml', 'yaml');
111              
112             =head2 write_range_ballots
113              
114             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'.
115              
116             write_range_ballots( $BestFastFood, '/tmp/fast.json', 'json' );
117              
118             =cut
119              
120             use Exporter::Easy ( EXPORT =>
121 40         2340 [qw( read_ballots write_ballots read_range_ballots write_range_ballots)],
122             );
123 40     40   16305  
  40         52335  
124             my $coder = Cpanel::JSON::XS->new->ascii->pretty;
125              
126             my %C = ();
127 152     152   1724 $choices =~ m/^\:CHOICES\:(.*)/;
  152         305  
  152         253  
128 152         365 for my $choice ( split /:/, $1 ) {
129 152         625 $C{$choice} = 1;
130 152         1047 }
131 1372         2645 return \%C;
132             }
133 152         686  
134             my %data = (
135             'choices' => undef,
136 151     151 1 277952 'ballots' => {},
  151         384  
  151         261  
137 151         1101 'options' => { 'rcv' => 1 },
138             'votescast' => 0,
139             'comment' => ''
140             );
141             BALLOTREADLINES:
142             for my $line_raw ( path($filename)->lines({chomp => 1}) ) {
143             if ( $line_raw =~ m/^\#/ ) {
144             $data{'comment'} .= $line_raw;
145 151         767 next BALLOTREADLINES;
146 9582 100       118324 }
147 116         432 if ( $line_raw =~ m/^\:CHOICES\:/ ) {
148 116         272 if ( $data{'choices'} ) {
149             croak("File $filename redefines CHOICES \n$line_raw\n");
150 9466 100       16322 }
151 153 100       547 else { $data{'choices'} = _choices($line_raw); }
152 2         32 next;
153             }
154 151         536 my $line = $line_raw;
155 151         361 next unless ( $line =~ /\w/ );
156             $line =~ s/^(\d+)\://;
157 9313         13891 my $numbals = $1 ? $1 : 1;
158 9313 100       22823 $data{'votescast'} += $numbals;
159 9298         27921 if ( $data{'ballots'}{$line} ) {
160 9298 100       23097 $data{'ballots'}{$line}{'count'} =
161 9298         15238 $data{'ballots'}{$line}{'count'} + $numbals;
162 9298 100       16407 }
163             else {
164 129         363 my @votes = ();
165             for my $choice ( split( /:/, $line ) ) {
166             unless ( $data{'choices'}{$choice} ) {
167 9169         13336 die "Choice: $choice is not in defined choice list: "
168 9169         26964 . join( ", ", keys( $data{'choices'}->%* ) )
169 43044 100       76461 . "\n -- $line\n";
170             }
171 2         38 push @votes, $choice;
172             }
173             $data{'ballots'}{$line}{'count'} = $numbals;
174 43042         70332 $data{'ballots'}{$line}{'votes'} = \@votes;
175             }
176 9167         32112 }
177 9167         22021 for my $K ( keys $data{'ballots'}->%* ) { $data{'ballots'}{$K}{'votevalue'} = 1 }
178             return \%data;
179             }
180 147         3190  
  9147         16024  
181 147         4526 my @data = ('# Data rewritten in compressed form.');
182             my $choicelist = join( ':', sort keys( $BallotSet->{'choices'}->%* ) );
183             push @data, "CHOICES:$choicelist";
184 1     1 1 6 for my $k ( sort keys $BallotSet->{'ballots'}->%* ) {
  1         2  
  1         3  
  1         2  
185 1         3 my $cnt = $BallotSet->{'ballots'}{$k}{'count'};
186 1         11 push @data, "$cnt:$k";
187 1         4 }
188 1         5 for my $D (@data) { $D .= "\n" if $D !~ /\n$/ }
189 5         10 path($destination)->spew(@data);
190 5         12 }
191              
192 1 50       3 $BallotSet->{'choices'} = [ sort keys $BallotSet->{'choices'}->%* ];
  7         17  
193 1         5 if ( $format eq 'json' ) {
194             path($destination)->spew( $coder->encode($BallotSet) );
195             }
196 1     1 1 7 elsif ( $format eq 'yaml' ) {
  1         2  
  1         3  
  1         2  
  1         2  
197 1         10 $BallotSet = Load path->($destination)->slurp;
198 1 50       4 path($destination)->spew( Dump $BallotSet);
    0          
199 1         4 }
200             else { die "invalid score ballot format $format." }
201             }
202 0         0  
203 0         0 my $BallotSet = undef;
204             if ( $format eq 'json' ) {
205 0         0 $BallotSet = $coder->decode( path($source)->slurp );
206             }
207             elsif ( $format eq 'yaml' ) {
208 20     20 1 14077 $BallotSet = Load path($source)->slurp;
  20         47  
  20         46  
  20         33  
209 20         40 }
210 20 100       78 else { die "invalid score ballot format $format." }
    50          
211 17         76 $BallotSet->{'votescast'} = 0;
212             $BallotSet->{'options'} = { 'range' => 1, 'rcv' => 0 };
213             my @choices = $BallotSet->{'choices'}->@*;
214 3         12 $BallotSet->{'choices'} = { map { $_ => 1 } @choices };
215             for my $ballot ( $BallotSet->{'ballots'}->@* ) {
216 0         0 $BallotSet->{'votescast'} += $ballot->{'count'};
217 20         6567 }
218 20         97 return $BallotSet;
219 20         93 }
220 20         53  
  144         300  
221 20         89 1;
222 96         159  
223             #buildpod
224 20         749  
225             #FOOTER
226              
227             =pod
228              
229             BUG TRACKER
230              
231             L<https://github.com/brainbuz/Vote-Count/issues>
232              
233             AUTHOR
234              
235             John Karr (BRAINBUZ) brainbuz@cpan.org
236              
237             CONTRIBUTORS
238              
239             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
240              
241             LICENSE
242              
243             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>.
244              
245             SUPPORT
246              
247             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
248              
249             =cut
250