File Coverage

blib/lib/Math/Formula.pm
Criterion Covered Total %
statement 117 122 95.9
branch 64 76 84.2
condition 25 30 83.3
subroutine 20 21 95.2
pod 7 8 87.5
total 233 257 90.6


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 26     26   1502470 use vars '$VERSION';
  26         194  
  26         1769  
12             $VERSION = '0.14';
13              
14              
15 26     26   162 use warnings;
  26         58  
  26         694  
16 26     26   159 use strict;
  26         103  
  26         612  
17 26     26   144 use utf8;
  26         78  
  26         161  
18              
19 26     26   12628 use Log::Report 'math-formula';
  26         2745377  
  26         174  
20 26     26   7302 use Scalar::Util qw/blessed/;
  26         59  
  26         1163  
21              
22 26     26   11726 use Math::Formula::Token;
  26         75  
  26         790  
23 26     26   11408 use Math::Formula::Type;
  26         99  
  26         16352  
24              
25              
26             #--------------------------
27              
28             sub new(%)
29 66     66 1 4184 { my ($class, $name, $expr, %self) = @_;
30 66         178 $self{_name} = $name;
31 66         125 $self{_expr} = $expr;
32 66         262 (bless {}, $class)->init(\%self);
33             }
34              
35             sub init($)
36 66     66 0 152 { my ($self, $args) = @_;
37 66 50       331 my $name = $self->{MSBE_name} = $args->{_name} or panic "every formular requires a name";
38 66 50       204 my $expr = $args->{_expr} or panic "every formular requires an expression";
39 66         149 my $returns = $self->{MSBE_returns} = $args->{returns};
40              
41 66 100 100     498 if(ref $expr eq 'SCALAR')
    50 66        
42 2         9 { $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 66         161 $self->{MSBE_expr} = $expr;
49 66         323 $self;
50             }
51              
52             #--------------------------
53              
54 23     23 1 5143 sub name() { $_[0]->{MSBE_name} }
55 255     255 1 850 sub expression() { $_[0]->{MSBE_expr} }
56 231     231 1 759 sub returns() { $_[0]->{MSBE_returns} }
57              
58              
59             sub tree($)
60 235     235 1 436 { my ($self, $expression) = @_;
61 235   66     903 $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 185     185   88151 { my ($self, $expr) = @_;
68 185         400 $self->{MSBE_expr} = $expr;
69 185         919 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_dt = MF::DATETIME->_match;
82             my $match_dur = MF::DURATION->_match;
83              
84             my $match_op = join '|',
85             qw{ // }, '[?*\/+\-#~.%]',
86             qw{ =~ !~ <=> <= >= == != < > }, # order is important
87             qw{ :(?![0-9][0-9]) (?<![0-9][0-9]): },
88             ( map "$_\\b", qw/ and or not xor exists like unlike cmp lt le eq ne ge gt/
89             );
90              
91             sub _tokenize($)
92 266     266   6764 { my ($self, $s) = @_;
93 266         473 our @t = ();
94 266         396 my $parens_open = 0;
95              
96 26     26   280 use re 'eval'; #XXX needed with newer than 5.16 perls?
  26         71  
  26         39294  
97              
98 266         4911 $s =~ m/ ^
99             (?: \s*
100             (?| \# (?: \s [^\n\r]+ | $ ) \
101 47         1099 | ( true\b | false\b ) (?{ push @t, MF::BOOLEAN->new($+) })
102             | ( \" (?: \\\" | [^"] )* \" )
103 82         2379 (?{ push @t, MF::STRING->new($+) })
104             | ( \' (?: \\\' | [^'] )* \' )
105 5         23 (?{ push @t, MF::STRING->new($+) })
106 22         1050 | ( $match_dur ) (?{ push @t, MF::DURATION->new($+) })
107 278         3408 | ( $match_op ) (?{ push @t, MF::OPERATOR->new($+) })
108 75         2812 | ( $match_name ) (?{ push @t, MF::NAME->new($+) })
109 31         1300 | ( $match_dt ) (?{ push @t, MF::DATETIME->new($+) })
110 23         1363 | ( $match_date ) (?{ push @t, MF::DATE->new($+) })
111 21         4164 | ( $match_time ) (?{ push @t, MF::TIME->new($+) })
112 41         1277 | ( $match_float ) (?{ push @t, MF::FLOAT->new($+) })
113 156         6627 | ( $match_int ) (?{ push @t, MF::INTEGER->new($+) })
114 5         1212 | \( (?{ push @t, MF::PARENS->new('(', ++$parens_open) })
115 5         25 | \) (?{ push @t, MF::PARENS->new(')', $parens_open--) })
116             | $
117 0         0 | (.+) (?{ error __x"expression '{name}', failed at '{where}'",
118             name => $self->name, where => $+ })
119             )
120             )+ \z /sxo;
121              
122 266 50       883 ! $parens_open
123             or error __x"expression '{name}', parenthesis do not match", name => $self->name;
124              
125 266         1025 \@t;
126             }
127              
128             sub _build_ast($$)
129 496     496   883 { my ($self, $t, $prio) = @_;
130 496 100       1633 return shift @$t if @$t < 2;
131              
132             PROGRESS:
133 262         859 while(my $first = shift @$t)
134             {
135 527 100       1955 if($first->isa('MF::PARENS'))
136 5         18 { my $level = $first->level;
137              
138 5         8 my @nodes;
139 5         21 while(my $node = shift @$t)
140 25 100 100     86 { last if $node->isa('MF::PARENS') && $node->level==$level;
141 20         48 push @nodes, $node;
142             }
143 5         32 $first = $self->_build_ast(\@nodes, 0);
144 5         12 redo PROGRESS;
145             }
146              
147 522 100       1114 if(ref $first eq 'MF::OPERATOR') # unresolved operator
148 36         96 { my $op = $first->token;
149              
150 36 100 100     147 if($op eq '#' || $op eq '.')
151             { # Fragments and Methods are always infix, but their left-side arg
152             # can be left-out. As PREFIX, they would be RTL but we need LTR
153 6         13 unshift @$t, $first;
154 6         16 $first = MF::NAME->new('');
155 6         14 redo PROGRESS;
156             }
157              
158 30 50       129 my $next = $self->_build_ast($t, $prio)
159             or error __x"expression '{name}', monadic '{op}' not followed by anything useful",
160             name => $self->name, op => $op;
161              
162 30         106 $first = MF::PREFIX->new($op, $next);
163 30         96 redo PROGRESS;
164             }
165              
166 486 100       1863 my $next = $t->[0]
167             or return $first; # end of expression
168              
169 250 50       521 ref $next eq 'MF::OPERATOR'
170             or error __x"expression '{name}', expected infix operator but found '{type}'",
171             name => $self->name, type => ref $next;
172              
173 250         606 my $op = $next->token;
174 250 50       605 @$t or error __x"expression '{name}', infix operator '{op}' requires right-hand argument",
175             name => $self->name, op => $op;
176              
177 250         639 my ($next_prio, $assoc) = MF::OPERATOR->find($op);
178              
179 250 100 66     978 return $first
      100        
180             if $next_prio < $prio
181             || ($next_prio==$prio && $assoc==MF::OPERATOR::LTR);
182              
183 231 100       464 if($op eq ':')
184 7         16 { return $first;
185             }
186              
187 224         318 shift @$t; # apply the operator
188 224 100       413 if($op eq '?')
189 7         14 { my $then = $self->_build_ast($t, 0);
190 7         11 my $colon = shift @$t;
191 7 0 33     23 $colon && $colon->token eq ':'
    50          
192             or error __x"expression '{name}', expected ':' in '?:', but got '{token}'",
193             name => $self->name, token => ($next ? $colon->token : 'end-of-line');
194              
195 7         18 my $else = $self->_build_ast($t, $next_prio);
196 7         25 $first = MF::TERNARY->new($op, $first, $then, $else);
197 7         19 redo PROGRESS;
198             }
199              
200 217         568 $first = MF::INFIX->new($op, $first, $self->_build_ast($t, $next_prio));
201 217         657 redo PROGRESS;
202             }
203             }
204              
205             #--------------------------
206              
207             sub evaluate($)
208 246     246 1 3730 { my ($self, $context, %args) = @_;
209 246         513 my $expr = $self->expression;
210              
211 246 50       1238 my $result
    100          
    100          
212             = ref $expr eq 'CODE' ? $self->toType($expr->($context, $self, %args))
213             : ! blessed $expr ? $self->tree($expr)->_compute($context, $self)
214             : $expr->isa('Math::Formula::Type') ? $expr
215             : panic;
216              
217             # For external evaluation calls, we must follow the request
218 246   100     896 my $expect = $args{expect} || $self->returns;
219 246 100 100     1344 $result && $expect && ! $result->isa($expect) ? $result->cast($expect, $context) : $result;
220             }
221              
222              
223             my %_match = map { my $match = $_->_match; ( $_ => qr/^$match$/x ) }
224             qw/MF::DATETIME MF::TIME MF::DATE MF::DURATION/;
225              
226             sub toType($)
227 22     22 1 11339 { my ($self, $data) = @_;
228 22 100       98 if(blessed $data)
229 8 100       60 { return $data if $data->isa('Math::Formula::Type'); # explicit type
230 4 100       21 return MF::DATETIME->new(undef, $data) if $data->isa('DateTime');
231 3 100       23 return MF::DURATION->new(undef, $data) if $data->isa('DateTime::Duration');
232 2 100       10 return MF::FRAGMENT->new($data->name, $data) if $data->isa('Math::Formula::Context');
233             }
234              
235 15     0   60 my $match = sub { my $type = shift; my $match = $type->_match; qr/^$match$/ };
  0         0  
  0         0  
  0         0  
236              
237             return
238             ref $data eq 'SCALAR' ? MF::STRING->new($data)
239             : $data =~ /^[+-]?[0-9]+$/ ? MF::INTEGER->new(undef, $data)
240             : $data =~ /^[+-]?[0-9]+\./ ? MF::FLOAT->new(undef, $data)
241             : $data =~ /^(?:true|false)$/ ? MF::BOOLEAN->new($data)
242             : ref $data eq 'Regexp' ? MF::REGEXP->new(undef, $data)
243             : $data =~ $_match{'MF::DATETIME'} ? MF::DATETIME->new($data)
244             : $data =~ $_match{'MF::TIME'} ? MF::TIME->new($data)
245             : $data =~ $_match{'MF::DATE'} ? MF::DATE->new($data)
246 15 50       330 : $data =~ $_match{'MF::DURATION'} ? MF::DURATION->new($data)
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
247             : $data =~ /^(['"]).*\1$/ ? MF::STRING->new($data)
248             : error __x"not an expression (string needs \\ ) for '{data}'", data => $data;
249             }
250              
251             #--------------------------
252              
253             1;