File Coverage

blib/lib/Math/Formula.pm
Criterion Covered Total %
statement 119 125 95.2
branch 66 78 84.6
condition 25 30 83.3
subroutine 19 20 95.0
pod 7 8 87.5
total 236 261 90.4


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Math-Formula version 0.18.
2             # The POD got stripped from this file by OODoc version 3.03.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2023-2025 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11             #oodist: *** DO NOT USE THIS VERSION FOR PRODUCTION ***
12             #oodist: This file contains OODoc-style documentation which will get stripped
13             #oodist: during its release in the distribution. You can use this file for
14             #oodist: testing, however the code of this development version may be broken!
15              
16             package Math::Formula;{
17             our $VERSION = '0.18';
18             }
19              
20              
21 28     28   1962934 use warnings;
  28         57  
  28         1766  
22 28     28   306 use strict;
  28         66  
  28         779  
23 28     28   137 use utf8;
  28         56  
  28         139  
24              
25 28     28   17438 use Log::Report 'math-formula';
  28         3373377  
  28         142  
26              
27 28     28   24812 use Math::Formula::Token ();
  28         105  
  28         1001  
28 28     28   17203 use Math::Formula::Type ();
  28         187  
  28         1490  
29              
30 28     28   306 use Scalar::Util qw/blessed/;
  28         93  
  28         22194  
31              
32             #--------------------
33              
34             #--------------------
35              
36             sub new(%)
37 104     104 1 6826263 { my ($class, $name, $expr, %self) = @_;
38 104         298 $self{_name} = $name;
39 104         305 $self{_expr} = $expr;
40 104         1171 (bless {}, $class)->init(\%self);
41             }
42              
43             sub init($)
44 104     104 0 247 { my ($self, $args) = @_;
45 104 50       535 my $name = $self->{MSBE_name} = $args->{_name} or panic "every formular requires a name";
46 104 50       319 my $expr = $args->{_expr} or panic "every formular requires an expression";
47 104         279 my $returns = $self->{MSBE_returns} = $args->{returns};
48              
49 104 100 100     751 if(ref $expr eq 'SCALAR')
    50 66        
50 3         16 { $expr = MF::STRING->new(undef, $$expr);
51             }
52             elsif(! ref $expr && $returns && $returns->isa('MF::STRING'))
53 0         0 { $expr = MF::STRING->new(undef, $expr);
54             }
55              
56 104         246 $self->{MSBE_expr} = $expr;
57 104         603 $self;
58             }
59              
60             #--------------------
61              
62 23     23 1 6495 sub name() { $_[0]->{MSBE_name} }
63 306     306 1 1241 sub expression() { $_[0]->{MSBE_expr} }
64 282     282 1 991 sub returns() { $_[0]->{MSBE_returns} }
65              
66              
67             sub tree($)
68 268     268 1 621 { my ($self, $expression) = @_;
69 268   66     1407 $self->{MSBE_ast} ||= $self->_build_ast($self->_tokenize($expression), 0);
70             }
71              
72             # For testing only: to load a new expression without the need to create
73             # a new object.
74             sub _test($$)
75 214     214   127637 { my ($self, $expr) = @_;
76 214         585 $self->{MSBE_expr} = $expr;
77 214         1452 delete $self->{MSBE_ast};
78             }
79              
80             ###
81             ### PARSER
82             ###
83              
84             my $match_int = MF::INTEGER->_match;
85             my $match_float = MF::FLOAT->_match;
86             my $match_name = MF::NAME->_match;
87             my $match_date = MF::DATE->_match;
88             my $match_time = MF::TIME->_match;
89             my $match_tz = MF::TIMEZONE->_match;
90             my $match_dt = MF::DATETIME->_match;
91             my $match_dur = MF::DURATION->_match;
92              
93             my $match_op = join '|',
94             qw{ // -> }, '[?*\/+\-#~.%]',
95             qw{ =~ !~ <=> <= >= == != < > }, # order is important
96             qw{ :(?![0-9][0-9]) (?<![0-9][0-9]): },
97             ( map "$_\\b", qw/ and or not xor exists like unlike cmp lt le eq ne ge gt/
98             );
99              
100             sub _tokenize($)
101 301     301   6372 { my ($self, $s) = @_;
102 301         589 our @t = ();
103 301         496 my $parens_open = 0;
104              
105 28     28   252 use re 'eval'; #XXX needed with newer than 5.16 perls?
  28         53  
  28         56146  
106              
107 301         33537 $s =~ m/ ^
108             (?: \s*
109             (?| \# (?: \s [^\n\r]+ | $ ) \
110 47         171 | ( true\b | false\b ) (?{ push @t, MF::BOOLEAN->new($+) })
111             | ( \" (?: \\\" | [^"] )* \" )
112 100         432 (?{ push @t, MF::STRING->new($+) })
113             | ( \' (?: \\\' | [^'] )* \' )
114 5         30 (?{ push @t, MF::STRING->new($+) })
115 39         228 | ( $match_dur ) (?{ push @t, MF::DURATION->new($+) })
116 13         112 | ( $match_tz ) (?{ push @t, MF::TIMEZONE->new($+) })
117 313         1374 | ( $match_op ) (?{ push @t, MF::OPERATOR->new($+) })
118 82         469 | ( $match_name ) (?{ push @t, MF::NAME->new($+) })
119 31         157 | ( $match_dt ) (?{ push @t, MF::DATETIME->new($+) })
120 23         88 | ( $match_date ) (?{ push @t, MF::DATE->new($+) })
121 21         155 | ( $match_time ) (?{ push @t, MF::TIME->new($+) })
122 42         187 | ( $match_float ) (?{ push @t, MF::FLOAT->new($+) })
123 166         853 | ( $match_int ) (?{ push @t, MF::INTEGER->new($+) })
124 5         36 | \( (?{ push @t, MF::PARENS->new('(', ++$parens_open) })
125 5         12 | \) (?{ push @t, MF::PARENS->new(')', $parens_open--) })
126 4         30 | \$ ([1-9][0-9]*) (?{ push @t, MF::CAPTURE->new($+) })
127             | $
128 0         0 | (.+) (?{ error __x"expression '{name}', failed at '{where}'",
129             name => $self->name, where => $+ })
130             )
131             )+ \z /sxo;
132              
133 301 50       1319 ! $parens_open
134             or error __x"expression '{name}', parenthesis do not match", name => $self->name;
135              
136 301         1303 \@t;
137             }
138              
139             sub _build_ast($$)
140 566     566   1261 { my ($self, $t, $prio) = @_;
141 566 100       2255 return shift @$t if @$t < 2;
142              
143             PROGRESS:
144 299         1152 while(my $first = shift @$t)
145             {
146 601 100       2935 if($first->isa('MF::PARENS'))
147 5         15 { my $level = $first->level;
148              
149 5         8 my @nodes;
150 5         15 while(my $node = shift @$t)
151 25 100 100     68 { last if $node->isa('MF::PARENS') && $node->level==$level;
152 20         86 push @nodes, $node;
153             }
154 5         24 $first = $self->_build_ast(\@nodes, 0);
155 5         10 redo PROGRESS;
156             }
157              
158 596 100       1471 if(ref $first eq 'MF::OPERATOR') # unresolved operator
159 38         108 { my $op = $first->token;
160              
161 38 100 100     184 if($op eq '#' || $op eq '.')
162             { # Fragments and Methods are always infix, but their left-side arg
163             # can be left-out. As PREFIX, they would be RTL but we need LTR
164 6         16 unshift @$t, $first;
165 6         18 $first = MF::NAME->new('');
166 6         17 redo PROGRESS;
167             }
168              
169 32 50       144 my $next = $self->_build_ast($t, $prio)
170             or error __x"expression '{name}', monadic '{op}' not followed by anything useful",
171             name => $self->name, op => $op;
172              
173 32         115 $first = MF::PREFIX->new($op, $next);
174 32         128 redo PROGRESS;
175             }
176              
177 558 100       2583 my $next = $t->[0]
178             or return $first; # end of expression
179              
180 291 100       703 if(ref $next ne 'MF::OPERATOR')
181 2 50       10 { if($next->isa('MF::TIMEZONE'))
182             { # Oops, mis-parse
183 2         10 unshift @$t, $next->cast('MF::INTEGER');
184 2         7 $next = MF::OPERATOR->new('+');
185             }
186             else
187 0         0 { error __x"expression '{name}', expected infix operator but found '{type}'", name => $self->name, type => ref $next;
188             }
189             }
190              
191 291         867 my $op = $next->token;
192 291 50       801 @$t or error __x"expression '{name}', infix operator '{op}' requires right-hand argument", name => $self->name, op => $op;
193              
194 291         917 my ($next_prio, $assoc) = MF::OPERATOR->find($op);
195              
196 291 100 66     1336 return $first
      100        
197             if $next_prio < $prio
198             || ($next_prio==$prio && $assoc==MF::OPERATOR::LTR);
199              
200 266 100       666 if($op eq ':')
201 7         12 { return $first;
202             }
203              
204 259         521 shift @$t; # apply the operator
205 259 100       595 if($op eq '?')
206 7         14 { my $then = $self->_build_ast($t, 0);
207 7         8 my $colon = shift @$t;
208 7 0 33     17 $colon && $colon->token eq ':'
    50          
209             or error __x"expression '{name}', expected ':' in '?:', but got '{token}'",
210             name => $self->name, token => ($next ? $colon->token : 'end-of-line');
211              
212 7         28 my $else = $self->_build_ast($t, $next_prio);
213 7         18 $first = MF::TERNARY->new($op, $first, $then, $else);
214 7         18 redo PROGRESS;
215             }
216              
217 252         824 $first = MF::INFIX->new($op, $first, $self->_build_ast($t, $next_prio));
218 252         1027 redo PROGRESS;
219             }
220             }
221              
222             #--------------------
223              
224             sub evaluate($)
225 279     279 1 4503 { my ($self, $context, %args) = @_;
226 279         778 my $expr = $self->expression;
227              
228 279 50       1382 my $result
    100          
    100          
229             = ref $expr eq 'CODE' ? $self->toType($expr->($context, $self, %args))
230             : ! blessed $expr ? $self->tree($expr)->compute($context)
231             : $expr->isa('Math::Formula::Type') ? $expr
232             : panic;
233              
234             # For external evaluation calls, we must follow the request
235 279   100     1661 my $expect = $args{expect} || $self->returns;
236 279 100 100     2009 $result && $expect && ! $result->isa($expect) ? $result->cast($expect, $context) : $result;
237             }
238              
239              
240              
241             my %_match = map { my $match = $_->_match; ( $_ => qr/^$match$/x ) }
242             qw/MF::DATETIME MF::TIME MF::DATE MF::DURATION/;
243              
244             sub toType($)
245 22     22 1 13110 { my ($self, $data) = @_;
246 22 100       90 if(blessed $data)
247 8 100       93 { return $data if $data->isa('Math::Formula::Type'); # explicit type
248 4 100       23 return MF::DATETIME->new(undef, $data) if $data->isa('DateTime');
249 3 100       30 return MF::DURATION->new(undef, $data) if $data->isa('DateTime::Duration');
250 2 100       16 return MF::FRAGMENT->new($data->name, $data) if $data->isa('Math::Formula::Context');
251             }
252              
253 15     0   70 my $match = sub { my $type = shift; my $match = $type->_match; qr/^$match$/ };
  0         0  
  0         0  
  0         0  
254              
255             ref $data eq 'SCALAR' ? MF::STRING->new($data)
256             : $data =~ /^[+-]?[0-9]+$/ ? MF::INTEGER->new(undef, $data)
257             : $data =~ /^[+-]?[0-9]+\./ ? MF::FLOAT->new(undef, $data)
258             : $data =~ /^(?:true|false)$/ ? MF::BOOLEAN->new($data)
259             : ref $data eq 'Regexp' ? MF::REGEXP->new(undef, $data)
260             : $data =~ $_match{'MF::DATETIME'} ? MF::DATETIME->new($data)
261             : $data =~ $_match{'MF::TIME'} ? MF::TIME->new($data)
262             : $data =~ $_match{'MF::DATE'} ? MF::DATE->new($data)
263 15 50       412 : $data =~ $_match{'MF::DURATION'} ? MF::DURATION->new($data)
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
264             : $data =~ /^(['"]).*\1$/ ? MF::STRING->new($data)
265             : error __x"not an expression (string needs \\ ) for '{data}'", data => $data;
266             }
267              
268             #--------------------
269              
270             1;