File Coverage

blib/lib/Data/Formula.pm
Criterion Covered Total %
statement 134 134 100.0
branch 35 40 87.5
condition 3 4 75.0
subroutine 25 25 100.0
pod 1 2 50.0
total 198 205 96.5


line stmt bran cond sub pod time code
1             package Data::Formula;
2              
3 1     1   726 use warnings;
  1         2  
  1         29  
4 1     1   5 use strict;
  1         1  
  1         15  
5 1     1   3 use utf8;
  1         1  
  1         5  
6 1     1   27 use 5.010;
  1         3  
7              
8 1     1   538 use List::MoreUtils qw(any);
  1         12377  
  1         5  
9 1     1   1505 use Moose;
  1         411012  
  1         8  
10 1     1   6985 use MooseX::StrictConstructor;
  1         28185  
  1         4  
11 1     1   8550 use Carp qw(croak);
  1         2  
  1         1588  
12              
13             our $VERSION = '0.02';
14             our @CARP_NOT;
15              
16             my %operators = (
17             '+' => {
18             method => 'plus',
19             calc => 'plus',
20             prio => 10,
21             },
22             '-' => {
23             method => 'minus',
24             calc => 'minus',
25             prio => 10,
26             },
27             '*' => {
28             method => 'multiply',
29             calc => 'multiply',
30             prio => 50,
31             },
32             '/' => {
33             method => 'divide',
34             calc => 'divide',
35             prio => 50,
36             },
37             '(' => {method => 'bracket_left',},
38             ')' => {method => 'bracket_right',},
39             );
40              
41             has 'variables' => (is => 'rw', isa => 'ArrayRef', default => sub {[]});
42             has 'formula' => (is => 'ro', isa => 'Str', default => sub {[]});
43             has '_tokens' => (is => 'ro', isa => 'ArrayRef', lazy_build => 1,);
44             has '_rpn' => (is => 'ro', isa => 'ArrayRef', lazy_build => 1,);
45             has '_op_indent' => (is => 'rw', isa => 'Int', default => 0,);
46             has 'used_variables' => (is => 'ro', isa => 'ArrayRef', lazy_build => 1,);
47              
48             has 'on_error' => (
49             is => 'rw',
50             predicate => 'has_on_error',
51             clearer => 'clear_on_error',
52             );
53             has 'on_missing_token' => (
54             is => 'rw',
55             predicate => 'has_on_missing_token',
56             clearer => 'clear_on_missing_token',
57             );
58              
59             sub _indented_operator {
60 27     27   38 my ($self, $op) = @_;
61             return {
62             name => $op,
63 27         528 %{$operators{$op}},
64 27         30 prio => ($operators{$op}->{prio} + ($self->_op_indent * 100)),
65             };
66             }
67              
68             sub _build__rpn {
69 13     13   21 my ($self) = @_;
70              
71 13         15 my $rpn = [];
72 13         14 my $ops = [];
73 13         19 foreach my $token (@{$self->_tokens}) {
  13         268  
74 77 100       141 if ($operators{$token}) {
75 37         69 my $rpn_method = '_rpn_method_' . $operators{$token}->{method};
76 37         80 ($rpn, $ops) = $self->$rpn_method($rpn, $ops);
77             }
78             else {
79 40         68 push(@$rpn, $token);
80             }
81             }
82              
83 13         260 return [@$rpn, reverse(@$ops)];
84             }
85              
86             sub _rpn_method_plus {
87 7     7   10 my ($self, $rpn, $ops) = @_;
88 7         11 return $self->rpn_standard_operator('+', $rpn, $ops);
89             }
90              
91             sub _rpn_method_minus {
92 8     8   12 my ($self, $rpn, $ops) = @_;
93 8         15 return $self->rpn_standard_operator('-', $rpn, $ops);
94             }
95              
96             sub _rpn_method_multiply {
97 3     3   7 my ($self, $rpn, $ops) = @_;
98 3         7 return $self->rpn_standard_operator('*', $rpn, $ops);
99             }
100              
101             sub _rpn_method_divide {
102 9     9   16 my ($self, $rpn, $ops) = @_;
103 9         15 return $self->rpn_standard_operator('/', $rpn, $ops);
104             }
105              
106             sub rpn_standard_operator {
107 27     27 0 40 my ($self, $cur_op, $rpn, $ops) = @_;
108 27         548 my $prio = $operators{$cur_op}->{prio} + ($self->_op_indent * 100);
109 27 100       53 if (@$ops) {
110 14         21 while (@$ops) {
111 18         21 my $prev_op = $ops->[-1];
112 18 100       25 if ($prev_op->{prio} >= $prio) {
113 10         22 push(@$rpn, pop(@$ops));
114             }
115             else {
116 8         12 last;
117             }
118             }
119             }
120 27         46 push(@$ops, $self->_indented_operator($cur_op));
121              
122 27         68 return ($rpn, $ops);
123             }
124              
125             sub _rpn_method_bracket_left {
126 5     5   7 my ($self, $rpn, $ops) = @_;
127              
128 5         122 $self->_op_indent($self->_op_indent + 1);
129              
130 5         11 return ($rpn, $ops);
131             }
132              
133             sub _rpn_method_bracket_right {
134 5     5   7 my ($self, $rpn, $ops) = @_;
135              
136 5         106 $self->_op_indent($self->_op_indent - 1);
137              
138 5         12 return ($rpn, $ops);
139             }
140              
141             sub _build_used_variables {
142 2     2   4 my ($self, @rpn) = @_;
143              
144             return [
145 10         61 grep {$_ !~ m/^[0-9]+$/}
146 2         4 grep {!$operators{$_}} @{$self->_tokens}
  20         31  
  2         66  
147             ];
148             }
149              
150             sub _build__tokens {
151 13     13   18 my ($self) = @_;
152              
153 13         19 my @tokens;
154 13         236 my $formula = $self->formula;
155 13         59 $formula =~ s/\s//g;
156              
157 13         44 my $op_regexp = join('', map {q{\\} . $_} keys %operators);
  78         115  
158 13         34 my $op_regexp_with_variable = '^([^' . $op_regexp . ']*?)([' . $op_regexp . '])';
159 13         71 while ($formula =~ m/$op_regexp_with_variable/) {
160 37         58 my $variable = $1;
161 37         39 my $operator = $2;
162 37 100       70 push(@tokens, $variable) if length($variable);
163 37         42 push(@tokens, $operator);
164 37         115 $formula = substr($formula, length($variable . $operator));
165             }
166 13 100       24 if (length($formula)) {
167 9         16 push(@tokens, $formula);
168             }
169              
170 13 100       16 return [map {$_ =~ m/^[0-9]+$/ ? $_ + 0 : $_} @tokens];
  77         413  
171             }
172              
173             sub _rpn_calc_plus {
174 7     7   11 my ($self, $rpn) = @_;
175              
176 7 50       26 die 'not enough parameters left on stack'
177             unless @$rpn > 1;
178              
179 7         11 my $val2 = pop(@$rpn);
180 7         9 my $val1 = pop(@$rpn);
181              
182 7         10 push(@$rpn, $val1 + $val2);
183 7         14 return $rpn;
184             }
185              
186             sub _rpn_calc_minus {
187 8     8   20 my ($self, $rpn) = @_;
188              
189 8 50       14 die 'not enough parameters left on stack'
190             unless @$rpn > 1;
191              
192 8         11 my $val2 = pop(@$rpn);
193 8         31 my $val1 = pop(@$rpn);
194              
195 8         13 push(@$rpn, $val1 - $val2);
196 8         18 return $rpn;
197             }
198              
199             sub _rpn_calc_multiply {
200 3     3   6 my ($self, $rpn) = @_;
201              
202 3 50       8 die 'not enough parameters left on stack'
203             unless @$rpn > 1;
204              
205 3         4 my $val2 = pop(@$rpn);
206 3         4 my $val1 = pop(@$rpn);
207              
208 3         4 push(@$rpn, $val1 * $val2);
209 3         7 return $rpn;
210             }
211              
212             sub _rpn_calc_divide {
213 8     8   13 my ($self, $rpn) = @_;
214              
215 8 50       12 die 'not enough parameters left on stack'
216             unless @$rpn > 1;
217              
218 8         11 my $val2 = pop(@$rpn);
219 8         11 my $val1 = pop(@$rpn);
220              
221 8 100       39 die "Illegal division by zero\n"
222             unless $val2;
223              
224 3         7 push(@$rpn, $val1 / $val2);
225 3         6 return $rpn;
226             }
227              
228             sub calculate {
229 13     13 1 18755 my ($self, %variables) = @_;
230              
231 13 100       21 if (@{$self->variables} == 0) {
  13         306  
232 11         216 $self->variables([keys %variables]);
233             }
234              
235 13         18 my $rpn = [];
236 13         20 my $ops = [];
237 13         15 foreach my $token (@{$self->_rpn}) {
  13         238  
238 66 100       103 if (ref($token) eq 'HASH') {
239 26         36 my $rpn_method = '_rpn_calc_' . $token->{calc};
240 26   100     27 ($rpn) = eval {$self->$rpn_method($rpn)} // [];
  26         53  
241 26 100       47 $self->_report_error($rpn, $@)
242             if $@;
243             }
244             else {
245 40 100       65 if (exists($variables{$token})) {
    100          
246 34   50     64 push(@$rpn, $variables{$token} // 0);
247             }
248             elsif ($token =~ /^[+\-]?\d*\.?\d*$/) {
249 2         4 push(@$rpn, $token);
250             }
251             else {
252 4 100       100 if (my $on_missing = $self->has_on_missing_token) {
253 1 50       3 push(@$rpn, (ref($on_missing) eq 'CODE' ? $on_missing->($token) : $on_missing));
254             }
255             else {
256 3         7 $self->_report_error($rpn,
257             '"' . $token . '" is not a literal number, not a valid token');
258             }
259             }
260             }
261             }
262              
263 11         40 return @$rpn[0];
264             }
265              
266             sub _report_error {
267 8     8   16 my ($self, $rpn, $err) = @_;
268 8         13 local @CARP_NOT = __PACKAGE__;
269 8         15 chomp($err);
270 8 100       183 if ($self->has_on_error) {
271 6         115 my $on_err = $self->on_error;
272 6 100       21 push(@$rpn, (ref($on_err) eq 'CODE' ? $on_err->($err) : $on_err));
273             }
274             else {
275 2         34 croak($err);
276             }
277             }
278              
279             1;
280              
281             __END__
282              
283             =encoding utf8
284              
285             =head1 NAME
286              
287             Data::Formula - formulas evaluation and calculation
288              
289             =head1 SYNOPSIS
290              
291             my $df = Data::Formula->new(
292             formula => 'var212 - var213 * var314 + var354',
293             );
294             my $val = $df->calculate(
295             var212 => 5,
296             var213 => 10,
297             var314 => 7,
298             var354 => 100
299             );
300             # 5-(10*7)+100
301              
302             my $df = Data::Formula->new(
303             variables => [qw( var212 var213 n274 n294 var314 var334 var354 var374 var394 )],
304             formula => 'var212 - var213 + var314 * (var354 + var394) - 10',
305             on_error => undef,
306             on_missing_token => 0,
307             );
308             my $used_variables = $df->used_variables;
309             # [ var212 var213 var314 var354 var394 ]
310              
311             my $val = $df->calculate(
312             var212 => 5,
313             var213 => 10,
314             var314 => 2,
315             var354 => 3,
316             var394 => 9,
317             );
318             # 5-10+2*(3+9)-10
319              
320             =head1 DESCRIPTION
321              
322             evaluate and calulate formulas with variables of the type var212 - var213 + var314 * (var354 + var394) - 10
323              
324             =head1 ACCESSORS
325              
326             =head2 formula
327              
328             Formula for calculation. Required.
329              
330             =head2 on_error
331              
332             Sets what should L</calculate()> return in case of an error. When division
333             by zero happens or unknown tokens are found.
334              
335             Can be a scalar value, like for example C<0> or C<undef>, or a code ref
336             that will be executed with error message as argument.
337              
338             Optional, if not set L</calculate()> will throw an exception in case of an error.
339              
340             =head2 on_missing_token
341              
342             Sets what should happen when there is a missing/unknown token found in
343             formula.
344              
345             Can be a scalar value, like fixed number, or a code ref
346             that will be executed with token name as argument.
347              
348             Optional, if not set L</calculate()> will throw an exception with unknown tokens.
349              
350             =head1 METHODS
351              
352             =head2 new()
353              
354             Object constructor.
355              
356             my $df = Data::Formula->new(
357             formula => 'var212 - var213 * var314 + var354',
358             );
359              
360             =head2 used_variables()
361              
362             return array with variables used in formula
363              
364             =head2 calculate()
365              
366             Evaluate formula with values for variables, returns calculated value.
367              
368             Will throw expetion on division by zero of unknown variables, unless
369             changes by L</on_error> or L</on_missing_token>
370              
371             =head1 AUTHOR
372              
373             Jozef Kutej, C<< <jkutej at cpan.org> >>
374              
375             =head1 CONTRIBUTORS
376            
377             The following people have contributed to the File::is by committing their
378             code, sending patches, reporting bugs, asking questions, suggesting useful
379             advises, nitpicking, chatting on IRC or commenting on my blog (in no particular
380             order):
381              
382             Andrea Pavlovic
383             Thomas Klausner
384              
385             =head1 THANKS
386              
387             Thanks to L<VÖV - Verband Österreichischer Volkshochschulen|http://www.vhs.or.at/>
388             for sponsoring development of this module.
389              
390             =head1 LICENSE AND COPYRIGHT
391              
392             This program is free software; you can redistribute it and/or modify it
393             under the terms of either: the GNU General Public License as published
394             by the Free Software Foundation; or the Artistic License.
395              
396             See http://dev.perl.org/licenses/ for more information.
397              
398             =cut