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   1269287 use vars '$VERSION';
  28         178  
  28         1569  
12             $VERSION = '0.16';
13              
14              
15 28     28   147 use warnings;
  28         45  
  28         602  
16 28     28   148 use strict;
  28         72  
  28         571  
17 28     28   145 use utf8;
  28         53  
  28         150  
18              
19 28     28   11505 use Log::Report 'math-formula';
  28         2496002  
  28         158  
20 28     28   6669 use Scalar::Util qw/blessed/;
  28         69  
  28         1124  
21              
22 28     28   10143 use Math::Formula::Token;
  28         286  
  28         681  
23 28     28   10822 use Math::Formula::Type;
  28         85  
  28         14557  
24              
25              
26             #--------------------------
27              
28             sub new(%)
29 68     68 1 3596 { my ($class, $name, $expr, %self) = @_;
30 68         147 $self{_name} = $name;
31 68         115 $self{_expr} = $expr;
32 68         233 (bless {}, $class)->init(\%self);
33             }
34              
35             sub init($)
36 68     68 0 137 { my ($self, $args) = @_;
37 68 50       283 my $name = $self->{MSBE_name} = $args->{_name} or panic "every formular requires a name";
38 68 50       180 my $expr = $args->{_expr} or panic "every formular requires an expression";
39 68         174 my $returns = $self->{MSBE_returns} = $args->{returns};
40              
41 68 100 100     433 if(ref $expr eq 'SCALAR')
    50 66        
42 2         8 { $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         129 $self->{MSBE_expr} = $expr;
49 68         250 $self;
50             }
51              
52             #--------------------------
53              
54 23     23 1 5197 sub name() { $_[0]->{MSBE_name} }
55 284     284 1 733 sub expression() { $_[0]->{MSBE_expr} }
56 260     260 1 593 sub returns() { $_[0]->{MSBE_returns} }
57              
58              
59             sub tree($)
60 264     264 1 411 { my ($self, $expression) = @_;
61 264   66     842 $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   98170 { my ($self, $expr) = @_;
68 214         378 $self->{MSBE_expr} = $expr;
69 214         959 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   7510 { my ($self, $s) = @_;
94 297         438 our @t = ();
95 297         375 my $parens_open = 0;
96              
97 28     28   235 use re 'eval'; #XXX needed with newer than 5.16 perls?
  28         54  
  28         36464  
98              
99 297         4866 $s =~ m/ ^
100             (?: \s*
101             (?| \# (?: \s [^\n\r]+ | $ ) \
102 47         1172 | ( true\b | false\b ) (?{ push @t, MF::BOOLEAN->new($+) })
103             | ( \" (?: \\\" | [^"] )* \" )
104 100         3303 (?{ push @t, MF::STRING->new($+) })
105             | ( \' (?: \\\' | [^'] )* \' )
106 5         21 (?{ push @t, MF::STRING->new($+) })
107 39         1170 | ( $match_dur ) (?{ push @t, MF::DURATION->new($+) })
108 13         1002 | ( $match_tz ) (?{ push @t, MF::TIMEZONE->new($+) })
109 311         2812 | ( $match_op ) (?{ push @t, MF::OPERATOR->new($+) })
110 79         2233 | ( $match_name ) (?{ push @t, MF::NAME->new($+) })
111 31         1061 | ( $match_dt ) (?{ push @t, MF::DATETIME->new($+) })
112 23         1036 | ( $match_date ) (?{ push @t, MF::DATE->new($+) })
113 21         3124 | ( $match_time ) (?{ push @t, MF::TIME->new($+) })
114 42         1110 | ( $match_float ) (?{ push @t, MF::FLOAT->new($+) })
115 163         5542 | ( $match_int ) (?{ push @t, MF::INTEGER->new($+) })
116 5         997 | \( (?{ push @t, MF::PARENS->new('(', ++$parens_open) })
117 5         17 | \) (?{ push @t, MF::PARENS->new(')', $parens_open--) })
118 4         16 | \$ ([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       862 ! $parens_open
126             or error __x"expression '{name}', parenthesis do not match", name => $self->name;
127              
128 297         895 \@t;
129             }
130              
131             sub _build_ast($$)
132 560     560   881 { my ($self, $t, $prio) = @_;
133 560 100       1477 return shift @$t if @$t < 2;
134              
135             PROGRESS:
136 297         821 while(my $first = shift @$t)
137             {
138 597 100       1945 if($first->isa('MF::PARENS'))
139 5         14 { my $level = $first->level;
140              
141 5         8 my @nodes;
142 5         12 while(my $node = shift @$t)
143 25 100 100     71 { last if $node->isa('MF::PARENS') && $node->level==$level;
144 20         40 push @nodes, $node;
145             }
146 5         23 $first = $self->_build_ast(\@nodes, 0);
147 5         10 redo PROGRESS;
148             }
149              
150 592 100       1081 if(ref $first eq 'MF::OPERATOR') # unresolved operator
151 38         89 { my $op = $first->token;
152              
153 38 100 100     135 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         11 unshift @$t, $first;
157 6         14 $first = MF::NAME->new('');
158 6         13 redo PROGRESS;
159             }
160              
161 32 50       136 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         135 $first = MF::PREFIX->new($op, $next);
166 32         87 redo PROGRESS;
167             }
168              
169 554 100       1795 my $next = $t->[0]
170             or return $first; # end of expression
171              
172 288 100       512 if(ref $next ne 'MF::OPERATOR')
173 2 50       7 { if($next->isa('MF::TIMEZONE'))
174             { # Oops, mis-parse
175 2         7 unshift @$t, $next->cast('MF::INTEGER');
176 2         6 $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         616 my $op = $next->token;
185 288 50       570 @$t or error __x"expression '{name}', infix operator '{op}' requires right-hand argument",
186             name => $self->name, op => $op;
187              
188 288         615 my ($next_prio, $assoc) = MF::OPERATOR->find($op);
189              
190 288 100 66     930 return $first
      100        
191             if $next_prio < $prio
192             || ($next_prio==$prio && $assoc==MF::OPERATOR::LTR);
193              
194 264 100       439 if($op eq ':')
195 7         14 { return $first;
196             }
197              
198 257         312 shift @$t; # apply the operator
199 257 100       419 if($op eq '?')
200 7         12 { my $then = $self->_build_ast($t, 0);
201 7         9 my $colon = shift @$t;
202 7 0 33     18 $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         11 my $else = $self->_build_ast($t, $next_prio);
207 7         21 $first = MF::TERNARY->new($op, $first, $then, $else);
208 7         17 redo PROGRESS;
209             }
210              
211 250         587 $first = MF::INFIX->new($op, $first, $self->_build_ast($t, $next_prio));
212 250         624 redo PROGRESS;
213             }
214             }
215              
216             #--------------------------
217              
218             sub evaluate($)
219 275     275 1 3571 { my ($self, $context, %args) = @_;
220 275         665 my $expr = $self->expression;
221              
222 275 50       1182 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     881 my $expect = $args{expect} || $self->returns;
230 275 100 100     1275 $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 10918 { my ($self, $data) = @_;
239 22 100       79 if(blessed $data)
240 8 100       48 { return $data if $data->isa('Math::Formula::Type'); # explicit type
241 4 100       16 return MF::DATETIME->new(undef, $data) if $data->isa('DateTime');
242 3 100       18 return MF::DURATION->new(undef, $data) if $data->isa('DateTime::Duration');
243 2 100       10 return MF::FRAGMENT->new($data->name, $data) if $data->isa('Math::Formula::Context');
244             }
245              
246 15     0   51 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       250 : $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;