File Coverage

lib/Data/Math.pm
Criterion Covered Total %
statement 153 157 97.4
branch 47 60 78.3
condition 35 41 85.3
subroutine 16 16 100.0
pod 6 6 100.0
total 257 280 91.7


line stmt bran cond sub pod time code
1             package Data::Math;
2 12     12   11774 use Moo;
  12         109502  
  12         53  
3 12     12   17471 use MooX::Types::MooseLike::Base qw(:all);
  12         50764  
  12         3415  
4              
5             =head1 NAME
6              
7             Data::Math - arithmetic operations on complex data structures
8              
9             =head1 SYNOPSIS
10              
11             use Data::Math;
12             my $db = Data::Math->new();
13              
14             # add values in two parallel structures
15             my $data_sum = $dm->calc( '+', $data_structure_1, $data_structure_2 );
16              
17              
18             # subtracting data structures
19             %gross = ( de => 2345.37,
20             es => 1238.99,
21             us => 1.98,
22             );
23             %costs = ( de => 35.00,
24             es => 259.11,
25             us => 666.66,
26             );
27             my $net = $dm->calc( '-', \%gross, \%costs );
28              
29             # $net:
30             # { 'de' => '2310.37',
31             # 'es' => '979.88',
32             # 'us' => '-664.68' };
33              
34             =head1 DESCRIPTION
35              
36             Data::Math is for doing arithmetic operations on roughly
37             parallel data structures.
38              
39             It's pretty clear what a line like this would be meant to do,
40             though Perl does nothing useful with it:
41              
42             %net = %gross - %costs;
43              
44             Instead, Data::Math's calc method can be used:
45              
46             my $net = $dm->calc( '-', \%gross, \%costs );
47              
48             The code here is customizeable in many ways, but has
49             defaults that should make it easy to use in simple
50             cases. The arithmetic operator is applied to numbers,
51             strings are just passed through if both sides are the same,
52             or concatenated (with '|' separator) if they differ.
53              
54             If there's a numeric field you don't want to do numeric
55             operations on (e.g. 'tax_rate') you can define a pattern
56             in the object's skip_key_patterns array to skip it.
57              
58             =head2 METHODS
59              
60             =over
61              
62             =item new
63              
64             Creates a new Data::Math object.
65              
66             Takes a hash as an argument (i.e. a list of key/value pairs),
67             to provide named fields that become object attributes.
68             These attributes are:
69              
70             =over
71              
72             =item string_policy
73              
74             If the values aren't numbers, instead of the numeric
75             operation, they'll be handled according to the string_policy.
76             The default is concat_if_differ. If there are two different
77             strings, they will be joined together using the L
78             (if not, the string is just passed through).
79              
80             Other allowed settings for string_policy:
81              
82             "pick_one" if there are two different values, use the first one.
83             "pick_2nd" if there are two different values, use the second.
84              
85             =item join_char
86              
87             Defaults to "|".
88              
89             =item skip_key_patterns
90              
91             Skip numeric operation on keys that match any of this list of patterns.
92              
93             =item skip_policy
94              
95             Default: "pick_one", meaning that when we skip applying the
96             numeric operation, by default we'll just pass through the
97             value unchanged, picking the first if they differ.
98              
99             The set of allowed skip policies is a superset of the string_policies.
100             In addition to a string_policy, there's also the 'remove_key'
101             policy, which will remove the matching keys from the result set.
102              
103             =back
104              
105             =cut
106              
107 12     12   231 use 5.008;
  12         32  
108 12     12   39 use Carp;
  12         15  
  12         542  
109 12     12   48 use Data::Dumper;
  12         10  
  12         481  
110              
111 12     12   523 use List::MoreUtils qw( uniq any );
  12         6572  
  12         124  
112 12     12   4692 use Scalar::Util qw( reftype looks_like_number );
  12         15  
  12         490  
113 12     12   40 use List::Util qw( max );
  12         18  
  12         458  
114              
115 12     12   4603 use Scalar::Classify qw( classify classify_pair );
  12         9459  
  12         9333  
116              
117             has string_policy =>
118             (is => 'rw', isa => Str, default => 'concat_if_differ' );
119              
120             has join_char =>
121             (is => 'rw', isa => Str, default => '|' );
122              
123             # array of qr{}s
124             has skip_key_patterns =>
125             (is => 'rw', isa => ArrayRef );
126              
127             has skip_policy =>
128             (is => 'rw', isa => Str, default => 'pick_one' );
129              
130             our $VERSION = '0.01';
131             my $DEBUG = 0; # unused, at present
132              
133             =item calc
134              
135             Takes an arithmetic operator given as a quoted string as the
136             first argument and applies it to the following references to data
137             structures.
138              
139             Allowed operators: '+', '-', '*', '/' and '%'
140              
141             =cut
142              
143             sub calc {
144 39     39 1 49907 my $self = shift;
145 39         51 my $op = shift;
146              
147 39         35 my ($ds1, $ds2, $new);
148 39         41 $ds1 = shift;
149 39         93 while( @_ ) {
150 41         36 $ds2 = shift;
151              
152             # hack to cover two undefs. For perl: undef + undef = 0
153 41 100 66     85 if ( not ( defined( $ds1 ) ) && not ( defined( $ds2 ) ) ) {
154 1         2 $ds1 = 0;
155 1         3 $ds2 = 0;
156             }
157 41         113 $new = classify_pair( $ds1, $ds2, { mismatch_policy => 'error' } );
158              
159 41         1545 my $ref = \( $new );
160 41         108 $self->do_calc( $ds1, $ds2, $ref, { op => $op } );
161              
162 40         104 $ds1 = $new;
163             }
164 38         78 return $new;
165             }
166              
167             =item do_calc
168              
169             do_calc does recursive descent of two roughly parallel perl
170             structures, performing the indicated operation on each,
171             storing the result in a newly created parallel structure
172             (a reference passed in as the third argument).
173              
174             Typically, the indicated operation is a simple numeric operator,
175             defaulting to '+'. The operator may be supplied as the 'op' option:
176              
177             $self->do_calc( $structure1, $structure2, $result_structure, { op => '-' };
178              
179             =cut
180              
181             sub do_calc {
182 350     350 1 259 my $self = shift;
183 350         231 my $ds1 = shift;
184 350         209 my $ds2 = shift;
185 350         233 my $ref = shift;
186              
187 350         190 my $opt = shift;
188 350   50     549 my $op = $opt->{op} || '+';
189              
190 350         5170 my $skip_key_patterns = $self->skip_key_patterns;
191 350         8363 my $skip_policy = $self->skip_policy;
192              
193 350         4926 my ( $new, $refcode, $class ) =
194             classify_pair( $ds1, $ds2, { mismatch_policy => 'error' } );
195              
196 350 100       10379 return unless defined $refcode;
197              
198             # First, we do the scalar cases
199 340 100       524 if ( $refcode eq ':NUMBER:' ) {
    100          
200 156         295 my $result =
201             $self->numeric_handler( $ds1, $ds2, { op => $op } );
202 156         151 ${ $ref } = $result;
  156         376  
203             }
204             elsif ( $refcode eq ':STRING:' ) {
205 83         108 my $result =
206             $self->string_handler( $ds1, $ds2 );
207 83         65 ${ $ref } = $result;
  83         232  
208             } else { # working on refs
209              
210             # ultimately, we call do_calc recursively but we need to first
211             # put the right default in the new structure we're building up.
212              
213             # Need to expand the ref and call do_calc on each item,
214             # first creating a parallel location, and passing a *ref* to the
215             # parallel location as new_ref.
216              
217 101         65 ${ $ref } = $new;
  101         103  
218              
219 101 100       157 if ($refcode eq 'HASH') {
    50          
220 87         130 my ($keys, $qh1, $qh2) = $self->qualify_hash( $ds1, $ds2 );
221              
222             KEY:
223 87         69 foreach my $k ( @{ $keys } ) {
  87         112  
224 223         190 my $v1 = $qh1->{ $k };
225 223         166 my $v2 = $qh2->{ $k };
226              
227             # skip key feature
228 223         149 foreach my $rule ( @{ $skip_key_patterns } ) {
  223         257  
229 15 100       38 if( $k =~ /$rule/ ) {
230              
231 2 50       4 unless( $skip_policy eq 'remove_key' ) { # TODO other policies?
232              
233             # actually, the default usually works out to just use first value
234 2         4 ${ $ref }->{ $k } =
  2         5  
235             $self->string_handler( $v1, $v2, $skip_policy );
236              
237             # TODO support all string_policy as skip_policy,
238             # by using string_handler like this with a policy override
239             # Also support a skip policy of "treat_as_string"
240             # using object-level string_policy
241              
242             }
243 2         4 next KEY;
244             }
245             }
246              
247 221         453 my $new = classify_pair( $v1, $v2, { mismatch_policy => 'error' } );
248              
249             # Need to assign *this* $new to the given point
250             # to the href in the output structure, using this key
251             # Then get a reference to this and use in recursive call to do_calc.
252             # (Gotta be a better way to express this.)
253 220         5915 ${ $ref }->{ $k } = $new;
  220         273  
254 220         142 my $item_ref = \( ${ $ref }->{ $k } );
  220         185  
255              
256 220         323 $self->do_calc( $v1, $v2, $item_ref, $opt );
257             } # next foreach my $k
258             }
259             elsif ($refcode eq 'ARRAY') {
260 14         25 my ($limit, $vals1, $vals2) = $self->qualify_array( $ds1, $ds2 );
261              
262 14         20 foreach my $i ( 0 .. $limit ) {
263 89         69 my $v1 = $vals1->[ $i ];
264 89         57 my $v2 = $vals2->[ $i ];
265 89         81 my $item_ref = \( $new->[ $i ] );
266 89         118 $self->do_calc( $v1, $v2, $item_ref, $opt );
267             }
268             }
269             }
270             }
271              
272             =item qualify_hash
273              
274             Given two hash references, returns a joint list of keys,
275             and two "qualified" versions of the hashes, where undef
276             values are replaced with default values based on the type
277             of what's in the parallel location in the other hash.
278              
279             Example usage:
280              
281             my ($keys, $qh1, $qh2) = $self->qualify_hash( $ds1, $ds2 );
282              
283             =cut
284              
285             sub qualify_hash {
286 91     91 1 7128 my $self = shift;
287 91         76 my $h1 = shift;
288 91         70 my $h2 = shift;
289              
290             # no warnings 'uninitialized'; # TODO not needed, right?
291              
292 91         59 my @keys = uniq ( keys %{ $h1 }, keys %{ $h2 } );
  91         142  
  91         457  
293              
294 91         140 my ( %new1, %new2 );
295 91         112 foreach my $key ( @keys ) {
296 251         4582 $new1{ $key } = $h1->{ $key };
297 251         224 $new2{ $key } = $h2->{ $key };
298              
299 251         462 classify_pair( $new1{ $key }, $new2{ $key }, { also_qualify => 1 } );
300             }
301              
302 91         2885 return (\@keys, \%new1, \%new2 );
303             }
304              
305             =item qualify_array
306              
307             Given two array references, returns the maximum index limit
308             and two "qualified" versions of the arrays, where undef
309             values are replaced with default values based on the type
310             of what's in the parallel location in the other hash.
311              
312             Example usage:
313              
314             my ( $limit, $aref1, $aref2 ) = $self->qualify_array( $aref1, $aref2 );
315              
316             =cut
317              
318             sub qualify_array {
319 18     18 1 5749 my $self = shift;
320 18   50     30 my $a1 = shift || [];
321 18   50     28 my $a2 = shift || [];
322              
323 18         26 my $policy_opt = { mismatch_policy => 'error'};
324              
325 18         11 my $lim1 = $#{ $a1 };
  18         22  
326 18         16 my $lim2 = $#{ $a2 };
  18         18  
327              
328 18         29 my $limit = max( $lim1, $lim2 );
329              
330             # Make copies (burning memory to avoid touching originals)
331 18         15 my @new1 = @{ $a1 };
  18         30  
332 18         14 my @new2 = @{ $a2 };
  18         21  
333              
334             # replace undefs on one side with default depending on other side:
335             # e.g. 0 '' [] {}
336 18         38 foreach my $i ( 0 .. $limit ) {
337 109         2824 classify_pair( $new1[ $i ], $new2[ $i ], { also_qualify => 1 } );
338             }
339 18         517 return ( $limit, \@new1, \@new2 );
340             }
341              
342              
343              
344             =item numeric_handler
345              
346             Perform the indicated numeric operation on the two arguments.
347             The operation is passed in as an option named "op", included in
348             the options hashref in the third position.
349              
350             Example usage:
351              
352             my $result =
353             $self->numeric_handler( $ds1, $ds2, { op => '-' } );
354              
355             =cut
356              
357             sub numeric_handler {
358 156     156 1 118 my $self = shift;
359 156         105 my $s1 = shift;
360 156         129 my $s2 = shift;
361 156         100 my $opt = shift;
362              
363 156   50     236 my $op = $opt->{op} || '+';
364 156         103 my $result;
365 156 100 100     452 $s1 = 0 if( $s2 && not( $s1 ) );
366 156 100 100     389 $s2 = 0 if( $s1 && not( $s2 ) );
367              
368 156 100       209 if ( $op eq '+' ) {
    50          
    0          
    0          
    0          
369 109         90 $result = $s1 + $s2;
370             } elsif ( $op eq '-' ) {
371 47         48 $result = $s1 - $s2;
372             } elsif ( $op eq '*' ) {
373 0         0 $result = $s1 * $s2;
374             } elsif ( $op eq '/' ) {
375 0         0 $result = $s1 / $s2
376             } elsif ( $op eq '%' ) {
377 0         0 $result = $s1 % $s2
378             }
379 156         165 return $result;
380             }
381              
382             =item string_handler
383              
384             Handle two string arguments, according to the "string_policy"
385             defined for this object. The default string handling behavior is
386             to pass through the existing string if there's just one available
387             or if there are two, to concatenate them using the object's
388             "join_char" (typically a '|').
389              
390             Other allowed values of "string_policy" are:
391              
392             "pick_one" if there are two different values, use the first one.
393             "pick_2nd" if there are two different values, use the second.
394              
395             Example usage:
396              
397             my $result = $self->string_handler( $ds1, $ds2 );
398              
399             # override object level string_policy
400             my $result = $self->string_handler( $ds1, $ds2, 'pick_one' );
401              
402             =cut
403              
404             sub string_handler {
405 108     108 1 9298 my $self = shift;
406 108         83 my $s1 = shift;
407 108         78 my $s2 = shift;
408 108   66     1442 my $policy = shift || $self->string_policy;
409 108         4729 my $join_char = $self->join_char;
410              
411             # silence complaints when doing 'ne' on an undef
412 12     12   64 no warnings 'uninitialized';
  12         13  
  12         2205  
413              
414 108         3250 my $result;
415              
416 108 100 100     352 if ($policy eq 'default' || $policy eq 'concat_if_differ') {
    100          
    50          
417             # print STDERR "MEEP: concat_if_differ\n";
418 90 100       138 if ( $s1 ne $s2 ) {
    50          
419 56 100 100     246 if ( not( defined $s1 ) || $s1 eq '' ) {
    100 100        
420 23         24 $result = $s2;
421             } elsif ( not( defined $s2 ) || $s2 eq '' ) {
422 19         25 $result = $s1;
423             } else {
424 14         23 $result = $s1 . $join_char. $s2;
425             }
426             } elsif ( $s1 eq $s2 ) {
427 34         36 $result = $s1;
428             }
429             }
430             elsif ( $policy eq 'pick_one') {
431              
432 10 100       20 if ( $s1 ne $s2 ) {
    50          
433 8 100 100     44 if ( not( defined $s2 ) || $s2 eq '' ) {
    100 100        
434 2         3 $result = $s1;
435             } elsif ( not( defined $s1 ) || $s1 eq '' ) {
436 2         21 $result = $s2;
437             } else {
438 4         5 $result = $s1; # favor the first if the second is different
439             }
440             } elsif ( $s1 eq $s2 ) {
441 2         2 $result = $s1;
442             }
443             }
444              
445             elsif ( $policy eq 'pick_2nd') {
446 8 100       13 if ( $s1 ne $s2 ) {
    50          
447 7 100 100     40 if ( not( defined $s1 ) || $s1 eq '' ) {
    100 100        
448 2         2 $result = $s2;
449             } elsif ( not( defined $s2 ) || $s2 eq '' ) {
450 2         3 $result = $s1;
451             } else {
452 3         4 $result = $s2; # favor the second if it differs from the first
453             }
454             } elsif ( $s1 eq $s2 ) {
455 1         2 $result = $s2;
456             }
457             } else {
458 0         0 carp "Data::Math: Unsupported string_policy: $policy";
459             }
460 108         148 return $result;
461             }
462              
463             1;
464              
465             # A Mouse/Moose performance tweak
466             __PACKAGE__->meta->make_immutable();
467              
468             1;
469              
470             =back
471              
472             =head1 TODO
473              
474             o look into 'preserve_source' options and such to
475             improve memory efficiency
476              
477             o try an operator overload interface
478              
479             o examine possibility of arbitrary user-defineable
480             operations (pattern/action callbacks?)
481              
482             =head1 AUTHOR
483              
484             Joseph Brenner, Edoom@kzsu.stanford.eduE
485              
486             =head1 COPYRIGHT AND LICENSE
487              
488             Copyright (C) 2016 by Joseph Brenner
489              
490             This library is free software; you can redistribute it and/or modify
491             it under the same terms as Perl itself.
492              
493             See http://dev.perl.org/licenses/ for more information.
494              
495             =cut