File Coverage

blib/lib/Data/Formula.pm
Criterion Covered Total %
statement 108 108 100.0
branch 21 24 87.5
condition 1 2 50.0
subroutine 20 20 100.0
pod 1 2 50.0
total 151 156 96.7


line stmt bran cond sub pod time code
1             package Data::Formula;
2              
3 1     1   975 use warnings;
  1         2  
  1         50  
4 1     1   7 use strict;
  1         2  
  1         47  
5 1     1   29 use utf8;
  1         2  
  1         9  
6 1     1   144 use 5.010;
  1         5  
  1         89  
7              
8 1     1   754 use List::MoreUtils qw(any);
  1         51548  
  1         17  
9 1     1   1758 use Moose;
  1         619381  
  1         11  
10              
11             our $VERSION = '0.01';
12              
13             my %operators = (
14             '+' => {
15             method => 'plus',
16             calc => 'plus',
17             prio => 10,
18             },
19             '-' => {
20             method => 'minus',
21             calc => 'minus',
22             prio => 10,
23             },
24             '*' => {
25             method => 'multiply',
26             calc => 'multiply',
27             prio => 50,
28             },
29             '(' => {
30             method => 'bracket_left',
31             },
32             ')' => {
33             method => 'bracket_right',
34             },
35             );
36              
37             has 'variables' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
38             has 'formula' => ( is => 'ro', isa => 'Str', default => sub { [] } );
39             has '_tokens' => ( is => 'ro', isa => 'ArrayRef', lazy_build => 1, );
40             has '_rpn' => ( is => 'ro', isa => 'ArrayRef', lazy_build => 1, );
41             has '_op_indent' => ( is => 'rw', isa => 'Int', default => 0, );
42             has 'used_variables' => ( is => 'ro', isa => 'ArrayRef', lazy_build => 1, );
43              
44             sub _indented_operator {
45 14     14   21 my ($self,$op) = @_;
46             return {
47 14         552 name => $op,
48 14         28 %{$operators{$op}},
49             prio => ($operators{$op}->{prio}+($self->_op_indent*100)),
50             };
51             }
52              
53             sub _build__rpn {
54 4     4   8 my ($self) = @_;
55              
56 4         8 my $rpn = [];
57 4         8 my $ops = [];
58 4         5 foreach my $token (@{$self->_tokens}) {
  4         153  
59 36 100       92 if ($operators{$token}) {
60 18         52 my $rpn_method = '_rpn_method_'.$operators{$token}->{method};
61 18         76 ($rpn,$ops) = $self->$rpn_method($rpn,$ops)
62             }
63             else {
64 18         42 push(@$rpn, $token);
65             }
66             }
67              
68 4         216 return [@$rpn,reverse(@$ops)];
69             }
70              
71             sub _rpn_method_plus {
72 7     7   9 my ($self, $rpn, $ops) = @_;
73 7         19 return $self->rpn_standard_operator('+', $rpn, $ops);
74             }
75             sub _rpn_method_minus {
76 5     5   10 my ($self, $rpn, $ops) = @_;
77 5         17 return $self->rpn_standard_operator('-', $rpn, $ops);
78             }
79             sub _rpn_method_multiply {
80 2     2   5 my ($self, $rpn, $ops) = @_;
81 2         8 return $self->rpn_standard_operator('*', $rpn, $ops);
82             }
83              
84             sub rpn_standard_operator {
85 14     14 0 26 my ($self, $cur_op, $rpn, $ops) = @_;
86 14         601 my $prio = $operators{$cur_op}->{prio}+($self->_op_indent*100);
87 14 100       38 if (@$ops) {
88 10         22 while (@$ops) {
89 14         21 my $prev_op = $ops->[-1];
90 14 100       29 if ($prev_op->{prio} >= $prio) {
91 9         30 push(@$rpn,pop(@$ops));
92             }
93             else {
94 5         11 last;
95             }
96             }
97             }
98 14         46 push(@$ops,$self->_indented_operator($cur_op));
99              
100 14         59 return ($rpn, $ops);
101             }
102              
103             sub _rpn_method_bracket_left {
104 2     2   6 my ($self, $rpn, $ops) = @_;
105              
106 2         85 $self->_op_indent($self->_op_indent + 1);
107              
108 2         6 return ($rpn, $ops);
109             }
110             sub _rpn_method_bracket_right {
111 2     2   4 my ($self, $rpn, $ops) = @_;
112              
113 2         73 $self->_op_indent($self->_op_indent - 1);
114              
115 2         8 return ($rpn, $ops);
116             }
117              
118             sub _build_used_variables {
119 2     2   6 my ($self, @rpn) = @_;
120              
121             return [
122 10         103 grep { $_ !~ m/^[0-9]+$/ }
  20         40  
123 2         77 grep { !$operators{$_} }
124 2         4 @{$self->_tokens}
125             ];
126             }
127              
128             sub _build__tokens {
129 4     4   8 my ($self) = @_;
130              
131 4         10 my @tokens;
132 4         170 my $formula = $self->formula;
133 4         42 $formula =~ s/\s//g;
134              
135 4         23 my $op_regexp = join('',map { q{\\}.$_ } keys %operators);
  20         45  
136 4         17 my $op_regexp_with_variable = '^([^'.$op_regexp.']*?)(['.$op_regexp.'])';
137 4         93 while ($formula =~ m/$op_regexp_with_variable/) {
138 18         37 my $variable = $1;
139 18         31 my $operator = $2;
140 18 100       48 push(@tokens, $variable) if length($variable);
141 18         27 push(@tokens, $operator);
142 18         116 $formula = substr($formula,length($variable.$operator));
143             }
144 4 100       16 if (length($formula)) {
145 3         10 push(@tokens, $formula);
146             }
147              
148             return [
149 36 100       341 map {
150 4         9 $_ =~ m/^[0-9]+$/
151             ? $_+0
152             : $_
153             } @tokens
154             ];
155             }
156              
157             sub _rpn_calc_plus {
158 7     7   15 my ($self, $rpn) = @_;
159              
160 7 50       18 die 'not enough parameters left on stack'
161             unless @$rpn > 1;
162              
163 7         11 my $val2 = pop(@$rpn);
164 7         19 my $val1 = pop(@$rpn);
165              
166 7         13 push(@$rpn,$val1+$val2);
167 7         22 return $rpn;
168             }
169             sub _rpn_calc_minus {
170 5     5   8 my ($self, $rpn) = @_;
171              
172 5 50       16 die 'not enough parameters left on stack'
173             unless @$rpn > 1;
174              
175 5         9 my $val2 = pop(@$rpn);
176 5         10 my $val1 = pop(@$rpn);
177              
178 5         12 push(@$rpn,$val1-$val2);
179 5         17 return $rpn;
180             }
181             sub _rpn_calc_multiply {
182 2     2   5 my ($self, $rpn) = @_;
183              
184 2 50       8 die 'not enough parameters left on stack'
185             unless @$rpn > 1;
186              
187 2         5 my $val2 = pop(@$rpn);
188 2         5 my $val1 = pop(@$rpn);
189              
190 2         6 push(@$rpn,$val1*$val2);
191 2         7 return $rpn;
192             }
193              
194             sub calculate {
195 4     4 1 12160 my ($self, %variables) = @_;
196              
197 4 100       9 if (@{$self->variables} == 0) {
  4         215  
198 2         83 $self->variables([keys %variables]);
199             }
200              
201 4         9 my $rpn = [];
202 4         11 my $ops = [];
203 4         8 foreach my $token (@{$self->_rpn}) {
  4         153  
204 32 100       61 if (ref($token) eq 'HASH') {
205 14         32 my $rpn_method = '_rpn_calc_'.$token->{calc};
206 14         56 ($rpn) = $self->$rpn_method($rpn)
207             }
208             else {
209 18 100       35 if (exists($variables{$token})) {
210 17   50     53 push(@$rpn, $variables{$token} // 0);
211             }
212             else {
213 1         4 push(@$rpn, $token);
214             }
215             }
216             }
217              
218 4         29 return @$rpn[0];
219             }
220              
221             1;
222              
223              
224             __END__
225              
226             =head1 NAME
227              
228             Data::Formula - formulas evaluation and calculation
229              
230             =head1 SYNOPSIS
231              
232             my $df = Data::Formula->new(
233             formula => 'var212 - var213 * var314 + var354',
234             );
235             my $val = $df->calculate(
236             var212 => 5,
237             var213 => 10,
238             var314 => 7,
239             var354 => 100
240             );
241             # 5-(10*7)+100
242              
243             my $df = Data::Formula->new(
244             variables => [qw( var212 var213 n274 n294 var314 var334 var354 var374 var394 )],
245             formula => 'var212 - var213 + var314 * (var354 + var394) - 10',
246             );
247             my $used_variables = $df->used_variables;
248             # [ var212 var213 var314 var354 var394 ]
249              
250             my $val = $df->calculate(
251             var212 => 5,
252             var213 => 10,
253             var314 => 2,
254             var354 => 3,
255             var394 => 9,
256             );
257             # 5-10+2*(3+9)-10
258              
259             =head1 DESCRIPTION
260              
261             evaluate and calulate formulas with variables of the type var212 - var213 + var314 * (var354 + var394) - 10
262              
263             =head1 METHODS
264              
265             =head2 new()
266              
267             Object constructor.
268              
269             my $df = Data::Formula->new(
270             formula => 'var212 - var213 * var314 + var354',
271             );
272              
273             =head2 used_variables()
274              
275             return array with variables used in formula
276              
277             =head2 calculate()
278              
279             evaluate formula with values for variables, returns caluculated value
280              
281             =head1 AUTHOR
282              
283             Jozef Kutej, C<< <jkutej at cpan.org> >>
284              
285             =head1 CONTRIBUTORS
286            
287             The following people have contributed to the File::is by committing their
288             code, sending patches, reporting bugs, asking questions, suggesting useful
289             advises, nitpicking, chatting on IRC or commenting on my blog (in no particular
290             order):
291              
292             Andrea Pavlovic
293              
294             =head1 LICENSE AND COPYRIGHT
295              
296             This program is free software; you can redistribute it and/or modify it
297             under the terms of either: the GNU General Public License as published
298             by the Free Software Foundation; or the Artistic License.
299              
300             See http://dev.perl.org/licenses/ for more information.
301              
302             =cut