File Coverage

blib/lib/Math/Formula.pm
Criterion Covered Total %
statement 122 128 95.3
branch 66 78 84.6
condition 25 30 83.3
subroutine 20 21 95.2
pod 7 8 87.5
total 240 265 90.5


line stmt bran cond sub pod time code
1             # Copyrights 2023 by [Mark Overmeer <markov@cpan.org>].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.03.
5             #!/usr/bin/env perl
6             #
7             # This code will be run incredabily fast, hence is tries to avoid copying etc. It
8             # is not always optimally readible when your Perl skills are poor.
9              
10             package Math::Formula;
11 28     28   1523509 use vars '$VERSION';
  28         207  
  28         2016  
12             $VERSION = '0.15';
13              
14              
15 28     28   243 use warnings;
  28         57  
  28         892  
16 28     28   201 use strict;
  28         108  
  28         729  
17 28     28   182 use utf8;
  28         64  
  28         219  
18              
19 28     28   14087 use Log::Report 'math-formula';
  28         3092512  
  28         200  
20 28     28   8381 use Scalar::Util qw/blessed/;
  28         67  
  28         1280  
21              
22 28     28   12910 use Math::Formula::Token;
  28         383  
  28         807  
23 28     28   13756 use Math::Formula::Type;
  28         111  
  28         18638  
24              
25              
26             #--------------------------
27              
28             sub new(%)
29 68     68 1 4887 { my ($class, $name, $expr, %self) = @_;
30 68         180 $self{_name} = $name;
31 68         163 $self{_expr} = $expr;
32 68         283 (bless {}, $class)->init(\%self);
33             }
34              
35             sub init($)
36 68     68 0 161 { my ($self, $args) = @_;
37 68 50       411 my $name = $self->{MSBE_name} = $args->{_name} or panic "every formular requires a name";
38 68 50       216 my $expr = $args->{_expr} or panic "every formular requires an expression";
39 68         168 my $returns = $self->{MSBE_returns} = $args->{returns};
40              
41 68 100 100     548 if(ref $expr eq 'SCALAR')
    50 66        
42 2         15 { $expr = MF::STRING->new(undef, $$expr);
43             }
44             elsif(! ref $expr && $returns && $returns->isa('MF::STRING'))
45 0         0 { $expr = MF::STRING->new(undef, $expr);
46             }
47              
48 68         163 $self->{MSBE_expr} = $expr;
49 68         301 $self;
50             }
51              
52             #--------------------------
53              
54 23     23 1 4825 sub name() { $_[0]->{MSBE_name} }
55 284     284 1 925 sub expression() { $_[0]->{MSBE_expr} }
56 260     260 1 718 sub returns() { $_[0]->{MSBE_returns} }
57              
58              
59             sub tree($)
60 264     264 1 531 { my ($self, $expression) = @_;
61 264   66     1013 $self->{MSBE_ast} ||= $self->_build_ast($self->_tokenize($expression), 0);
62             }
63              
64             # For testing only: to load a new expression without the need to create
65             # a new object.
66             sub _test($$)
67 214     214   106768 { my ($self, $expr) = @_;
68 214         462 $self->{MSBE_expr} = $expr;
69 214         1204 delete $self->{MSBE_ast};
70             }
71              
72             ###
73             ### PARSER
74             ###
75              
76             my $match_int = MF::INTEGER->_match;
77             my $match_float = MF::FLOAT->_match;
78             my $match_name = MF::NAME->_match;
79             my $match_date = MF::DATE->_match;
80             my $match_time = MF::TIME->_match;
81             my $match_tz = MF::TIMEZONE->_match;
82             my $match_dt = MF::DATETIME->_match;
83             my $match_dur = MF::DURATION->_match;
84              
85             my $match_op = join '|',
86             qw{ // -> }, '[?*\/+\-#~.%]',
87             qw{ =~ !~ <=> <= >= == != < > }, # order is important
88             qw{ :(?![0-9][0-9]) (?<![0-9][0-9]): },
89             ( map "$_\\b", qw/ and or not xor exists like unlike cmp lt le eq ne ge gt/
90             );
91              
92             sub _tokenize($)
93 297     297   7304 { my ($self, $s) = @_;
94 297         573 our @t = ();
95 297         451 my $parens_open = 0;
96              
97 28     28   303 use re 'eval'; #XXX needed with newer than 5.16 perls?
  28         78  
  28         46039  
98              
99 297         6023 $s =~ m/ ^
100             (?: \s*
101             (?| \# (?: \s [^\n\r]+ | $ ) \
102 47         1376 | ( true\b | false\b ) (?{ push @t, MF::BOOLEAN->new($+) })
103             | ( \" (?: \\\" | [^"] )* \" )
104 100         4329 (?{ push @t, MF::STRING->new($+) })
105             | ( \' (?: \\\' | [^'] )* \' )
106 5         23 (?{ push @t, MF::STRING->new($+) })
107 39         1379 | ( $match_dur ) (?{ push @t, MF::DURATION->new($+) })
108 13         1413 | ( $match_tz ) (?{ push @t, MF::TIMEZONE->new($+) })
109 311         3596 | ( $match_op ) (?{ push @t, MF::OPERATOR->new($+) })
110 79         2887 | ( $match_name ) (?{ push @t, MF::NAME->new($+) })
111 31         1556 | ( $match_dt ) (?{ push @t, MF::DATETIME->new($+) })
112 23         1294 | ( $match_date ) (?{ push @t, MF::DATE->new($+) })
113 21         3950 | ( $match_time ) (?{ push @t, MF::TIME->new($+) })
114 42         1342 | ( $match_float ) (?{ push @t, MF::FLOAT->new($+) })
115 163         6864 | ( $match_int ) (?{ push @t, MF::INTEGER->new($+) })
116 5         1327 | \( (?{ push @t, MF::PARENS->new('(', ++$parens_open) })
117 5         19 | \) (?{ push @t, MF::PARENS->new(')', $parens_open--) })
118 4         48 | \$ ([1-9][0-9]*) (?{ push @t, MF::CAPTURE->new($+) })
119             | $
120 0         0 | (.+) (?{ error __x"expression '{name}', failed at '{where}'",
121             name => $self->name, where => $+ })
122             )
123             )+ \z /sxo;
124              
125 297 50       1055 ! $parens_open
126             or error __x"expression '{name}', parenthesis do not match", name => $self->name;
127              
128 297         1216 \@t;
129             }
130              
131             sub _build_ast($$)
132 560     560   1042 { my ($self, $t, $prio) = @_;
133 560 100       1874 return shift @$t if @$t < 2;
134              
135             PROGRESS:
136 297         1028 while(my $first = shift @$t)
137             {
138 597 100       2403 if($first->isa('MF::PARENS'))
139 5         22 { my $level = $first->level;
140              
141 5         7 my @nodes;
142 5         18 while(my $node = shift @$t)
143 25 100 100     87 { last if $node->isa('MF::PARENS') && $node->level==$level;
144 20         46 push @nodes, $node;
145             }
146 5         34 $first = $self->_build_ast(\@nodes, 0);
147 5         12 redo PROGRESS;
148             }
149              
150 592 100       1260 if(ref $first eq 'MF::OPERATOR') # unresolved operator
151 38         107 { my $op = $first->token;
152              
153 38 100 100     163 if($op eq '#' || $op eq '.')
154             { # Fragments and Methods are always infix, but their left-side arg
155             # can be left-out. As PREFIX, they would be RTL but we need LTR
156 6         22 unshift @$t, $first;
157 6         23 $first = MF::NAME->new('');
158 6         19 redo PROGRESS;
159             }
160              
161 32 50       158 my $next = $self->_build_ast($t, $prio)
162             or error __x"expression '{name}', monadic '{op}' not followed by anything useful",
163             name => $self->name, op => $op;
164              
165 32         149 $first = MF::PREFIX->new($op, $next);
166 32         110 redo PROGRESS;
167             }
168              
169 554 100       2244 my $next = $t->[0]
170             or return $first; # end of expression
171              
172 288 100       652 if(ref $next ne 'MF::OPERATOR')
173 2 50       8 { if($next->isa('MF::TIMEZONE'))
174             { # Oops, mis-parse
175 2         7 unshift @$t, $next->cast('MF::INTEGER');
176 2         8 $next = MF::OPERATOR->new('+');
177             }
178             else
179 0         0 { error __x"expression '{name}', expected infix operator but found '{type}'",
180             name => $self->name, type => ref $next;
181             }
182             }
183              
184 288         766 my $op = $next->token;
185 288 50       681 @$t or error __x"expression '{name}', infix operator '{op}' requires right-hand argument",
186             name => $self->name, op => $op;
187              
188 288         764 my ($next_prio, $assoc) = MF::OPERATOR->find($op);
189              
190 288 100 66     1152 return $first
      100        
191             if $next_prio < $prio
192             || ($next_prio==$prio && $assoc==MF::OPERATOR::LTR);
193              
194 264 100       556 if($op eq ':')
195 7         23 { return $first;
196             }
197              
198 257         392 shift @$t; # apply the operator
199 257 100       498 if($op eq '?')
200 7         25 { my $then = $self->_build_ast($t, 0);
201 7         10 my $colon = shift @$t;
202 7 0 33     24 $colon && $colon->token eq ':'
    50          
203             or error __x"expression '{name}', expected ':' in '?:', but got '{token}'",
204             name => $self->name, token => ($next ? $colon->token : 'end-of-line');
205              
206 7         21 my $else = $self->_build_ast($t, $next_prio);
207 7         39 $first = MF::TERNARY->new($op, $first, $then, $else);
208 7         20 redo PROGRESS;
209             }
210              
211 250         679 $first = MF::INFIX->new($op, $first, $self->_build_ast($t, $next_prio));
212 250         1127 redo PROGRESS;
213             }
214             }
215              
216             #--------------------------
217              
218             sub evaluate($)
219 275     275 1 3923 { my ($self, $context, %args) = @_;
220 275         649 my $expr = $self->expression;
221              
222 275 50       1383 my $result
    100          
    100          
223             = ref $expr eq 'CODE' ? $self->toType($expr->($context, $self, %args))
224             : ! blessed $expr ? $self->tree($expr)->compute($context)
225             : $expr->isa('Math::Formula::Type') ? $expr
226             : panic;
227              
228             # For external evaluation calls, we must follow the request
229 275   100     1037 my $expect = $args{expect} || $self->returns;
230 275 100 100     1644 $result && $expect && ! $result->isa($expect) ? $result->cast($expect, $context) : $result;
231             }
232              
233              
234             my %_match = map { my $match = $_->_match; ( $_ => qr/^$match$/x ) }
235             qw/MF::DATETIME MF::TIME MF::DATE MF::DURATION/;
236              
237             sub toType($)
238 22     22 1 11324 { my ($self, $data) = @_;
239 22 100       96 if(blessed $data)
240 8 100       51 { return $data if $data->isa('Math::Formula::Type'); # explicit type
241 4 100       17 return MF::DATETIME->new(undef, $data) if $data->isa('DateTime');
242 3 100       23 return MF::DURATION->new(undef, $data) if $data->isa('DateTime::Duration');
243 2 100       12 return MF::FRAGMENT->new($data->name, $data) if $data->isa('Math::Formula::Context');
244             }
245              
246 15     0   65 my $match = sub { my $type = shift; my $match = $type->_match; qr/^$match$/ };
  0         0  
  0         0  
  0         0  
247              
248             return
249             ref $data eq 'SCALAR' ? MF::STRING->new($data)
250             : $data =~ /^[+-]?[0-9]+$/ ? MF::INTEGER->new(undef, $data)
251             : $data =~ /^[+-]?[0-9]+\./ ? MF::FLOAT->new(undef, $data)
252             : $data =~ /^(?:true|false)$/ ? MF::BOOLEAN->new($data)
253             : ref $data eq 'Regexp' ? MF::REGEXP->new(undef, $data)
254             : $data =~ $_match{'MF::DATETIME'} ? MF::DATETIME->new($data)
255             : $data =~ $_match{'MF::TIME'} ? MF::TIME->new($data)
256             : $data =~ $_match{'MF::DATE'} ? MF::DATE->new($data)
257 15 50       331 : $data =~ $_match{'MF::DURATION'} ? MF::DURATION->new($data)
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
258             : $data =~ /^(['"]).*\1$/ ? MF::STRING->new($data)
259             : error __x"not an expression (string needs \\ ) for '{data}'", data => $data;
260             }
261              
262             #--------------------------
263              
264             1;