File Coverage

blib/lib/Math/Formula/Type.pm
Criterion Covered Total %
statement 411 435 94.4
branch 285 362 78.7
condition 84 105 80.0
subroutine 105 112 93.7
pod 5 7 71.4
total 890 1021 87.1


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