File Coverage

blib/lib/Data/RunningTotal.pm
Criterion Covered Total %
statement 276 290 95.1
branch 91 114 79.8
condition 24 35 68.5
subroutine 20 21 95.2
pod 7 7 100.0
total 418 467 89.5


line stmt bran cond sub pod time code
1             package Data::RunningTotal;
2              
3              
4 1     1   71726 use 5.005;
  1         6  
  1         50  
5 1     1   6 use strict;
  1         2  
  1         40  
6 1     1   7 use warnings;
  1         8  
  1         36  
7 1     1   15 use Carp;
  1         2  
  1         105  
8 1     1   895 use Data::RunningTotal::Item;
  1         3  
  1         3360  
9              
10             require Exporter;
11              
12             our @ISA = qw(Exporter);
13             our $VERSION = '0.03';
14              
15             # Create a new Running Total object
16             sub new {
17 2     2 1 1621 my $that = shift;
18 2   33     17 my $class = ref($that) || $that;
19 2         8 my %opts = @_;
20              
21 2 50       10 croak("Missing required parameter 'dimensions'") if !exists($opts{dimensions});
22 2 50       9 croak("'dimensions' paramter must be an array ref") if (ref($opts{dimensions}) ne "ARRAY");
23              
24 2         4 my @dimensions = @{$opts{dimensions}};
  2         8  
25              
26             # Remember the dimensions
27 2         15 my %state = (dims => \@dimensions,
28             numDims => scalar(@dimensions),
29             counts => [],
30             pendingUpdates => {},
31             updatesPending => 0,
32             );
33              
34 2         18 return bless(\%state);
35              
36             }
37              
38             # Increment the count for a specific point
39             sub inc {
40 14     14 1 41 my ($self, $time, %opts) = @_;
41              
42 14 50       34 croak("Missing required parameter 'coords'") if !exists($opts{coords});
43 14 50       36 croak("'coords' paramter must be an array ref") if (ref($opts{coords}) ne "ARRAY");
44              
45 14   50     29 my $weight = $opts{weight} || 1;
46              
47 14 50       16 if (scalar(@{$opts{coords}}) != $self->{numDims}) {
  14         37  
48 0         0 croak("Expected $self->{numDims} coordinates, but ".scalar(@{$opts{coords}})." given");
  0         0  
49             }
50            
51 14         21 $self->{updatesPending}++;
52 14         18 push(@{$self->{pendingUpdates}{$time}}, [$weight, @{$opts{coords}}]);
  14         40  
  14         74  
53              
54             }
55              
56             # Decrement the count for a specific point
57             sub dec {
58 7     7 1 19 my ($self, $time, %opts) = @_;
59              
60 7 50       19 croak("Missing required parameter 'coords'") if !exists($opts{coords});
61 7 50       20 croak("'coords' paramter must be an array ref") if (ref($opts{coords}) ne "ARRAY");
62              
63 7   50     17 my $weight = ($opts{weight} || 1) * -1;
64              
65 7 50       8 if (scalar(@{$opts{coords}}) != $self->{numDims}) {
  7         21  
66 0         0 croak("Expected $self->{numDims} coordinates, but ".scalar(@{$opts{coords}})." given");
  0         0  
67             }
68            
69 7         10 $self->{updatesPending}++;
70 7         8 push(@{$self->{pendingUpdates}{$time}}, [$weight, @{$opts{coords}}]);
  7         21  
  7         32  
71              
72             }
73              
74             # Create a new item that can be moved in the space
75             sub newItem {
76 5     5 1 21 my ($self, %opts) = @_;
77 5   50     14 my $weight = $opts{weight} || 1;
78 5         31 return Data::RunningTotal::Item->new($self, $self->{numDims}, $weight);
79             }
80              
81             # Get the count for a specific point or volume in time
82             # Note that the coords can be code refs
83             sub getValue {
84 16     16 1 74 my ($self, $time, %opts) = @_;
85              
86 16 50       47 croak("Missing required parameter 'coords'") if !exists($opts{coords});
87 16 50       46 croak("'coords' paramter must be an array ref") if (ref($opts{coords}) ne "ARRAY");
88              
89 16         40 $self->_applyPending();
90              
91 16         37 my $lastIdx = $self->_findIndexForTime($time);
92 16         42 my ($eligCoords,
93 16         21 $numPermutations) = $self->_getEligibleCoords(@{$opts{coords}});
94              
95             # It is possible that the user asked for a value that has never been seen
96 16 50       39 return 0 if !defined $eligCoords;
97              
98 16         33 my $dimArrays = $self->_getDimArrays($eligCoords, $lastIdx);
99              
100             # Do the real work
101 16         40 return $self->_getValueWalkingBackwards($dimArrays, $numPermutations);
102              
103             }
104              
105             # Get a list of changes to the weight of each of the specified points/volumes
106             sub getChangeList {
107 6     6 1 4064 my ($self, %opts) = @_;
108              
109 6 50       21 croak("Missing required parameter 'coords'") if !exists($opts{coords});
110 6 50       19 croak("'coords' paramter must be an array ref") if (ref($opts{coords}) ne "ARRAY");
111              
112 6         14 $self->_applyPending();
113              
114 6         19 my ($eligCoords,
115 6         8 $numPermutations) = $self->_getEligibleCoords(@{$opts{coords}});
116              
117             # It is possible that the user asked for a value that has never been seen
118 6 50       15 return [] if !defined $eligCoords;
119              
120 6         13 my $dimArrays = $self->_getDimArrays($eligCoords);
121              
122             # Do the real work
123 6         37 return $self->_getListWalkingForwards($dimArrays, $opts{period}, $opts{start}, $opts{end});
124              
125             }
126              
127             # Pass in N change lists and it will combine them into a single
128             # changelist with a time value and N following values
129             sub combineChangeList {
130 1     1 1 3 my ($self, @lists) = @_;
131              
132 1         3 my @res;
133              
134 1         2 my $numLists = scalar(@lists);
135 1         2 my @listIdxs;
136             my @lastVals;
137 1         4 for my $i (0 .. $numLists-1) {
138 2         5 $listIdxs[$i] = 0;
139 2         4 $lastVals[$i] = 0;
140             }
141              
142 1         2 while(1) {
143 4         8 my $minTime = $lists[0][$listIdxs[0]][0];
144 4         7 for my $i (1 .. $numLists-1) {
145 4         9 my $listTime = $lists[$i][$listIdxs[$i]][0];
146 4 100 100     43 $minTime = $listTime if !defined $minTime || defined $listTime && $listTime < $minTime;
      66        
147             }
148              
149 4 100       10 last if !defined $minTime;
150            
151 3         7 for my $i (0 .. $numLists-1) {
152 6         11 my $listTime = $lists[$i][$listIdxs[$i]][0];
153 6 100 100     81 if (defined $listTime && $listTime == $minTime) {
154 3         8 $lastVals[$i] = $lists[$i][$listIdxs[$i]][1];
155 3         5 $listIdxs[$i]++;
156             }
157             }
158 3         9 push(@res, [$minTime, @lastVals]);
159             }
160              
161 1         6 return \@res;
162              
163             }
164              
165              
166             ######################################################################
167             ## Private Methods
168             ######################################################################
169              
170             # Just dump out the state - should be removed before prime time
171             sub _dumpState {
172 0     0   0 my ($self) = @_;
173              
174             # print Dumper $self;
175              
176             }
177              
178             # Apply all the pending updates
179             sub _applyPending {
180 22     22   31 my ($self) = @_;
181              
182 22 100       56 if ($self->{updatesPending} == 0) {
183 20         30 return;
184             }
185              
186             # Clean out last data
187 2         5 @{$self->{counts}} = ();
  2         5  
188 2         4 @{$self->{dimIdxs}} = ();
  2         6  
189              
190 2         3 my %last;
191 2         3 my $idx = 0;
192 2         5 foreach my $time (sort {$a <=> $b} keys(%{$self->{pendingUpdates}})) {
  33         44  
  2         598  
193 16         17 foreach my $update (@{$self->{pendingUpdates}{$time}}) {
  16         35  
194 21         41 my $tref = \%last;
195 21         25 my $coords = $update;
196 21         21 my $weight = shift(@{$coords});
  21         29  
197             # map {$tref->{$_} = {} if !defined($tref->{$_}); $tref = $tref->{$_};} @{$coords};
198 21   100     23 map {$tref = ($tref->{$_} ||= {});} @{$coords};
  84         288  
  21         34  
199 21         40 $tref->{val} += $weight;
200 21         22 push(@{$self->{counts}}, [$time, $tref->{val}, $tref->{lastIdx}]);
  21         71  
201 21         33 $tref->{lastIdx} = $idx;
202 21         45 foreach my $dim (0 .. $self->{numDims}-1) {
203 84         69 push(@{$self->{dimIdxs}[$dim]{$coords->[$dim]}}, $idx);
  84         230  
204             }
205 21         60 $idx++;
206             }
207             }
208 2         16 $self->{updatesPending} = 0;
209              
210             }
211              
212             # Look through the list of counts ordered by time and find the
213             # index for the specified time
214             sub _findIndexForTime {
215 16     16   18 my ($self, $time) = @_;
216              
217             # just do a simple binary search to find the first entry that
218             # is greater than the time or the last entry that equals the time
219 16         18 my $start = 0;
220 16         15 my $end = scalar(@{$self->{counts}})-1;
  16         35  
221 16         18 my $origEnd = $end;
222 16         17 my $lastSmaller;
223             my $lastEqual;
224              
225 16         16 my $notDone = 1;
226 16         33 while ($notDone) {
227              
228 53 100       88 if ($start >= $end) {
229 16         17 $notDone = 0;
230             }
231 53         85 my $idx = int(($end-$start)/2)+$start;
232              
233 53 100       129 if ($self->{counts}[$idx][0] < $time) {
    100          
234 38         40 $lastSmaller = $idx;
235 38         247 $start = $idx+1;
236             }
237             elsif ($self->{counts}[$idx][0] == $time) {
238 4         8 $lastEqual = $idx;
239 4         9 $start = $idx+1;
240             }
241             else {
242 11         23 $end = $idx-1;
243             }
244              
245             }
246              
247 16 100       30 return $lastEqual if defined $lastEqual;
248 13 50       40 return $lastSmaller if defined $lastSmaller;
249 0         0 return $origEnd;
250              
251             }
252              
253              
254             # Go through all the possible coords and produce a list of lists of them
255             sub _getEligibleCoords {
256 22     22   46 my ($self, @coordSels) = @_;
257              
258 22         29 my @res;
259 22         22 my $permutations = 1;
260 22         56 for my $i (0 .. $self->{numDims}-1) {
261 88         91 my @elig;
262 88 100       311 if (ref $coordSels[$i] eq "CODE") {
    100          
    50          
263             # Call the code ref on each possible coordinate, collecting ones that return true
264 2         4 foreach my $coord (keys(%{$self->{dimIdxs}[$i]})) {
  2         7  
265 6 100       22 if (&{$coordSels[$i]}($coord)) {
  6         18  
266 4         26 push(@elig, $coord);
267             }
268             }
269             }
270             elsif (!defined $coordSels[$i]) {
271             # An undefined coordidate selector behaves like a wildcard
272             # foreach my $coord (keys(%{$self->{dimIdxs}[$i]})) {
273             # push(@elig, $coord);
274             # }
275 63         76 push(@res, undef);
276 63         60 $permutations *= scalar(keys(%{$self->{dimIdxs}[$i]}));
  63         119  
277 63         118 next;
278             }
279             elsif (defined $self->{dimIdxs}[$i]{$coordSels[$i]}) {
280 23         42 push(@elig, $coordSels[$i]);
281             }
282              
283 25 50       50 if (scalar(@elig) == 0) {
284             # No coords found for the selector - no value will be found
285 0         0 return undef;
286             }
287            
288 25         36 $permutations *= scalar(@elig);
289 25         53 push(@res, \@elig);
290              
291             }
292              
293 22         74 return (\@res, $permutations);
294              
295             }
296              
297             # This will combine and sort all the selected coordinates into single
298             # arrays per dimension
299             sub _getDimArrays {
300 22     22   33 my ($self, $eligibleCoords, $lastIdx) = @_;
301              
302 22         20 my @res;
303 22         51 for my $i (0 .. $self->{numDims}-1) {
304 88 100       178 if (!defined $eligibleCoords->[$i]) {
  25 100       55  
305             # Do nothing - this indicates that all coordinates should be
306             # included, which is what will happen if we don't include any
307             }
308             elsif (scalar(@{$eligibleCoords->[$i]}) > 1) {
309 2         3 my @coordList;
310 2         3 foreach my $coord (@{$eligibleCoords->[$i]}) {
  2         15  
311 4         9 my $list = $self->{dimIdxs}[$i]{$coord};
312 4 50       7 if (defined $lastIdx) {
313 4         8 my $end = _binarySearch($list, $lastIdx);
314 4         7 @coordList = (@coordList, @{$list}[0..$end]);
  4         16  
315             }
316             else {
317 0         0 @coordList = (@coordList, @{$list});
  0         0  
318             }
319             }
320 2         10 @coordList = sort {$a <=> $b} @coordList;
  16         19  
321 2         5 push(@res, \@coordList);
322             }
323             else {
324 23 100       34 if (defined $lastIdx) {
325 12         36 my $end = _binarySearch($self->{dimIdxs}[$i]{$eligibleCoords->[$i][0]}, $lastIdx);
326 12         22 my @subset = @{$self->{dimIdxs}[$i]{$eligibleCoords->[$i][0]}}[0..$end];
  12         47  
327 12         34 push(@res, \@subset);
328             }
329             else {
330 11         41 push(@res, $self->{dimIdxs}[$i]{$eligibleCoords->[$i][0]});
331             }
332             }
333            
334             }
335            
336             # Special case - if all the selectors were undef, then there
337             # will be no dimension arrays on the result list. Create
338             # a single list that contains all the indices
339 22 100       55 if (scalar(@res) == 0) {
340 8         21 my @allIndices = (0..$lastIdx);
341 8         17 push(@res, \@allIndices);
342             }
343            
344 22         48 return \@res;
345              
346             }
347              
348             sub _getValueWalkingBackwards {
349 16     16   21 my ($self, $dimArrays, $numPermutations) = @_;
350 16         19 my $sum = 0;
351 16         14 my $nDims = scalar(@{$dimArrays});
  16         24  
352 16         21 my $dimBound = $nDims-1;
353 16         16 my %blackList;
354 16         21 my $permsFound = 0;
355              
356 16         15 my @idxs;
357 16         26 for my $i (0 .. $dimBound) {
358 22         21 $idxs[$i] = scalar(@{$dimArrays->[$i]})-1;
  22         61  
359             }
360              
361 16         23 my $currMin = scalar(@{$self->{counts}});
  16         32  
362             mainLoop:
363 16         34 while ($permsFound < $numPermutations) {
364 82         82 my $allTheSame = 1;
365 82         111 foreach my $i (0 .. $dimBound) {
366 106   100     452 while ($idxs[$i] >= 0 && $dimArrays->[$i][$idxs[$i]] > $currMin) {
367 36         128 $idxs[$i]--;
368             }
369 106 100       192 if ($idxs[$i] < 0) {
370 5         9 last mainLoop;
371             }
372 101 100       211 if ($dimArrays->[$i][$idxs[$i]] < $currMin) {
373 82         98 $currMin = $dimArrays->[$i][$idxs[$i]];
374 82 100       215 $allTheSame = 0 unless $i == 0;
375             }
376             }
377              
378 77 100       166 if ($allTheSame) {
379 70 100       130 if ($blackList{$currMin}) {
380             }
381             else {
382 38         65 $sum += $self->{counts}[$currMin][1];
383 38         44 $permsFound++;
384             }
385 70 100       199 $blackList{$self->{counts}[$currMin][2]} = 1 if defined $self->{counts}[$currMin][2];
386 70         76 $idxs[0]--;
387 70 100       236 if ($idxs[0] < 0) {
388 11         21 last mainLoop;
389             }
390             }
391              
392             }
393              
394 16         131 return $sum;
395              
396             }
397              
398              
399             sub _getListWalkingForwards {
400 6     6   20 my ($self, $dimArrays, $period, $start, $end) = @_;
401 6         11 my $currSum = 0;
402 6         8 my $nDims = scalar(@{$dimArrays});
  6         9  
403 6         12 my $dimBound = $nDims-1;
404 6         7 my @res;
405              
406             my @idxs;
407 0         0 my @maxIdx;
408 6         12 for my $i (0 .. $dimBound) {
409 11         17 $idxs[$i] = 0;
410 11         12 $maxIdx[$i] = scalar(@{$dimArrays->[$i]});
  11         27  
411             }
412              
413 6         8 my $currMax = -1;
414              
415 6         18 my $lastSum;
416             my $lastTime;
417             mainLoop:
418 6         8 while (1) {
419 43         48 my $allTheSame = 1;
420 43         120 foreach my $i (0 .. $dimBound) {
421 71   100     320 while ($idxs[$i] < $maxIdx[$i] &&
422             $dimArrays->[$i][$idxs[$i]] < $currMax) {
423 32         121 $idxs[$i]++;
424             }
425 71 100       129 if ($idxs[$i] >= $maxIdx[$i]) {
426 3         7 last mainLoop;
427             }
428 68 100       145 if ($dimArrays->[$i][$idxs[$i]] > $currMax) {
429 45         64 $currMax = $dimArrays->[$i][$idxs[$i]];
430 45 100       125 $allTheSame = 0 unless $i == 0;
431             }
432             }
433              
434 40 100       89 if ($allTheSame) {
435 31 50 33     65 last mainLoop if defined($end) && $self->{counts}[$currMax][0] > $end;
436 31 100       99 $currSum -= $self->{counts}[$self->{counts}[$currMax][2]][1] if defined $self->{counts}[$currMax][2];
437 31         44 $currSum += $self->{counts}[$currMax][1];
438 31         80 _addToPeriodicList(\@res, $period, $self->{counts}[$currMax][0], $currSum);
439             }
440              
441 40         61 $idxs[0]++;
442 40 100       71 if ($idxs[0] >= $maxIdx[0]) {
443 3         7 last mainLoop;
444             }
445             }
446              
447 6         52 return \@res;
448              
449             }
450              
451             # Add the results to a list, taking into account
452             # duplicates for the same time as well as the periodic
453             # requirements
454             sub _addToPeriodicList {
455 31     31   47 my ($list, $period, $time, $value) = @_;
456              
457 31         30 my $adjustedTime;
458 31 50       48 if ($period) {
459 0         0 $adjustedTime = (int($time/$period)+1)*$period;
460             }
461             else {
462 31         39 $adjustedTime = $time;
463             }
464              
465 31         35 my $lastIdx = scalar(@{$list})-1;
  31         42  
466            
467 31 100       65 if ($lastIdx >= 0) {
468 25 50 66     147 if ($list->[$lastIdx][1] == $value) {
    100          
469 0         0 return;
470             }
471             elsif ($lastIdx >= 0 && $list->[$lastIdx][0] == $adjustedTime) {
472             # The penultimate entry on the list may already have this value
473             # don't put it on if it is the same
474 5 50 33     26 if ($lastIdx >= 1 && $list->[$lastIdx-1][1] == $value) {
475 5         6 pop(@{$list});
  5         7  
476             }
477             else {
478 0         0 $list->[$lastIdx] = [$adjustedTime, $value];
479             }
480 5         15 return;
481             }
482             }
483              
484 26         102 $list->[$lastIdx+1] = [$adjustedTime, $value];
485              
486             }
487              
488              
489             # Returns the index of the last value that is equal to the search
490             # value or the first value that is greater.
491             sub _binarySearch {
492 16     16   18 my ($list, $value) = @_;
493              
494 16         18 my $start = 0;
495 16         17 my $end = scalar(@{$list}) - 1;
  16         25  
496 16         20 my $origEnd = $end;
497 16         14 my $notDone = 1;
498 16         49 my $lastSmaller;
499             my $lastEqual;
500            
501 16         36 while ($notDone) {
502              
503 42 100       76 if ($start >= $end) {
504 16         19 $notDone = 0;
505             }
506 42         63 my $idx = int(($end-$start)/2)+$start;
507              
508 42 100       87 if ($list->[$idx] < $value) {
    100          
509 28         29 $lastSmaller = $idx;
510 28         56 $start = $idx+1;
511             }
512             elsif ($list->[$idx] == $value) {
513 10         11 $start = $idx+1;
514 10         25 $lastEqual = $idx;
515             }
516             else {
517 4         9 $end = $idx;
518             }
519              
520             }
521              
522 16 100       36 return $lastEqual if defined $lastEqual;
523 6 50       18 return $lastSmaller if defined $lastSmaller;
524 0           return $origEnd;
525             }
526              
527              
528             1;
529             __END__