File Coverage

blib/lib/Math/Formula/Type.pm
Criterion Covered Total %
statement 409 433 94.4
branch 282 358 78.7
condition 86 108 79.6
subroutine 106 113 93.8
pod 3 7 42.8
total 886 1019 86.9


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 28     28   1275 use warnings;
  28         48  
  28         765  
6 28     28   124 use strict;
  28         43  
  28         439  
7 28     28   245 use v5.16; # fc
  28         81  
8              
9             package Math::Formula::Type;
10 28     28   119 use vars '$VERSION';
  28         49  
  28         1128  
11             $VERSION = '0.16';
12              
13 28     28   140 use base 'Math::Formula::Token';
  28         47  
  28         2283  
14              
15             #!!! The declarations of all other packages in this file are indented to avoid
16             #!!! indexing by CPAN.
17              
18 28     28   169 use Log::Report 'math-formula', import => [ qw/warning error __x/ ];
  28         41  
  28         157  
19              
20             # Object is an ARRAY. The first element is the token, as read from the formula
21             # or constructed from a computed value. The second is a value, which can be
22             # used in computation. More elements are type specific.
23              
24              
25             #-----------------
26              
27             sub cast($)
28 11     11 1 24 { my ($self, $to, $context) = @_;
29              
30 11 100       345 return MF::STRING->new(undef, $self->token)
31             if $to eq 'MF::STRING';
32              
33 6         18 undef;
34             }
35              
36             # token() is implemented in de base-class ::Token, but documented here
37              
38              
39             # Returns a value as result of a calculation.
40             # nothing to compute for most types: simply itself
41 511     511 0 1098 sub compute { $_[0] }
42              
43              
44 790   100 790 1 6308 sub value { my $self = shift; $self->[1] //= $self->_value($self->[0], @_) }
  790         2400  
45 0     0   0 sub _value { $_[1] }
46              
47              
48 1     1 1 13 sub collapsed($) { $_[0]->token =~ s/\s+/ /gr =~ s/^ //r =~ s/ $//r }
49              
50             sub prefix()
51 0     0 0 0 { my ($self, $op, $context) = @_;
52              
53 0         0 error __x"cannot find prefx operator '{op}' on a {child}",
54             op => $op, child => ref $self;
55             }
56              
57             sub attribute {
58 0     0 0 0 warning __x"cannot find attribute '{attr}' for {class} '{token}'",
59             attr => $_[1], class => ref $_[0], token => $_[0]->token;
60 0         0 undef;
61             }
62              
63             sub infix($@)
64 34     34 0 52 { my $self = shift;
65 34         60 my ($op, $right, $context) = @_;
66              
67 34 100 66     163 if($op eq '.' && $right->isa('MF::NAME'))
68 33 50       103 { if(my $attr = $self->attribute($right->token))
69 33 50       129 { return ref $attr eq 'CODE' ? $attr->($self, @_) : $attr;
70             }
71             }
72              
73             # object used as string
74 1 50       5 return $self->cast('MF::STRING', $context)->infix(@_)
75             if $op eq '~';
76              
77 0         0 error __x"cannot match infix operator '{op}' for ({left} -> {right})",
78             op => $op, left => ref $self, right => ref $right;
79             }
80              
81             #-----------------
82              
83             package
84             MF::BOOLEAN;
85              
86 28     28   14605 use base 'Math::Formula::Type';
  28         58  
  28         10209  
87              
88             # $class->new($token, $value, %options)
89             # When the value is derived from an expression, this should result in 1 or 0
90             sub new($$@)
91 165     165   368 { my ($class, $token, $value) = (shift, shift, shift);
92 165 100       379 defined $token or $value = $value ? 1 : 0;
    100          
93 165         447 $class->SUPER::new($token, $value, @_);
94             }
95              
96             sub prefix($)
97 6     6   13 { my ($self, $op, $context) = @_;
98 6 50       15 if($op eq 'not')
99 6         15 { return MF::BOOLEAN->new(undef, ! $self->value);
100             }
101 0         0 $self->SUPER::prefix($op, $context);
102             }
103              
104             sub infix($$$)
105 26     26   35 { my $self = shift;
106 26         51 my ($op, $right, $context) = @_;
107              
108 26 100       104 if(my $r = $right->isa('MF::BOOLEAN') ? $right : $right->cast('MF::BOOLEAN', $context))
    100          
    50          
109             { # boolean values are 0 or 1, never undef
110 23 50 100     80 my $v = $op eq 'and' ? ($self->value and $r->value)
    100 100        
    100 100        
111             : $op eq 'or' ? ($self->value or $r->value)
112             : $op eq 'xor' ? ($self->value xor $r->value)
113             : undef;
114              
115 23 50       72 return MF::BOOLEAN->new(undef, $v) if defined $v;
116             }
117             elsif($op eq '->')
118 3 50       8 { $self->value or return undef; # case false
119 3         7 my $result = $right->compute($context);
120 3         8 $context->setCaptures([]); # do not leak captures
121 3         9 return $result;
122             }
123              
124 0         0 $self->SUPER::infix(@_);
125             }
126              
127 98 100   98   516 sub _token($) { $_[1] ? 'true' : 'false' }
128 38     38   132 sub _value($) { $_[1] eq 'true' }
129              
130             #-----------------
131              
132             package
133             MF::STRING;
134              
135 28     28   180 use base 'Math::Formula::Type';
  28         42  
  28         2093  
136              
137 28     28   17009 use Unicode::Collate ();
  28         195291  
  28         17112  
138             my $collate = Unicode::Collate->new; #XXX which options do we need?
139              
140             sub new($$@)
141 176     176   753 { my ($class, $token, $value) = (shift, shift, shift);
142 176 100       387 ($token, $value) = (undef, $$token) if ref $token eq 'SCALAR';
143 176         535 $class->SUPER::new($token, $value, @_);
144             }
145              
146 18     18   134 sub _token($) { '"' . ($_[1] =~ s/[\"]/\\$1/gr) . '"' }
147              
148             sub _value($)
149 106   50 106   203 { my $token = $_[1] // '';
150              
151 106 50       815 substr($token, 0, 1) eq '"' ? $token =~ s/^"//r =~ s/"$//r =~ s/\\([\\"])/$1/gr
    100          
152             : substr($token, 0, 1) eq "'" ? $token =~ s/^'//r =~ s/'$//r =~ s/\\([\\'])/$1/gr
153             : $token; # from code
154             }
155              
156             sub cast($)
157 28     28   47 { my ($self, $to) = @_;
158              
159 28 100 66     177 ref $self eq __PACKAGE__ && $to eq 'MF::REGEXP' ? MF::REGEXP->_from_string($self)
    100 66        
160             : ref $self eq __PACKAGE__ && $to eq 'MF::PATTERN' ? MF::PATTERN->_from_string($self)
161             : $self->SUPER::cast($to);
162             }
163              
164             sub infix($$$)
165 60     60   83 { my $self = shift;
166 60         87 my ($op, $right, $context) = @_;
167              
168 60 100 100     260 if($op eq '~')
    100          
    100          
    100          
    100          
169 6 100       24 { my $r = $right->isa('MF::STRING') ? $right : $right->cast('MF::STRING', $context);
170 6 50       22 return MF::STRING->new(undef, $self->value . $r->value) if $r;
171             }
172             elsif($op eq '=~')
173 9 50       37 { if(my $r = $right->isa('MF::REGEXP') ? $right : $right->cast('MF::REGEXP', $context))
    50          
174 9 100       18 { if(my @captures = $self->value =~ $r->regexp)
175 7         30 { $context->setCaptures(\@captures);
176 7         23 return MF::BOOLEAN->new(undef, 1);
177             }
178 2         9 return MF::BOOLEAN->new(undef, 0);
179             }
180             }
181             elsif($op eq '!~')
182 4 50       18 { my $r = $right->isa('MF::REGEXP') ? $right : $right->cast('MF::REGEXP', $context);
183 4 50       14 return MF::BOOLEAN->new(undef, $self->value !~ $r->regexp) if $r;
184             }
185             elsif($op eq 'like' || $op eq 'unlike')
186             { # When expr is CODE, it may produce a qr// instead of a pattern.
187 12 100 66     83 my $r = $right->isa('MF::PATTERN') || $right->isa('MF::REGEXP') ? $right : $right->cast('MF::PATTERN', $context);
188 12 100       47 my $v = ! $r ? undef
    50          
189             : $op eq 'like' ? $self->value =~ $r->regexp : $self->value !~ $r->regexp;
190 12 50       48 return MF::BOOLEAN->new(undef, $v) if $r;
191             }
192             elsif($op eq 'cmp')
193 21 50       52 { my $r = $right->isa('MF::STRING') ? $right : $right->cast('MF::STRING', $context);
194 21         47 return MF::INTEGER->new(undef, $collate->cmp($self->value, $right->value));
195             }
196              
197 8         25 $self->SUPER::infix(@_);
198             }
199              
200             my %string_attrs = (
201             length => sub { MF::INTEGER->new(undef, length($_[0]->value)) },
202             is_empty => sub { MF::BOOLEAN->new(undef, $_[0]->value !~ m/\P{Whitespace}/) },
203             lower => sub { MF::STRING->new(undef, fc($_[0]->value)) },
204             );
205              
206 8 50   8   28 sub attribute($) { $string_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
207              
208             #-----------------
209              
210             package
211             MF::INTEGER;
212              
213 28     28   16680 use base 'Math::Formula::Type';
  28         54  
  28         3036  
214 28     28   173 use Log::Report 'math-formula', import => [ qw/error __x/ ];
  28         53  
  28         198  
215              
216             sub cast($)
217 16     16   35 { my ($self, $to) = @_;
218 16 100       70 $to eq 'MF::BOOLEAN' ? MF::BOOLEAN->new(undef, $_[0]->value == 0 ? 0 : 1)
    100          
    100          
219             : $to eq 'MF::FLOAT' ? MF::FLOAT->new(undef, $_[0]->value)
220             : $self->SUPER::cast($to);
221             }
222              
223             sub prefix($)
224 9     9   16 { my ($self, $op, $context) = @_;
225 9 50       28 $op eq '+' ? $self
    100          
226             : $op eq '-' ? MF::INTEGER->new(undef, - $self->value)
227             : $self->SUPER::prefix($op, $context);
228             }
229              
230             sub infix($$$)
231 58     58   82 { my $self = shift;
232 58         101 my ($op, $right, $context) = @_;
233              
234 58 100 100     298 return $self->cast('MF::BOOLEAN', $context)->infix(@_)
      100        
235             if $op eq 'and' || $op eq 'or' || $op eq 'xor';
236              
237 52 100       182 $right->cast('MF::INTEGER')
238             if $right->isa('MF::TIMEZONE'); # mis-parse
239              
240 52 100 100     164 if($right->isa('MF::INTEGER') || $right->isa('MF::FLOAT'))
241 47 100       144 { my $v = $op eq '+' ? $self->value + $right->value
    100          
    100          
    100          
242             : $op eq '-' ? $self->value - $right->value
243             : $op eq '*' ? $self->value * $right->value
244             : $op eq '%' ? $self->value % $right->value
245             : undef;
246 47 100       116 return ref($right)->new(undef, $v) if defined $v;
247              
248 25 100       61 return MF::INTEGER->new(undef, $self->value <=> $right->value)
249             if $op eq '<=>';
250              
251 1 50       6 return MF::FLOAT->new(undef, $self->value / $right->value)
252             if $op eq '/';
253             }
254              
255 5 100 66     27 return $right->infix($op, $self, @_[2..$#_])
256             if $op eq '*' && $right->isa('MF::DURATION');
257              
258 3         17 $self->SUPER::infix(@_);
259             }
260              
261             my $gibi = 1024 * 1024 * 1024;
262              
263             my $multipliers = '[kMGTEZ](?:ibi)?\b';
264 28     28   128 sub _match { "[0-9][0-9_]* (?:$multipliers)?" }
265              
266             my %multipliers = (
267             k => 1000, M => 1000_000, G => 1000_000_000, T => 1000_000_000_000, E => 1e15, Z => 1e18,
268             kibi => 1024, Mibi => 1024*1024, Gibi => $gibi, Tibi => 1024*$gibi, Eibi => 1024*1024*$gibi,
269             Zibi => $gibi*$gibi,
270             );
271              
272             sub _value($)
273 123 50   123   1119 { my ($v, $m) = $_[1] =~ m/^ ( [0-9]+ (?: _[0-9][0-9][0-9] )* ) ($multipliers)? $/x
274             or error __x"illegal number format for '{string}'", string => $_[1];
275              
276 123 100       809 ($1 =~ s/_//gr) * ($2 ? $multipliers{$2} : 1);
277             }
278              
279             my %int_attrs = (
280             abs => sub { $_[0]->value < 0 ? MF::INTEGER->new(undef, - $_[0]->value) : $_[0] },
281             );
282 2 50   2   10 sub attribute($) { $int_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
283              
284             #-----------------
285              
286             package
287             MF::FLOAT;
288              
289 28     28   20325 use base 'Math::Formula::Type';
  28         69  
  28         2296  
290 28     28   169 use POSIX qw/floor/;
  28         58  
  28         208  
291              
292 29     29   75 sub _match { '[0-9]+ (?: \.[0-9]+ (?: e [+-][0-9]+ )? | e [+-][0-9]+ )' }
293 37     37   236 sub _value($) { $_[1] + 0.0 }
294 24 100   24   166 sub _token($) { my $t = sprintf '%g', $_[1]; $t =~ /[e.]/ ? $t : "$t.0" }
  24         188  
295              
296             sub cast($)
297 2     2   5 { my ($self, $to) = @_;
298 2 100       14 $to eq 'MF::INTEGER' ? MF::INTEGER->new(undef, floor($_[0]->value))
299             : $self->SUPER::cast($to);
300             }
301              
302             sub prefix($$)
303 9     9   15 { my ($self, $op, $context) = @_;
304 9 50       28 $op eq '+' ? $self
    100          
305             : $op eq '-' ? MF::FLOAT->new(undef, - $self->value)
306             : $self->SUPER::prefix($op, $context)
307             }
308              
309             sub infix($$$)
310 17     17   23 { my $self = shift;
311 17         30 my ($op, $right, $context) = @_;
312              
313 17 50 33     81 return $self->cast('MF::BOOLEAN', $context)->infix(@_)
      33        
314             if $op eq 'and' || $op eq 'or' || $op eq 'xor';
315              
316 17 100       59 $right->cast('MF::INTEGER')
317             if $right->isa('MF::TIMEZONE'); # mis-parse
318              
319 17 50 66     62 if($right->isa('MF::FLOAT') || $right->isa('MF::INTEGER'))
320             { # Perl will upgrade the integers
321 17 100       76 my $v = $op eq '+' ? $self->value + $right->value
    100          
    100          
    100          
    100          
322             : $op eq '-' ? $self->value - $right->value
323             : $op eq '*' ? $self->value * $right->value
324             : $op eq '%' ? $self->value % $right->value
325             : $op eq '/' ? $self->value / $right->value
326             : undef;
327 17 100       53 return MF::FLOAT->new(undef, $v) if defined $v;
328              
329 6 50       18 return MF::INTEGER->new(undef, $self->value <=> $right->value)
330             if $op eq '<=>';
331             }
332 0         0 $self->SUPER::infix(@_);
333             }
334              
335             # I really do not want a math library in here! Use formulas with CODE expr
336             # my %float_attrs;
337             #sub attribute($) { $float_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
338              
339              
340             #-----------------
341              
342             package
343             MF::DATETIME;
344              
345 28     28   12958 use base 'Math::Formula::Type';
  28         61  
  28         2296  
346 28     28   18903 use DateTime ();
  28         10988359  
  28         23728  
347            
348             sub _match {
349 56     56   124 '[12][0-9]{3} \- (?:0[1-9]|1[012]) \- (?:0[1-9]|[12][0-9]|3[01]) T '
350             . '(?:[01][0-9]|2[0-3]) \: [0-5][0-9] \: (?:[0-5][0-9]) (?:\.[0-9]+)?'
351             . '(?:[+-][0-9]{4})?';
352             }
353              
354 4     4   31 sub _token($) { $_[1]->datetime . ($_[1]->time_zone->name =~ s/UTC$/+0000/r) }
355              
356             sub _value($)
357 37     37   66 { my ($self, $token) = @_;
358 37 50       220 $token =~ m/^
359             ([12][0-9]{3}) \- (0[1-9]|1[012]) \- (0[1-9]|[12][0-9]|3[01]) T
360             ([01][0-9]|2[0-3]) \: ([0-5][0-9]) \: ([0-5][0-9]|6[01]) (?:(\.[0-9]+))?
361             ([+-] [0-9]{4})?
362             $ /x or return;
363              
364 37   100     122 my $tz_offset = $8 // '+0000'; # careful with named matches :-(
365 37   100     238 my @args = ( year => $1, month => $2, day => $3, hour => $4, minute => $5, second => $6,
366             nanosecond => ($7 // 0) * 1_000_000_000 );
367 37         119 my $tz = DateTime::TimeZone::OffsetOnly->new(offset => $tz_offset);
368              
369 37         5005 DateTime->new(@args, time_zone => $tz);
370             }
371              
372             sub _to_time($)
373 2     2   501 { +{ hour => $_[0]->hour, minute => $_[0]->minute, second => $_[0]->second, ns => $_[0]->nanosecond };
374             }
375              
376             sub cast($)
377 2     2   8 { my ($self, $to) = @_;
378 2 50       14 $to eq 'MF::TIME' ? MF::TIME->new(undef, _to_time($_[0]->value))
    100          
379             : $to eq 'MF::DATE' ? MF::DATE->new(undef, $_[0]->value->clone)
380             : $self->SUPER::cast($to);
381             }
382              
383             sub infix($$$@)
384 21     21   25 { my $self = shift;
385 21         35 my ($op, $right, $context) = @_;
386              
387 21 100 100     78 if($op eq '+' || $op eq '-')
388 3         6 { my $dt = $self->value->clone;
389 3 100       717 if($right->isa('MF::DURATION'))
390 2 100       10 { my $v = $op eq '+' ? $dt->add_duration($right->value) : $dt->subtract_duration($right->value);
391 2         1569 return MF::DATETIME->new(undef, $v);
392             }
393 1 50       5 if($op eq '-')
394 1 50       5 { my $r = $right->isa('MF::DATETIME') ? $right : $right->cast('MF::DATETIME', $context);
395 1         3 return MF::DURATION->new(undef, $dt->subtract_datetime($right->value));
396             }
397             }
398              
399 18 100       33 if($op eq '<=>')
400 8 100       28 { return MF::INTEGER->new(undef, DateTime->compare($self->value, $right->value))
401             if $right->isa('MF::DATETIME');
402              
403 3 50       8 if($right->isa('MF::DATE'))
404             { # Many timezone problems solved by DateTime
405 3         8 my $date = $right->token;
406 3 50       15 my $begin = $self->_value($date =~ /\+/ ? $date =~ s/\+/T00:00:00+/r : $date.'T00:00:00');
407 3 100       676 return MF::INTEGER->new(undef, -1) if DateTime->compare($begin, $self->value) > 0;
408              
409 2 50       559 my $end = $self->_value($date =~ /\+/ ? $date =~ s/\+/T23:59:59+/r : $date.'T23:59:59');
410 2 100       436 return MF::INTEGER->new(undef, DateTime->compare($self->value, $end) > 0 ? 1 : 0);
411             }
412             }
413              
414 10         26 $self->SUPER::infix(@_);
415             }
416              
417             my %dt_attrs = (
418             'time' => sub { MF::TIME->new(undef, _to_time($_[0]->value)) },
419             date => sub { MF::DATE->new(undef, $_[0]->value) }, # dt's are immutable
420             hour => sub { MF::INTEGER->new(undef, $_[0]->value->hour) },
421             minute => sub { MF::INTEGER->new(undef, $_[0]->value->minute) },
422             second => sub { MF::INTEGER->new(undef, $_[0]->value->second) },
423             fracsec => sub { MF::FLOAT ->new(undef, $_[0]->value->fractional_second) },
424             );
425              
426             sub attribute($)
427 10 50 66 10   43 { $dt_attrs{$_[1]} || $MF::DATE::date_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]);
428             }
429              
430             #-----------------
431              
432             package
433             MF::DATE;
434              
435 28     28   276 use base 'Math::Formula::Type';
  28         60  
  28         3774  
436              
437 28     28   187 use Log::Report 'math-formula', import => [ qw/error warning __x/ ];
  28         76  
  28         303  
438              
439 28     28   5261 use DateTime::TimeZone ();
  28         70  
  28         539  
440 28     28   136 use DateTime::TimeZone::OffsetOnly ();
  28         52  
  28         23440  
441              
442 56     56   115 sub _match { '[12][0-9]{3} \- (?:0[1-9]|1[012]) \- (?:0[1-9]|[12][0-9]|3[01]) (?:[+-][0-9]{4})?' }
443              
444 5     5   37 sub _token($) { $_[1]->ymd . ($_[1]->time_zone->name =~ s/UTC$/+0000/r) }
445              
446             sub _value($)
447 11     11   22 { my ($self, $token) = @_;
448 11 50       61 $token =~ m/^
449             ([12][0-9]{3}) \- (0[1-9]|1[012]) \- (0[1-9]|[12][0-9]|3[01])
450             ([+-] [0-9]{4})?
451             $ /x or return;
452              
453 11   100     35 my $tz_offset = $4 // '+0000'; # careful with named matches :-(
454 11         38 my @args = ( year => $1, month => $2, day => $3);
455 11         52 my $tz = DateTime::TimeZone::OffsetOnly->new(offset => $tz_offset);
456              
457 11         1822 DateTime->new(@args, time_zone => $tz);
458             }
459              
460             sub cast($)
461 3     3   8 { my ($self, $to) = @_;
462 3 100       9 if($to eq 'MF::INTEGER')
463             { # In really exceptional cases, an integer expression can be mis-detected as DATE
464 2         5 bless $self, 'MF::INTEGER';
465 2         102 $self->[0] = $self->[1] = eval "$self->[0]";
466 2         11 return $self;
467             }
468              
469 1 50       3 if($to eq 'MF::DATETIME')
470 1         3 { my $t = $self->token;
471 1 50       7 my $dt = $t =~ /\+/ ? $t =~ s/\+/T00:00:00+/r : $t . 'T00:00:00';
472 1         10 return MF::DATETIME->new($dt);
473             }
474              
475 0         0 $self->SUPER::cast($to);
476             }
477              
478             sub infix($$@)
479 12     12   13 { my $self = shift;
480 12         22 my ($op, $right, $context) = @_;
481              
482 12 100 100     35 if($op eq '+' && $right->isa('MF::TIME'))
483 1         3 { my $l = $self->value;
484 1         262 my $r = $right->value;
485             my $v = DateTime->new(year => $l->year, month => $l->month, day => $l->day,
486             hour => $r->{hour}, minute => $r->{minute}, second => $r->{second},
487 1         4 nanosecond => $r->{ns}, time_zone => $l->time_zone);
488              
489 1         247 return MF::DATETIME->new(undef, $v);
490             }
491              
492 11 100 100     35 if($op eq '-' && $right->isa('MF::DATE'))
493 1         4 { return MF::DURATION->new(undef, $self->value->clone->subtract_datetime($right->value));
494             }
495              
496 10 100 100     34 if($op eq '+' || $op eq '-')
497 2 50       8 { my $r = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION', $context);
498 2 50 33     9 ! $r || $r->token !~ m/T.*[1-9]/
499             or error __x"only duration with full days with DATE, found '{value}'",
500             value => $r->token;
501              
502 2         18 my $dt = $self->value->clone;
503 2 100       535 my $v = $op eq '+' ? $dt->add_duration($right->value) : $dt->subtract_duration($right->value);
504 2         1796 return MF::DATE->new(undef, $v);
505             }
506              
507 8 100       16 if($op eq '<=>')
508 4 50       14 { my $r = $right->isa('MF::DATE') ? $right : $right->cast('MF::DATE', $context);
509 4         9 my ($ld, $ltz) = $self->token =~ m/(.{10})(.*)/;
510 4         10 my ($rd, $rtz) = $r->token =~ m/(.{10})(.*)/;
511              
512             # It is probably a configuration issue when you configure this.
513 4 100 50     20 $ld ne $rd || ($ltz //'') eq ($rtz //'')
      50        
      100        
514             or warning __x"dates '{first}' and '{second}' do not match on timezone",
515             first => $self->token, second => $r->token;
516              
517 4         108 return MF::INTEGER->new(undef, $ld cmp $rd);
518             }
519              
520 4         13 $self->SUPER::infix(@_);
521             }
522              
523             our %date_attrs = (
524             year => sub { MF::INTEGER->new(undef, $_[0]->value->year) },
525             month => sub { MF::INTEGER->new(undef, $_[0]->value->month) },
526             day => sub { MF::INTEGER->new(undef, $_[0]->value->day) },
527             timezone => sub { MF::TIMEZONE->new($_[0]->value->time_zone->name) },
528             );
529 4 50   4   16 sub attribute($) { $date_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
530              
531             #-----------------
532              
533             package
534             MF::TIME;
535 28     28   207 use base 'Math::Formula::Type';
  28         78  
  28         2648  
536              
537 28     28   180 use constant GIGA => 1_000_000_000;
  28         51  
  28         23085  
538              
539 56     56   126 sub _match { '(?:[01][0-9]|2[0-3]) \: [0-5][0-9] \: (?:[0-5][0-9]) (?:\.[0-9]+)?' }
540              
541             sub _token($)
542 7     7   10 { my $time = $_[1];
543 7         12 my $ns = $time->{ns};
544 7 100       30 my $frac = $ns ? sprintf(".%09d", $ns) =~ s/0+$//r : '';
545 7         62 sprintf "%02d:%02d:%02d%s", $time->{hour}, $time->{minute}, $time->{second}, $frac;
546             }
547              
548             sub _value($)
549 23     23   48 { my ($self, $token) = @_;
550 23 50       102 $token =~ m/^ ([01][0-9]|2[0-3]) \: ([0-5][0-9]) \: ([0-5][0-9]) (?:(\.[0-9]+))? $/x
551             or return;
552              
553 23   100     231 +{ hour => $1+0, minute => $2+0, second => $3+0, ns => ($4 //0) * GIGA };
554             }
555              
556             our %time_attrs = (
557             hour => sub { MF::INTEGER->new(undef, $_[0]->value->{hour}) },
558             minute => sub { MF::INTEGER->new(undef, $_[0]->value->{minute}) },
559             second => sub { MF::INTEGER->new(undef, $_[0]->value->{second}) },
560             fracsec => sub { my $t = $_[0]->value; MF::FLOAT->new(undef, $t->{second} + $t->{ns}/GIGA) },
561             );
562              
563 10 50   10   49 sub attribute($) { $time_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
564              
565             sub _sec_diff($$)
566 8     8   16 { my ($self, $diff, $ns) = @_;
567 8 100       26 if($ns < 0) { $ns += GIGA; $diff -= 1 }
  1 50       2  
  1         2  
568 0         0 elsif($ns > GIGA) { $ns -= GIGA; $diff += 1 }
  0         0  
569              
570 8         22 my $sec = $diff % 60; $diff /= 60;
  8         18  
571 8         12 my $min = $diff % 60;
572 8         15 my $hrs = ($diff / 60) % 24;
573 8         34 +{ hour => $hrs, minute => $min, second => $sec, nanosecond => $ns};
574             }
575              
576             sub infix($$@)
577 13     13   21 { my $self = shift;
578 13         21 my ($op, $right, $context) = @_;
579              
580 13 100 100     50 if($op eq '+' || $op eq '-')
581             { # normalization is a pain, so bluntly convert to seconds
582 8         25 my $time = $self->value;
583 8         23 my $was = $time->{hour} * 3600 + $time->{minute} * 60 + $time->{second};
584              
585 8 100       46 if(my $r = $right->isa('MF::TIME') ? $right : $right->cast('MF::TIME', $context))
    100          
586 5         28 { my $v = $r->value;
587 5         16 my $min = $v->{hour} * 3600 + $v->{minute} * 60 + $v->{second};
588 5         20 my $diff = $self->_sec_diff($was - $min, $time->{ns} - $v->{ns});
589 5 100       25 my $frac = $diff->{nanosecond} ? sprintf(".%09d", $diff->{nanosecond}) =~ s/0+$//r : '';
590             return MF::DURATION->new(sprintf "PT%dH%dM%d%sS", $diff->{hour}, $diff->{minute},
591 5         44 $diff->{second}, $frac);
592             }
593              
594 3 50       12 if(my $r = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION', $context))
    50          
595 3         10 { my (undef, $hours, $mins, $secs, $ns) =
596             $r->value->in_units(qw/days hours minutes seconds nanoseconds/);
597              
598 3         127 my $dur = $hours * 3600 + $mins * 60 + $secs;
599 3 100       8 my $diff = $op eq '+' ? $was + $dur : $was - $dur;
600 3 100       6 my $nns = $op eq '+' ? $time->{ns} + $ns : $time->{ns} - $ns;
601 3         9 return MF::TIME->new(undef, $self->_sec_diff($diff, $ns));
602             }
603             }
604              
605 5         20 $self->SUPER::infix(@_);
606             }
607              
608             #-----------------
609              
610             package
611             MF::TIMEZONE;
612 28     28   215 use base 'Math::Formula::Type';
  28         88  
  28         2650  
613 28     28   179 use POSIX 'floor';
  28         50  
  28         233  
614              
615 28     28   62 sub _match { '[+-] (?: 0[0-9]|1[012] ) [0-5][0-9]' }
616              
617             sub _token($)
618 4     4   8 { my $count = $_[1];
619 4         5 my $sign = '+';
620 4 100       11 ($sign, $count) = ('-', -$count) if $count < 0;
621 4         13 my $hours = floor($count / 60 + 0.0001);
622 4         6 my $mins = $count % 60;
623 4         32 sprintf "%s%02d%02d", $sign, $hours, $mins;
624             }
625              
626             # The value is stored in minutes
627              
628             sub _value($)
629 12     12   25 { my ($self, $token) = @_;
630 12 50       53 $token =~ m/^ ([+-]) (0[0-9]|1[012]) ([0-5][0-9]) $/x
631             or return;
632              
633 12 100       97 ($1 eq '-' ? -1 : 1) * ( $2 * 60 + $3 );
634             }
635              
636             sub cast($)
637 6     6   13 { my ($self, $to) = @_;
638 6 100 66     42 if($to->isa('MF::INTEGER') || $to->isa('MF::FLOAT'))
639             { # Oops, we mis-parsed and integer when 1[0-2][0-5][0-9]
640 5         16 $self->[1] = $self->[0] + 0;
641 5         7 $self->[0] = undef;
642 5         12 return bless $self, $to;
643             }
644 1         8 $self->SUPER::cast($to);
645             }
646              
647             our %tz_attrs = (
648             in_seconds => sub { MF::INTEGER->new(undef, $_[0]->value * 60) },
649             in_minutes => sub { MF::INTEGER->new(undef, $_[0]->value) },
650             );
651              
652 2 50   2   8 sub attribute($) { $tz_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
653              
654             sub prefix($$)
655 2     2   4 { my ($self, $op, $context) = @_;
656 2 50       10 $op eq '+' ? $self
    100          
657             : $op eq '-' ? MF::TIMEZONE->new(undef, - $self->value)
658             : $self->SUPER::prefix($op, $context);
659             }
660              
661             sub infix($$@)
662 5     5   6 { my $self = shift;
663 5         7 my ($op, $right, $context) = @_;
664              
665 5 100 100     20 if($op eq '+' || $op eq '-')
666 3 50       15 { if(my $d = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION'))
    50          
667 3 100       7 { return MF::TIMEZONE->new(undef, $self->value +
668             ($op eq '-' ? -1 : 1) * floor($d->inSeconds / 60 + 0.000001));
669             }
670             }
671              
672 2         8 $self->SUPER::infix(@_);
673             }
674              
675             #-----------------
676              
677             package
678             MF::DURATION;
679 28     28   16658 use base 'Math::Formula::Type';
  28         64  
  28         2358  
680              
681 28     28   190 use DateTime::Duration ();
  28         52  
  28         564  
682 28     28   167 use POSIX qw/floor/;
  28         62  
  28         123  
683              
684 56     56   158 sub _match { '[+-]? P (?:[0-9]+Y)? (?:[0-9]+M)? (?:[0-9]+D)? '
685             . ' (?:T (?:[0-9]+H)? (?:[0-9]+M)? (?:[0-9]+(?:\.[0-9]+)?S)? )? \b';
686             }
687              
688 28     28   15769 use DateTime::Format::Duration::ISO8601 ();
  28         41134  
  28         13837  
689             my $dur_format = DateTime::Format::Duration::ISO8601->new;
690             # Implementation dus not like negatives, but DateTime::Duration does.
691              
692 13 100   13   922 sub _token($) { ($_[1]->is_negative ? '-' : '') . $dur_format->format_duration($_[1]) }
693              
694             sub _value($)
695 40     40   74 { my $value = $_[1];
696 40         91 my $negative = $value =~ s/^-//;
697 40         105 my $duration = $dur_format->parse_duration($value);
698 40 100       6034 $negative ? $duration->multiply(-1) : $duration;
699             }
700              
701             sub prefix($$)
702 2     2   9 { my ($self, $op, $context) = @_;
703 2 50       13 $op eq '+' ? $self
    100          
704             : $op eq '-' ? MF::DURATION->new('-' . $self->token)
705             : $self->SUPER::prefix($op, $context);
706             }
707              
708             sub infix($$@)
709 16     16   23 { my $self = shift;
710 16         23 my ($op, $right, $context) = @_;
711              
712 16 100 100     69 if($op eq '+' || $op eq '-')
    100          
    100          
713 4 50       19 { my $r = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION', $context);
714 4         6 my $v = $self->value->clone;
715 4 100       39 my $dt = ! $r ? undef : $op eq '+' ? $v->add_duration($r->value) : $v->subtract_duration($r->value);
    50          
716 4 50       268 return MF::DURATION->new(undef, $dt) if $r;
717             }
718             elsif($op eq '*')
719 4 50       15 { my $r = $right->isa('MF::INTEGER') ? $right : $right->cast('MF::INTEGER', $context);
720 4 50       13 return MF::DURATION->new(undef, $self->value->clone->multiply($r->value)) if $r;
721             }
722             elsif($op eq '<=>')
723 6 50       18 { my $r = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION', $context);
724 6 50       17 return MF::INTEGER->new(undef, DateTime::Duration->compare($self->value, $r->value)) if $r;
725             }
726              
727 2         9 $self->SUPER::infix(@_);
728             }
729              
730              
731             sub inSeconds()
732 5     5   15 { my $d = $_[0]->value;
733 5         16 ($d->years + $d->months/12) * 365.256 + $d->days * 86400 + $d->hours * 3600 + $d->minutes * 60 + $d->seconds;
734             }
735              
736             my %dur_attrs = (
737             in_days => sub { MF::INTEGER->new(undef, floor($_[0]->inSeconds / 86400 +0.00001)) },
738             in_seconds => sub { MF::INTEGER->new(undef, $_[0]->inSeconds) },
739             );
740              
741 2 50   2   9 sub attribute($) { $dur_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
742              
743             #-----------------
744              
745             package
746             MF::NAME;
747 28     28   206 use base 'Math::Formula::Type';
  28         57  
  28         3046  
748              
749 28     28   181 use Log::Report 'math-formula', import => [ qw/error __x/ ];
  28         78  
  28         210  
750              
751             my $pattern = '[_\p{Alpha}][_\p{AlNum}]*';
752 28     28   73 sub _match() { $pattern }
753              
754 0     0   0 sub value($) { error __x"name '{name}' cannot be used as value.", name => $_[0]->token }
755              
756              
757             sub validated($$)
758 0     0   0 { my ($class, $name, $where) = @_;
759              
760 0 0       0 $name =~ qr/^$pattern$/o
761             or error __x"Illegal name '{name}' in '{where}'",
762             name => $name =~ s/[^_\p{AlNum}]/ϴ/gr, where => $where;
763              
764 0         0 $class->new($name);
765             }
766              
767             sub cast(@)
768 2     2   6 { my ($self, $type, $context) = @_;
769              
770 2 50       13 if($type->isa('MF::FRAGMENT'))
771 0 0       0 { my $frag = $self->token eq '' ? $context : $context->fragment($self->token);
772 0 0       0 return MF::FRAGMENT->new($frag->name, $frag) if $frag;
773             }
774              
775 2         8 $context->evaluate($self->token, expect => $type);
776             }
777              
778             sub prefix($$)
779 4     4   6 { my ($self, $op, $context) = @_;
780              
781 4 50       11 return MF::BOOLEAN->new(undef, defined $context->formula($self->token))
782             if $op eq 'exists';
783              
784 0         0 $self->SUPER::prefix($op, $context);
785             }
786              
787             sub infix(@)
788 21     21   30 { my $self = shift;
789 21         31 my ($op, $right, $context) = @_;
790 21         43 my $name = $self->token;
791              
792 21 100       44 if($op eq '.')
793 4 100       12 { my $left = $name eq '' ? MF::FRAGMENT->new($context->name, $context) : $context->evaluate($name);
794 4 50       13 return $left->infix(@_) if $left;
795             }
796              
797 17 100       34 if($op eq '#')
798 7 50       24 { my $left = $name eq '' ? MF::FRAGMENT->new($context->name, $context) : $context->fragment($name);
799 7 50       19 return $left->infix(@_) if $left;
800             }
801              
802 10 100       19 if($op eq '//')
803 5 100       25 { return defined $context->formula($name) ? $context->evaluate($name) : $right->compute($context);
804             }
805              
806 5         17 my $left = $context->evaluate($name);
807 5 50       34 $left ? $left->infix($op, $right, $context): undef;
808             }
809              
810              
811             #-----------------
812              
813             package
814             MF::PATTERN;
815 28     28   34406 use base 'MF::STRING';
  28         55  
  28         7435  
816              
817 28     28   180 use Log::Report 'math-formula', import => [ qw/warning __x/ ];
  28         50  
  28         161  
818              
819             sub _token($) {
820 0     0   0 warning __x"cannot convert qr back to pattern, do {regexp}", regexp => $_[1];
821 0         0 "pattern meaning $_[1]";
822             }
823              
824             sub _from_string($)
825 11     11   18 { my ($class, $string) = @_;
826 11         30 $string->token; # be sure the pattern is kept as token: cannot be recovered
827 11         22 bless $string, $class;
828             }
829              
830             sub _to_regexp($)
831 24     24   13122 { my @chars = $_[0] =~ m/( \\. | . )/gxu;
832 24         43 my (@regexp, $in_alts, $in_range);
833              
834 24         43 foreach my $char (@chars)
835 106 100       169 { if(length $char==2) { push @regexp, $char; next }
  2         5  
  2         4  
836 104 100       257 if($char !~ /^[\[\]*?{},!]$/) { push @regexp, $in_range ? $char : quotemeta $char }
  63 100       117  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
837 12         24 elsif($char eq '*') { push @regexp, '.*' }
838 1         4 elsif($char eq '?') { push @regexp, '.' }
839 3         7 elsif($char eq '[') { push @regexp, '['; $in_range++ }
  3         3  
840 3         5 elsif($char eq ']') { push @regexp, ']'; $in_range=0 }
  3         6  
841 2 100 66     15 elsif($char eq '!') { push @regexp, $in_range && $regexp[-1] eq '[' ? '^' : '\!' }
842 5 100       11 elsif($char eq '{') { push @regexp, $in_range ? '{' : '(?:'; $in_range or $in_alts++ }
  5 100       13  
843 5 100       11 elsif($char eq '}') { push @regexp, $in_range ? '}' : ')'; $in_range or $in_alts=0 }
  5 100       11  
844 10 100       19 elsif($char eq ',') { push @regexp, $in_alts ? '|' : '\,' }
845 0         0 else {die}
846             }
847 24         51 my $regexp = join '', @regexp;
848 24         363 qr/^${regexp}$/u;
849             }
850              
851              
852 12   66 12   50 sub regexp() { $_[0][2] //= _to_regexp($_[0]->value) }
853              
854             #-----------------
855              
856             package
857             MF::REGEXP;
858 28     28   15142 use base 'MF::STRING';
  28         52  
  28         8913  
859              
860             sub _from_string($)
861 14     14   23 { my ($class, $string) = @_;
862 14         33 bless $string, $class;
863             }
864              
865              
866             sub regexp
867 17     17   27 { my $self = shift;
868 17 100       44 return $self->[2] if defined $self->[2];
869 15         27 my $value = $self->value =~ s!/!\\/!gr;
870 15         224 $self->[2] = qr/$value/xu;
871             }
872              
873             #-----------------
874              
875             package
876             MF::FRAGMENT;
877 28     28   188 use base 'Math::Formula::Type';
  28         78  
  28         2698  
878              
879 28     28   174 use Log::Report 'math-formula', import => [ qw/panic error __x/ ];
  28         70  
  28         123  
880              
881 0     0   0 sub name { $_[0][0] }
882 15     15   35 sub context { $_[0][1] }
883              
884             sub infix($$@)
885 15     15   19 { my $self = shift;
886 15         21 my ($op, $right, $context) = @_;
887 15         25 my $name = $right->token;
888              
889 15 100 66     40 if($op eq '#' && $right->isa('MF::NAME'))
890 7 50       15 { my $fragment = $self->context->fragment($name)
891             or error __x"cannot find fragment '{name}' in '{context}'",
892             name => $name, context => $context->name;
893              
894 7         22 return $fragment;
895             }
896              
897 8 50 33     30 if($op eq '.' && $right->isa('MF::NAME'))
898 8         13 { my $result = $self->context->evaluate($name);
899 8 50       24 return $result if $result;
900             }
901              
902 0           $self->SUPER::infix(@_);
903             }
904              
905             1;