File Coverage

blib/lib/Data/Grouper.pm
Criterion Covered Total %
statement 106 140 75.7
branch 24 34 70.5
condition 12 18 66.6
subroutine 9 11 81.8
pod 7 8 87.5
total 158 211 74.8


line stmt bran cond sub pod time code
1             package Data::Grouper;
2            
3 1     1   716 use strict;
  1         2  
  1         1535  
4             #use vars qw($VERSION);
5            
6             $Data::Grouper::VERSION = '0.06';
7            
8            
9             #
10             # Options
11             #
12             # COLNAMES => [ name, name, ... ]
13             # SORTCOLS => [ name, ... ]
14             # AGGREGATES => [ colidx, colidx, ... ]
15             # DATA => array of hashrefs or arrayrefs
16             #
17             #
18             # Note: The lastvals array's indexes correspond to the
19             # array indexes for SORTCOLS.
20             #
21             sub new
22             {
23 2     2 1 115 my $proto = shift;
24 2   33     25 my $class = ref($proto) || $proto;
25            
26 2         4 my $self = {};
27            
28             # set option defaults so they won't be undefined
29 2         6 $self->{OUTER} = [];
30 2         5 $self->{LASTVALS} = [];
31 2         5 $self->{TOPLEVEL_AGGS} = {};
32 2         4 $self->{OPTIONS} = {};
33            
34             #$self->{LASTADDED} # we want this to be undefined at first
35            
36             # Set this to 0 to avoid extra work. If somebody uses an
37             # option that requires we compute aggregates, this will be
38             # set to 1 later.
39             #
40 2         5 $self->{OPTIONS}->{USE_AGGREGATES} = 0;
41            
42             # load in options supplied to new()
43 2         73 for (my $x = 0; $x <= $#_; $x += 2)
44             {
45 5         12 my $opt = uc($_[$x]);
46            
47 5 50       13 defined($_[($x + 1)]) or die "grouper->new() called with odd number of option parameters - should be of the form option => value";
48 5         22 $self->{OPTIONS}->{$opt} = $_[($x + 1)];
49             }
50            
51             # automatically set this if necessary
52 2 100       7 if (defined($self->{OPTIONS}->{AGGREGATES}) )
53             {
54 1         2 $self->{OPTIONS}->{USE_AGGREGATES} = 1;
55             }
56            
57 2         3 bless($self);
58            
59 2 100       12 if (defined($self->{OPTIONS}->{DATA}))
60             {
61 1         4 $self->add_array($self->{OPTIONS}->{DATA});
62             }
63            
64 2         7 return $self;
65             }
66            
67             # DBI can return entire arrays with selectall_arrayref. This
68             # function lets you just pass an array ref in instead of doing
69             # a while loop and calling add_row for each row.
70             #
71             sub add_array
72             {
73 1     1 1 2 my ($self,$aref) = @_;
74 1         2 for my $r (@{$aref})
  1         2  
75             {
76 3 50       9 if (ref($r) eq 'HASH')
77             {
78 3         14 $self->add_hash($r);
79             }
80             else
81             {
82 0         0 $self->add_row(@{$r});
  0         0  
83             }
84             }
85             }
86            
87             #
88             # This adds a row to our dataset.
89             # This is pretty much the most important function in this
90             # module.
91             #
92             # This really should take a reference!!!!
93             #
94             sub add_row
95             {
96 2     2 1 12 my ($self,@row) = @_;
97 2         4 my $options = $self->{OPTIONS};
98 2         3 my (%h);
99            
100 2 50       8 warn "You must define COLNAMES when using add_row or DATA with arrayrefs.\n"
101             if not defined $options->{COLNAMES};
102            
103             # Turn @row into a hash
104 2         3 @h{ @{ $options->{COLNAMES} } } = @row;
  2         9  
105            
106 2         7 $self->add_hash(\%h);
107             }
108            
109             sub add_hash
110             {
111 5     5 1 7 my ($self,$href) = @_;
112            
113 5         9 my $options = $self->{OPTIONS};
114 5         6 my $sortcols = 1 + $#{ $options->{SORTCOLS} };
  5         11  
115            
116             # Automatically populate COLNAMES if it hasn't been done before
117 5 100       23 if ($#{$options->{COLNAMES}} == -1)
  5         14  
118             {
119 1         2 @{$options->{COLNAMES}} = keys ( %{$href} );
  1         4  
  1         3  
120             }
121            
122            
123             # Create our own copy. We don't want to modify someone
124             # elses hash.
125 5         6 my %h2 = %{$href};
  5         19  
126            
127             # apply format functions
128 5         9 for my $fkey (keys(%{$options->{FORMAT}}))
  5         26  
129             {
130 0         0 my $fref = $options->{FORMAT}->{$fkey};
131 0         0 $h2{$fkey} = &$fref($h2{$fkey});
132             }
133            
134             # describe aref here
135 5         10 my $aref = $self->{OUTER};
136            
137             # Update top level aggregates
138 5 100       14 if ($options->{USE_AGGREGATES} == 1)
139             {
140 2         5 for my $colname (@{$options->{AGGREGATES}})
  2         5  
141             {
142 2         4 my $val = $h2{$colname};
143 2         9 $self->{TOPLEVEL_AGGS}->{"SUM_$colname"} += $val;
144             }
145             }
146            
147             #
148             # This loop iterates through the sort items, descending through
149             # the arrays of hashes until it gets to the leaf node where
150             # this row belongs. It then pushes the hash ref onto the
151             # appropritate array. Non-leaf arrays contain aggregate info.
152             #
153             # $i in this loop is an index into the SORTCOLS and LASTVALS arrays.
154             #
155            
156 5         13 for (my $i=0;$i<$sortcols;$i++)
157             {
158             # Must figure out rowidx based on $i and COLNAMES
159 5         11 my $colname = $options->{SORTCOLS}->[$i];
160            
161 10         28 warn "Item $colname in SORTCOLS does not correspond to COLNAMES.\n"
162 5 50       6 if (!grep {$_ eq $colname} @{$options->{COLNAMES}});
  5         10  
163            
164             # If this is the first row, or it is a new value, create
165             # new entries
166             #
167             # Comment from Sam Tregar-- use of 'ne' possibly inappropriate for
168             # floating point data. Provide an option here?
169             #
170 5 100 100     27 if (!defined($self->{LASTVALS}->[$i]) ||
171             $self->{LASTVALS}->[$i] ne $h2{$colname}
172             )
173             {
174            
175            
176             #
177             # apply format functions to aggregates before moving on
178             # to new array
179             # This needs to work for all the grouping levels after this...
180             # i.e., if $i=2 and $sortcols=4, we still have to format $i==3
181             #
182 3 100       9 if (defined($self->{LASTVALS}->[$i]))
183             {
184 1         3 $self->_format_tails($aref);
185             }
186            
187            
188             # Add a new hash to the current array of hash refs
189             # I copy this from the hash for this row as a shortcut to
190             # give access to $colname, which is generally needed. Also
191             # other variables may be in @row, like IDs or something, that
192             # the author wants to use
193            
194 3         13 my %h3 = %h2;
195 3         9 $h3{INNER} = [];
196 3         4 push @{$aref}, \%h3;
  3         7  
197            
198            
199 3 100       9 if ($options->{USE_AGGREGATES} == 1)
200             {
201 1         12 $self->_do_aggregates($aref,\%h2);
202             }
203            
204            
205             # Set aref, our array pointer, to the array of the
206             # next inner element
207 3         5 $aref = $h3{INNER};
208            
209             # undefine all vals after this in order to
210             # force new arrays to be generated
211 3         4 $#{$self->{LASTVALS}} = $i;
  3         11  
212            
213 3         13 $self->{LASTVALS}->[$i] = $h2{$colname};
214             }
215             else
216             {
217             # this is the place to do totals....
218 2 100       7 if ($options->{USE_AGGREGATES} == 1)
219             {
220 1         2 $self->_do_aggregates($aref,\%h2);
221             }
222            
223             #Move to inner array
224 2         8 $aref = $aref->[-1]->{INNER};
225             }
226            
227             }
228            
229             # We've found the right array, add the row
230 5         8 push @{$aref},\%h2;
  5         10  
231 5         18 $self->{LASTADDED} = $aref;
232             }
233            
234             # Sometimes you will want to add some additional information
235             # to a row or a parent. This function helps you do that.
236             #
237             # It takes these parameters:
238             # * Address (ar_loc)
239             # This specifies which row you would like to add data to. If you are
240             # sorting on 3 columns, you would pass an array ref with between 1 and 3
241             # values. This would specify a non-innermost loop row. This row would
242             # then have the contents of the details parameter (below) added to its
243             # hash.
244             #
245             # * Details (hr_dtls)
246             # This is a reference to a hash that will be added to the indicated
247             # row, if it is found.
248             #
249             sub add_details
250             {
251 0     0 1 0 my ($self,$ar_loc,$hr_dtls) = @_;
252            
253             # ar_loc contains basically an address
254 0         0 my $colidx = 0;
255 0         0 my $aref = $self->{OUTER};
256 0         0 my $the_href;
257 0         0 my $options = $self->{OPTIONS};
258            
259 0         0 for my $loc (@{$ar_loc})
  0         0  
260             {
261 0         0 my $found = 0;
262            
263             # look through rows for match
264 0         0 for my $href (@{$aref})
  0         0  
265             {
266 0         0 my $colname = $options->{SORTCOLS}->[$colidx];
267             # print "test ($loc) ($colname)
";
268 0 0       0 if ($loc eq $href->{$colname})
269             {
270 0         0 $the_href = $href;
271 0         0 $aref = $href->{INNER};
272 0         0 $found = 1;
273 0         0 last;
274             }
275             }
276            
277             # if it wasn't found return failure
278 0 0       0 if (!$found)
279             {
280             #print "not found ($loc) !";
281 0         0 return 0;
282             }
283            
284 0         0 $colidx++;
285             }
286            
287 0         0 for my $k (keys (%{$hr_dtls}) )
  0         0  
288             {
289 0         0 $the_href->{$k} = $hr_dtls->{$k};
290             }
291            
292 0         0 1;
293             }
294            
295             sub get_details
296 0     0 0 0 {
297             }
298            
299             #
300             #
301             # Returns the array ref you will need to pass to HTML::Template
302             #
303             # Computes any top level aggregates first though
304             #
305             sub get_data
306             {
307 2     2 1 11 my ($self) = @_;
308 2         8 $self->_format_tails($self->{OUTER});
309 2         6 $self->{OUTER};
310             }
311            
312             sub get_top_aggregates
313             {
314 1     1 1 122 my ($self) = @_;
315 1         3 my $options = $self->{OPTIONS};
316            
317             # possibly defer computation until here?
318            
319             # apply format functions
320             # Does this make any sense?
321 1         2 for my $fkey (keys(%{$options->{FORMAT}}))
  1         4  
322             {
323 0         0 my $fref = $options->{FORMAT}->{$fkey};
324 0         0 $self->{TOPLEVEL_AGGS}->{$fkey} = &$fref($self->{TOPLEVEL_AGGS}->{$fkey});
325             }
326            
327 1         4 $self->{TOPLEVEL_AGGS};
328             }
329            
330             #
331             # Private functions
332             #
333            
334            
335             # _do_aggregates
336             #
337             # This helper function computes aggregates:
338             #
339             # SUM
340             # AVG
341             # COUNT
342             # MIN
343             # MAX
344             #
345             # $aref is an array reference into the structure we are building.
346             # $hr_row is a hash reference of the hash of the row we are adding.
347             #
348             sub _do_aggregates
349             {
350 2     2   5 my ($self,$aref,$hr_row) = @_;
351 2         2 my $href = $aref->[$#{$aref}];
  2         5  
352            
353             #
354             # For each colidx in our list of columns to be summed,
355             # add to its total
356             #
357            
358 2         4 for my $colname (@{$self->{OPTIONS}->{AGGREGATES}})
  2         6  
359             {
360 2         4 my $val = $hr_row->{$colname};
361            
362 2         6 $href->{"SUM_$colname"} += $val;
363            
364 2         5 $href->{"COUNT_$colname"} ++;
365            
366 2         4 my $x = $href->{"MIN_$colname"};
367 2 100 66     16 if (!defined( $x) || $val < $x)
368             {
369 1         4 $href->{"MIN_$colname"} = $val;
370             }
371            
372 2         4 $x = $href->{"MAX_$colname"};
373 2 50 66     33 if (!defined( $x) || $val > $x)
374             {
375 2         9 $href->{"MAX_$colname"} = $val;
376             }
377            
378             }
379            
380             # Do the averages
381            
382             }
383            
384             #
385             # This code isn't very elegant. I need to find a better
386             # way.
387             #
388             sub _format_tails
389             {
390 3     3   7 my ($self,$aref2) = @_;
391 3         5 my $options = $self->{OPTIONS};
392            
393 3   66     11 while (defined($aref2) && defined($#{$aref2}>=0 && $aref2->[-1]->{INNER}))
      66        
394             {
395 3         4 for my $fkey (keys(%{$options->{FORMAT}}))
  3         9  
396             {
397 0         0 my $fref = $options->{FORMAT}->{$fkey};
398 0         0 my $href = $aref2->[-1];
399 0         0 $href->{$fkey} = &$fref($href->{$fkey});
400             }
401            
402 3 50       5 if ( $#{$aref2} >=0)
  3         8  
403             {
404 3         10 $aref2 = $aref2->[-1]->{INNER};
405             }
406 0           else { undef $aref2; }
407             }
408             }
409            
410            
411            
412             1;
413             __END__