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; |