File Coverage

blib/lib/Math/Util/CalculatedValue.pm
Criterion Covered Total %
statement 120 120 100.0
branch 41 42 97.6
condition 23 24 95.8
subroutine 22 22 100.0
pod 15 15 100.0
total 221 223 99.1


line stmt bran cond sub pod time code
1             package Math::Util::CalculatedValue;
2              
3 3     3   24835 use 5.006;
  3         6  
4 3     3   9 use strict;
  3         3  
  3         61  
5 3     3   9 use warnings FATAL => 'all';
  3         3  
  3         122  
6              
7 3     3   11 use Carp qw(confess);
  3         7  
  3         148  
8 3     3   11 use List::Util qw(min max);
  3         3  
  3         3415  
9              
10             =head1 NAME
11              
12             Math::Util::CalculatedValue - math adjustment, which can containe another adjustments
13              
14             =head1 DESCRIPTION
15              
16             Represents an adjustment to a value (which can contain additional adjustments).
17              
18             =cut
19              
20             our $VERSION = '0.07';
21              
22             =head1 SYNOPSIS
23              
24             my $tid = Math::Util::CalculatedValue->new({
25             name => 'time_in_days',
26             description => 'Duration in days',
27             set_by => 'Contract',
28             base_amount => 0,
29             });
30              
31             my $tiy = Math::Util::CalculatedValue->new({
32             name => 'time_in_years',
33             description => 'Duration in years',
34             set_by => 'Contract',
35             base_amount => 1,
36             });
37              
38             my $dpy = Math::Util::CalculatedValue->new({
39             name => 'days_per_year',
40             description => 'days in a year',
41             set_by => 'Contract',
42             base_amount => 365,
43             });
44              
45             $tid->include_adjustment('reset', $tiy);
46             $tid->include_adjustment('multiply', $dpy);
47              
48             print $tid->amount;
49              
50             =head1 ATTRIBUTES
51              
52             =head2 name
53              
54             This is the name of the operation which called this module
55              
56             =cut
57              
58             sub name {
59 173     173 1 112 my ($self) = @_;
60 173         249 return $self->{'name'};
61             }
62              
63             =head2 description
64              
65             This is the description of the operation which called this module
66              
67             =cut
68              
69             sub description {
70 1     1 1 1 my ($self) = @_;
71 1         3 return $self->{'description'};
72             }
73              
74             =head2 set_by
75              
76             This is the name of the module which called this module
77              
78             =cut
79              
80             sub set_by {
81 1     1 1 3 my ($self) = @_;
82 1         3 return $self->{'set_by'};
83             }
84              
85             =head2 base_amount
86              
87             This is the base amount on which the adjustments are to be made
88              
89             =cut
90              
91             sub base_amount {
92 4     4 1 7 my ($self) = @_;
93 4   100     20 return $self->{'base_amount'} || 0;
94             }
95              
96             =head2 metadata
97              
98             Additional information that you wish to include.
99              
100             =cut
101              
102             sub metadata {
103 1     1 1 2 my ($self) = @_;
104 1         3 return $self->{'metadata'};
105             }
106              
107             =head2 minimum
108              
109             The minimum value for amount
110              
111             =cut
112              
113             sub minimum {
114 1     1 1 3 my ($self) = @_;
115 1         4 return $self->{'minimum'};
116             }
117              
118             =head2 maximum
119              
120             The maximum value for amount
121              
122             =cut
123              
124             sub maximum {
125 1     1 1 3 my ($self) = @_;
126 1         3 return $self->{'maximum'};
127             }
128              
129             my %available_adjustments = (
130             'add' => sub { my ($this, $prev) = @_; return $prev + $this->amount; },
131             'multiply' => sub { my ($this, $prev) = @_; return $prev * $this->amount; },
132             'subtract' => sub { my ($this, $prev) = @_; return $prev - $this->amount; },
133             'divide' => sub { my ($this, $prev) = @_; return $prev / $this->amount; },
134             'reset' => sub { my ($this, $prev) = @_; return $this->amount; },
135             'exp' => sub { my ($this, $prev) = @_; return exp($this->amount); },
136             'log' => sub { my ($this, $prev) = @_; return log($this->amount); },
137             'info' => sub { my ($this, $prev) = @_; return $prev; },
138             'absolute' => sub { my ($this, $prev) = @_; return abs($this->amount); },
139             );
140              
141             =head1 Methods
142              
143             =head2 new
144              
145             New instance method
146              
147             =cut
148              
149             sub new { ## no critic (ArgUnpacking)
150 37     37 1 12293 my $class = shift;
151 37 50       73 my %params_ref = ref($_[0]) ? %{$_[0]} : @_;
  37         116  
152              
153 37         48 foreach my $required ('name', 'description', 'set_by') {
154             confess "Attribute $required is required"
155 90 100       267 unless $params_ref{$required};
156             }
157              
158 23         21 my $self = \%params_ref;
159 23         20 my $minimum = $self->{'minimum'};
160 23         20 my $maximum = $self->{'maximum'};
161              
162 23 100 100     91 confess "Provided maximum [$maximum] is less than the provided minimum [$minimum]"
      100        
163             if (defined $minimum
164             and defined $maximum
165             and $maximum < $minimum);
166              
167 21         25 $self->{'calculatedValue'} = 1;
168              
169 21         30 my $obj = bless $self, $class;
170 21         57 return $obj;
171             }
172              
173             =head2 amount
174              
175             This is the final amount from this object, after applying all adjustments.
176              
177             =cut
178              
179             sub amount {
180 134     134 1 775 my $self = shift;
181              
182 134         131 my $value = $self->_verified_cached_value;
183 134 100       172 if (not defined $value) {
184 45         60 $value = $self->_apply_all_adjustments;
185 45         40 my $min = $self->{'minimum'};
186 45 100       76 $value = max($min, $value) if (defined $min);
187 45         46 my $max = $self->{'maximum'};
188 45 100       60 $value = min($max, $value) if (defined $max);
189              
190 45         49 $self->{_cached_amount} = $value;
191             }
192              
193 134         306 return $value;
194             }
195              
196             =head2 adjustments
197              
198             The ordered adjustments (if any) applied to arrive at the final value.
199              
200             =cut
201              
202             sub adjustments {
203 3     3 1 5 my ($self) = @_;
204 3   100     23 return $self->{'_adjustments'} || [];
205             }
206              
207             =head2 include_adjustment
208              
209             Creates the ordered adjustments as per the operation.
210              
211             =cut
212              
213             sub include_adjustment {
214 23     23 1 114 my ($self, $operation, $adjustment) = @_;
215              
216             confess 'Operation [' . $operation . '] is not supported by ' . __PACKAGE__
217 23 100       69 unless ($available_adjustments{$operation});
218 22 100       49 confess 'Supplied adjustment must be type of ' . __PACKAGE__
219             if !ref($adjustment);
220             confess 'Supplied adjustment must be type of' . __PACKAGE__
221 21 100       43 if !$adjustment->{calculatedValue};
222              
223 20         20 delete $self->{_cached_amount};
224 20   100     46 my $adjustments = $self->{'_adjustments'} || [];
225 20         14 push @{$adjustments}, [$operation, $adjustment];
  20         37  
226 20         36 return $self->{'_adjustments'} = $adjustments;
227             }
228              
229             =head2 exclude_adjustment
230              
231             Remove an adjustment by name. Returns the number of instances found and excluded.
232              
233             Excluded items are changed into 'info' so that that still show up but are do not alter the parent value
234              
235             THis can be extremely dangerous, so make sure you know where and why you are doing it.
236              
237             =cut
238              
239             sub exclude_adjustment {
240 63     63 1 35 my ($self, $adj_name) = @_;
241              
242 63         42 my $excluded = 0;
243 63   100     109 my $adjustments = $self->{'_adjustments'} || [];
244 63         36 foreach my $sub_adj (@{$adjustments}) {
  63         53  
245 60         44 my $obj = $sub_adj->[1];
246 60         57 $excluded += $obj->exclude_adjustment($adj_name);
247 60 100       52 if ($obj->name eq $adj_name) {
248 30         17 $sub_adj->[0] = 'info';
249 30         29 $excluded++;
250             }
251             }
252              
253 63 100       94 delete $self->{_cached_amount} if ($excluded);
254              
255 63         57 return $excluded;
256             }
257              
258             =head2 replace_adjustment
259              
260             Replace all instances of the same named adjustment with the provided adjustment
261              
262             Returns the number of instances replaced.
263              
264             =cut
265              
266             sub replace_adjustment {
267 30     30 1 58 my ($self, $replacement) = @_;
268              
269 30 100       50 confess 'Supplied replacement must be type of ' . __PACKAGE__
270             if !ref($replacement);
271              
272             confess 'Supplied replacement must be type of' . __PACKAGE__
273 29 100       48 if !$replacement->{calculatedValue};
274              
275 28         17 my $replaced = 0;
276 28   100     41 my $adjustments = $self->{'_adjustments'} || [];
277 28         17 foreach my $sub_adj (@{$adjustments}) {
  28         28  
278 43         26 my $obj = $sub_adj->[1];
279 43 100       71 $replaced += $obj->replace_adjustment($replacement)
280             if ($obj != $replacement);
281 43 100       37 if ($obj->name eq $replacement->name) {
282 21         16 $sub_adj->[1] = $replacement;
283 21         40 $replaced++;
284             }
285             }
286              
287 28 100       37 delete $self->{_cached_amount} if ($replaced);
288              
289 28         29 return $replaced;
290             }
291              
292             # Loops through the ordered adjustments and performs the operation/adjustment
293             sub _apply_all_adjustments {
294 45     45   30 my ($self) = @_;
295 45   50     70 my $value = $self->{'base_amount'} || 0;
296 45   100     86 my $adjustments = $self->{'_adjustments'} || [];
297 45         29 foreach my $adjustment (@{$adjustments}) {
  45         54  
298 142         201 $value = $available_adjustments{$adjustment->[0]}->($adjustment->[1], $value);
299             }
300 45         48 return $value;
301             }
302              
303             sub _verified_cached_value {
304 204     204   158 my ($self) = @_;
305 204         113 my $can;
306 204 100       260 if (exists $self->{_cached_amount}) {
307 154         101 $can = $self->{_cached_amount};
308 154   100     295 my $adjustments = $self->{'_adjustments'} || [];
309 154         99 foreach my $adjustment (@{$adjustments}) {
  154         160  
310 62 100       69 if (not defined $adjustment->[-1]->_verified_cached_value) {
311 4         4 delete $self->{_cached_amount};
312 4         4 $can = undef;
313 4         4 last;
314             }
315             }
316             }
317 204         279 return $can;
318             }
319              
320             =head2 peek
321              
322             Peek at an included adjustment by name.
323              
324             =cut
325              
326             sub peek {
327 20     20 1 18 my ($self, $adj_name) = @_;
328              
329 20         12 my $picked;
330              
331 20 100       19 if ($self->name eq $adj_name) {
332 3         2 $picked = $self;
333             } else {
334             # Depth first traversal. We assume that if there are two things named the same
335             # in any given CV that they are, in fact, the same value. So we can just return the first one we find.
336 17   100     40 my $adjustments = $self->{'_adjustments'} || [];
337 17         8 foreach my $sub_adj (@{$adjustments}) {
  17         21  
338 14         13 my $obj = $sub_adj->[1];
339 14         14 $picked = $obj->peek($adj_name);
340 14 100       21 last if $picked;
341             }
342             }
343              
344 20         22 return $picked;
345             }
346              
347             =head2 peek_amount
348              
349             Peek at the value of an included adjustment by name.
350              
351             =cut
352              
353             sub peek_amount {
354 3     3 1 3 my ($self, $adj_name) = @_;
355 3         5 my $adj = $self->peek($adj_name);
356 3 100       10 return ($adj) ? $adj->amount : undef;
357             }
358              
359             =head1 AUTHOR
360              
361             binary.com, C<< <rakesh at binary.com> >>
362              
363             =head1 BUGS
364              
365             Please report any bugs or feature requests to C<bug-math-util-calculatedvalue at rt.cpan.org>, or through
366             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Math-Util-CalculatedValue>. I will be notified, and then you'll
367             automatically be notified of progress on your bug as I make changes.
368              
369              
370             =head1 SUPPORT
371              
372             You can find documentation for this module with the perldoc command.
373              
374             perldoc Math::Util::CalculatedValue
375              
376              
377             You can also look for information at:
378              
379             =over 4
380              
381             =item * RT: CPAN's request tracker (report bugs here)
382              
383             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Math-Util-CalculatedValue>
384              
385             =item * AnnoCPAN: Annotated CPAN documentation
386              
387             L<http://annocpan.org/dist/Math-Util-CalculatedValue>
388              
389             =item * CPAN Ratings
390              
391             L<http://cpanratings.perl.org/d/Math-Util-CalculatedValue>
392              
393             =item * Search CPAN
394              
395             L<http://search.cpan.org/dist/Math-Util-CalculatedValue/>
396              
397             =back
398              
399              
400             =head1 ACKNOWLEDGEMENTS
401              
402              
403              
404             =cut
405              
406             1; # End of Math::Util::CalculatedValue