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