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   1680 use warnings;
  28         66  
  28         994  
6 28     28   155 use strict;
  28         60  
  28         547  
7 28     28   308 use v5.16; # fc
  28         101  
8              
9             package Math::Formula::Type;
10 28     28   142 use vars '$VERSION';
  28         71  
  28         1356  
11             $VERSION = '0.15';
12              
13 28     28   187 use base 'Math::Formula::Token';
  28         67  
  28         2792  
14              
15             #!!! The declarations of all other packages in this file are indented to avoid
16             #!!! indexing by CPAN.
17              
18 28     28   182 use Log::Report 'math-formula', import => [ qw/warning error __x/ ];
  28         49  
  28         206  
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 35 { my ($self, $to, $context) = @_;
29              
30 11 100       44 return MF::STRING->new(undef, $self->token)
31             if $to eq 'MF::STRING';
32              
33 6         21 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 1384 sub compute { $_[0] }
42              
43              
44 790   100 790 1 7111 sub value { my $self = shift; $self->[1] //= $self->_value($self->[0], @_) }
  790         3064  
45 0     0   0 sub _value { $_[1] }
46              
47              
48 1     1 1 21 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 70 { my $self = shift;
65 34         83 my ($op, $right, $context) = @_;
66              
67 34 100 66     205 if($op eq '.' && $right->isa('MF::NAME'))
68 33 50       113 { if(my $attr = $self->attribute($right->token))
69 33 50       197 { return ref $attr eq 'CODE' ? $attr->($self, @_) : $attr;
70             }
71             }
72              
73             # object used as string
74 1 50       6 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   18100 use base 'Math::Formula::Type';
  28         71  
  28         13100  
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   460 { my ($class, $token, $value) = (shift, shift, shift);
92 165 100       450 defined $token or $value = $value ? 1 : 0;
    100          
93 165         495 $class->SUPER::new($token, $value, @_);
94             }
95              
96             sub prefix($)
97 6     6   19 { my ($self, $op, $context) = @_;
98 6 50       17 if($op eq 'not')
99 6         16 { return MF::BOOLEAN->new(undef, ! $self->value);
100             }
101 0         0 $self->SUPER::prefix($op, $context);
102             }
103              
104             sub infix($$$)
105 26     26   44 { my $self = shift;
106 26         57 my ($op, $right, $context) = @_;
107              
108 26 100       112 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     97 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       96 return MF::BOOLEAN->new(undef, $v) if defined $v;
116             }
117             elsif($op eq '->')
118 3 50       6 { $self->value or return undef; # case false
119 3         10 my $result = $right->compute($context);
120 3         12 $context->setCaptures([]); # do not leak captures
121 3         12 return $result;
122             }
123              
124 0         0 $self->SUPER::infix(@_);
125             }
126              
127 98 100   98   622 sub _token($) { $_[1] ? 'true' : 'false' }
128 38     38   182 sub _value($) { $_[1] eq 'true' }
129              
130             #-----------------
131              
132             package
133             MF::STRING;
134              
135 28     28   212 use base 'Math::Formula::Type';
  28         80  
  28         2569  
136              
137 28     28   22196 use Unicode::Collate ();
  28         242788  
  28         21286  
138             my $collate = Unicode::Collate->new; #XXX which options do we need?
139              
140             sub new($$@)
141 176     176   1035 { my ($class, $token, $value) = (shift, shift, shift);
142 176 100       486 ($token, $value) = (undef, $$token) if ref $token eq 'SCALAR';
143 176         672 $class->SUPER::new($token, $value, @_);
144             }
145              
146 18     18   163 sub _token($) { '"' . ($_[1] =~ s/[\"]/\\$1/gr) . '"' }
147              
148             sub _value($)
149 106   50 106   247 { my $token = $_[1] // '';
150              
151 106 50       1036 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   90 { my ($self, $to) = @_;
158              
159 28 100 66     234 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   93 { my $self = shift;
166 60         118 my ($op, $right, $context) = @_;
167              
168 60 100 100     315 if($op eq '~')
    100          
    100          
    100          
    100          
169 6 100       29 { my $r = $right->isa('MF::STRING') ? $right : $right->cast('MF::STRING', $context);
170 6 50       25 return MF::STRING->new(undef, $self->value . $r->value) if $r;
171             }
172             elsif($op eq '=~')
173 9 50       48 { if(my $r = $right->isa('MF::REGEXP') ? $right : $right->cast('MF::REGEXP', $context))
    50          
174 9 100       22 { if(my @captures = $self->value =~ $r->regexp)
175 7         48 { $context->setCaptures(\@captures);
176 7         34 return MF::BOOLEAN->new(undef, 1);
177             }
178 2         22 return MF::BOOLEAN->new(undef, 0);
179             }
180             }
181             elsif($op eq '!~')
182 4 50       23 { my $r = $right->isa('MF::REGEXP') ? $right : $right->cast('MF::REGEXP', $context);
183 4 50       21 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     100 my $r = $right->isa('MF::PATTERN') || $right->isa('MF::REGEXP') ? $right : $right->cast('MF::PATTERN', $context);
188 12 100       74 my $v = ! $r ? undef
    50          
189             : $op eq 'like' ? $self->value =~ $r->regexp : $self->value !~ $r->regexp;
190 12 50       66 return MF::BOOLEAN->new(undef, $v) if $r;
191             }
192             elsif($op eq 'cmp')
193 21 50       56 { my $r = $right->isa('MF::STRING') ? $right : $right->cast('MF::STRING', $context);
194 21         50 return MF::INTEGER->new(undef, $collate->cmp($self->value, $right->value));
195             }
196              
197 8         29 $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   36 sub attribute($) { $string_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
207              
208             #-----------------
209              
210             package
211             MF::INTEGER;
212              
213 28     28   20545 use base 'Math::Formula::Type';
  28         72  
  28         3590  
214 28     28   201 use Log::Report 'math-formula', import => [ qw/error __x/ ];
  28         91  
  28         236  
215              
216             sub cast($)
217 16     16   45 { my ($self, $to) = @_;
218 16 100       79 $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   17 { my ($self, $op, $context) = @_;
225 9 50       32 $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   111 { my $self = shift;
232 58         142 my ($op, $right, $context) = @_;
233              
234 58 100 100     364 return $self->cast('MF::BOOLEAN', $context)->infix(@_)
      100        
235             if $op eq 'and' || $op eq 'or' || $op eq 'xor';
236              
237 52 100       218 $right->cast('MF::INTEGER')
238             if $right->isa('MF::TIMEZONE'); # mis-parse
239              
240 52 100 100     208 if($right->isa('MF::INTEGER') || $right->isa('MF::FLOAT'))
241 47 100       223 { 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       158 return ref($right)->new(undef, $v) if defined $v;
247              
248 25 100       72 return MF::INTEGER->new(undef, $self->value <=> $right->value)
249             if $op eq '<=>';
250              
251 1 50       7 return MF::FLOAT->new(undef, $self->value / $right->value)
252             if $op eq '/';
253             }
254              
255 5 100 66     34 return $right->infix($op, $self, @_[2..$#_])
256             if $op eq '*' && $right->isa('MF::DURATION');
257              
258 3         36 $self->SUPER::infix(@_);
259             }
260              
261             my $gibi = 1024 * 1024 * 1024;
262              
263             my $multipliers = '[kMGTEZ](?:ibi)?\b';
264 28     28   148 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   1434 { 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       1027 ($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   12 sub attribute($) { $int_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
283              
284             #-----------------
285              
286             package
287             MF::FLOAT;
288              
289 28     28   24651 use base 'Math::Formula::Type';
  28         87  
  28         2882  
290 28     28   204 use POSIX qw/floor/;
  28         79  
  28         248  
291              
292 29     29   96 sub _match { '[0-9]+ (?: \.[0-9]+ (?: e [+-][0-9]+ )? | e [+-][0-9]+ )' }
293 37     37   215 sub _value($) { $_[1] + 0.0 }
294 24 100   24   222 sub _token($) { my $t = sprintf '%g', $_[1]; $t =~ /[e.]/ ? $t : "$t.0" }
  24         248  
295              
296             sub cast($)
297 2     2   5 { my ($self, $to) = @_;
298 2 100       16 $to eq 'MF::INTEGER' ? MF::INTEGER->new(undef, floor($_[0]->value))
299             : $self->SUPER::cast($to);
300             }
301              
302             sub prefix($$)
303 9     9   17 { my ($self, $op, $context) = @_;
304 9 50       30 $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   29 { my $self = shift;
311 17         33 my ($op, $right, $context) = @_;
312              
313 17 50 33     119 return $self->cast('MF::BOOLEAN', $context)->infix(@_)
      33        
314             if $op eq 'and' || $op eq 'or' || $op eq 'xor';
315              
316 17 100       80 $right->cast('MF::INTEGER')
317             if $right->isa('MF::TIMEZONE'); # mis-parse
318              
319 17 50 66     77 if($right->isa('MF::FLOAT') || $right->isa('MF::INTEGER'))
320             { # Perl will upgrade the integers
321 17 100       137 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       63 return MF::FLOAT->new(undef, $v) if defined $v;
328              
329 6 50       19 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   16311 use base 'Math::Formula::Type';
  28         87  
  28         2806  
346 28     28   24943 use DateTime ();
  28         13692208  
  28         30179  
347            
348             sub _match {
349 56     56   172 '[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   43 sub _token($) { $_[1]->datetime . ($_[1]->time_zone->name =~ s/UTC$/+0000/r) }
355              
356             sub _value($)
357 37     37   78 { my ($self, $token) = @_;
358 37 50       280 $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     146 my $tz_offset = $8 // '+0000'; # careful with named matches :-(
365 37   100     302 my @args = ( year => $1, month => $2, day => $3, hour => $4, minute => $5, second => $6,
366             nanosecond => ($7 // 0) * 1_000_000_000 );
367 37         147 my $tz = DateTime::TimeZone::OffsetOnly->new(offset => $tz_offset);
368              
369 37         6223 DateTime->new(@args, time_zone => $tz);
370             }
371              
372             sub _to_time($)
373 2     2   608 { +{ hour => $_[0]->hour, minute => $_[0]->minute, second => $_[0]->second, ns => $_[0]->nanosecond };
374             }
375              
376             sub cast($)
377 2     2   12 { 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   35 { my $self = shift;
385 21         42 my ($op, $right, $context) = @_;
386              
387 21 100 100     87 if($op eq '+' || $op eq '-')
388 3         7 { my $dt = $self->value->clone;
389 3 100       947 if($right->isa('MF::DURATION'))
390 2 100       14 { my $v = $op eq '+' ? $dt->add_duration($right->value) : $dt->subtract_duration($right->value);
391 2         1962 return MF::DATETIME->new(undef, $v);
392             }
393 1 50       6 if($op eq '-')
394 1 50       5 { my $r = $right->isa('MF::DATETIME') ? $right : $right->cast('MF::DATETIME', $context);
395 1         4 return MF::DURATION->new(undef, $dt->subtract_datetime($right->value));
396             }
397             }
398              
399 18 100       41 if($op eq '<=>')
400 8 100       33 { return MF::INTEGER->new(undef, DateTime->compare($self->value, $right->value))
401             if $right->isa('MF::DATETIME');
402              
403 3 50       9 if($right->isa('MF::DATE'))
404             { # Many timezone problems solved by DateTime
405 3         10 my $date = $right->token;
406 3 50       19 my $begin = $self->_value($date =~ /\+/ ? $date =~ s/\+/T00:00:00+/r : $date.'T00:00:00');
407 3 100       862 return MF::INTEGER->new(undef, -1) if DateTime->compare($begin, $self->value) > 0;
408              
409 2 50       707 my $end = $self->_value($date =~ /\+/ ? $date =~ s/\+/T23:59:59+/r : $date.'T23:59:59');
410 2 100       561 return MF::INTEGER->new(undef, DateTime->compare($self->value, $end) > 0 ? 1 : 0);
411             }
412             }
413              
414 10         30 $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   47 { $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   330 use base 'Math::Formula::Type';
  28         81  
  28         5037  
436              
437 28     28   263 use Log::Report 'math-formula', import => [ qw/error warning __x/ ];
  28         96  
  28         420  
438              
439 28     28   6520 use DateTime::TimeZone ();
  28         119  
  28         608  
440 28     28   205 use DateTime::TimeZone::OffsetOnly ();
  28         99  
  28         29405  
441              
442 56     56   147 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   43 sub _token($) { $_[1]->ymd . ($_[1]->time_zone->name =~ s/UTC$/+0000/r) }
445              
446             sub _value($)
447 11     11   23 { my ($self, $token) = @_;
448 11 50       75 $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     43 my $tz_offset = $4 // '+0000'; # careful with named matches :-(
454 11         44 my @args = ( year => $1, month => $2, day => $3);
455 11         49 my $tz = DateTime::TimeZone::OffsetOnly->new(offset => $tz_offset);
456              
457 11         2526 DateTime->new(@args, time_zone => $tz);
458             }
459              
460             sub cast($)
461 3     3   9 { my ($self, $to) = @_;
462 3 100       10 if($to eq 'MF::INTEGER')
463             { # In really exceptional cases, an integer expression can be mis-detected as DATE
464 2         8 bless $self, 'MF::INTEGER';
465 2         134 $self->[0] = $self->[1] = eval "$self->[0]";
466 2         11 return $self;
467             }
468              
469 1 50       4 if($to eq 'MF::DATETIME')
470 1         3 { my $t = $self->token;
471 1 50       9 my $dt = $t =~ /\+/ ? $t =~ s/\+/T00:00:00+/r : $t . 'T00:00:00';
472 1         12 return MF::DATETIME->new($dt);
473             }
474              
475 0         0 $self->SUPER::cast($to);
476             }
477              
478             sub infix($$@)
479 12     12   18 { my $self = shift;
480 12         24 my ($op, $right, $context) = @_;
481              
482 12 100 100     42 if($op eq '+' && $right->isa('MF::TIME'))
483 1         5 { my $l = $self->value;
484 1         303 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         6 nanosecond => $r->{ns}, time_zone => $l->time_zone);
488              
489 1         305 return MF::DATETIME->new(undef, $v);
490             }
491              
492 11 100 100     32 if($op eq '-' && $right->isa('MF::DATE'))
493 1         10 { return MF::DURATION->new(undef, $self->value->clone->subtract_datetime($right->value));
494             }
495              
496 10 100 100     37 if($op eq '+' || $op eq '-')
497 2 50       7 { my $r = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION', $context);
498 2 50 33     11 ! $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         10 my $dt = $self->value->clone;
503 2 100       704 my $v = $op eq '+' ? $dt->add_duration($right->value) : $dt->subtract_duration($right->value);
504 2         2275 return MF::DATE->new(undef, $v);
505             }
506              
507 8 100       28 if($op eq '<=>')
508 4 50       16 { my $r = $right->isa('MF::DATE') ? $right : $right->cast('MF::DATE', $context);
509 4         10 my ($ld, $ltz) = $self->token =~ m/(.{10})(.*)/;
510 4         12 my ($rd, $rtz) = $r->token =~ m/(.{10})(.*)/;
511              
512             # It is probably a configuration issue when you configure this.
513 4 100 50     22 $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         2009 return MF::INTEGER->new(undef, $ld cmp $rd);
518             }
519              
520 4         16 $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   18 sub attribute($) { $date_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
530              
531             #-----------------
532              
533             package
534             MF::TIME;
535 28     28   282 use base 'Math::Formula::Type';
  28         98  
  28         3149  
536              
537 28     28   253 use constant GIGA => 1_000_000_000;
  28         77  
  28         28311  
538              
539 56     56   151 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   13 { my $time = $_[1];
543 7         16 my $ns = $time->{ns};
544 7 100       35 my $frac = $ns ? sprintf(".%09d", $ns) =~ s/0+$//r : '';
545 7         73 sprintf "%02d:%02d:%02d%s", $time->{hour}, $time->{minute}, $time->{second}, $frac;
546             }
547              
548             sub _value($)
549 23     23   52 { my ($self, $token) = @_;
550 23 50       126 $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     273 +{ 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   56 sub attribute($) { $time_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
564              
565             sub _sec_diff($$)
566 8     8   21 { my ($self, $diff, $ns) = @_;
567 8 100       55 if($ns < 0) { $ns += GIGA; $diff -= 1 }
  1 50       3  
  1         2  
568 0         0 elsif($ns > GIGA) { $ns -= GIGA; $diff += 1 }
  0         0  
569              
570 8         21 my $sec = $diff % 60; $diff /= 60;
  8         18  
571 8         16 my $min = $diff % 60;
572 8         15 my $hrs = ($diff / 60) % 24;
573 8         38 +{ hour => $hrs, minute => $min, second => $sec, nanosecond => $ns};
574             }
575              
576             sub infix($$@)
577 13     13   24 { my $self = shift;
578 13         30 my ($op, $right, $context) = @_;
579              
580 13 100 100     60 if($op eq '+' || $op eq '-')
581             { # normalization is a pain, so bluntly convert to seconds
582 8         24 my $time = $self->value;
583 8         30 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         13 { my $v = $r->value;
587 5         20 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       38 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         52 $diff->{second}, $frac);
592             }
593              
594 3 50       16 if(my $r = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION', $context))
    50          
595 3         22 { my (undef, $hours, $mins, $secs, $ns) =
596             $r->value->in_units(qw/days hours minutes seconds nanoseconds/);
597              
598 3         171 my $dur = $hours * 3600 + $mins * 60 + $secs;
599 3 100       8 my $diff = $op eq '+' ? $was + $dur : $was - $dur;
600 3 100       21 my $nns = $op eq '+' ? $time->{ns} + $ns : $time->{ns} - $ns;
601 3         10 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   257 use base 'Math::Formula::Type';
  28         90  
  28         3412  
613 28     28   230 use POSIX 'floor';
  28         70  
  28         666  
614              
615 28     28   80 sub _match { '[+-] (?: 0[0-9]|1[012] ) [0-5][0-9]' }
616              
617             sub _token($)
618 4     4   9 { my $count = $_[1];
619 4         7 my $sign = '+';
620 4 100       23 ($sign, $count) = ('-', -$count) if $count < 0;
621 4         17 my $hours = floor($count / 60 + 0.0001);
622 4         8 my $mins = $count % 60;
623 4         40 sprintf "%s%02d%02d", $sign, $hours, $mins;
624             }
625              
626             # The value is stored in minutes
627              
628             sub _value($)
629 12     12   27 { my ($self, $token) = @_;
630 12 50       60 $token =~ m/^ ([+-]) (0[0-9]|1[012]) ([0-5][0-9]) $/x
631             or return;
632              
633 12 100       109 ($1 eq '-' ? -1 : 1) * ( $2 * 60 + $3 );
634             }
635              
636             sub cast($)
637 6     6   16 { my ($self, $to) = @_;
638 6 100 66     46 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         17 $self->[1] = $self->[0] + 0;
641 5         11 $self->[0] = undef;
642 5         14 return bless $self, $to;
643             }
644 1         9 $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   10 sub attribute($) { $tz_attrs{$_[1]} || $_[0]->SUPER::attribute($_[1]) }
653              
654             sub prefix($$)
655 2     2   5 { my ($self, $op, $context) = @_;
656 2 50       19 $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   9 { my $self = shift;
663 5         8 my ($op, $right, $context) = @_;
664              
665 5 100 100     21 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         28 $self->SUPER::infix(@_);
673             }
674              
675             #-----------------
676              
677             package
678             MF::DURATION;
679 28     28   20811 use base 'Math::Formula::Type';
  28         92  
  28         2871  
680              
681 28     28   284 use DateTime::Duration ();
  28         115  
  28         841  
682 28     28   176 use POSIX qw/floor/;
  28         96  
  28         163  
683              
684 56     56   155 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   19899 use DateTime::Format::Duration::ISO8601 ();
  28         52475  
  28         17225  
689             my $dur_format = DateTime::Format::Duration::ISO8601->new;
690             # Implementation dus not like negatives, but DateTime::Duration does.
691              
692 13 100   13   861 sub _token($) { ($_[1]->is_negative ? '-' : '') . $dur_format->format_duration($_[1]) }
693              
694             sub _value($)
695 40     40   71 { my $value = $_[1];
696 40         109 my $negative = $value =~ s/^-//;
697 40         123 my $duration = $dur_format->parse_duration($value);
698 40 100       7228 $negative ? $duration->multiply(-1) : $duration;
699             }
700              
701             sub prefix($$)
702 2     2   10 { my ($self, $op, $context) = @_;
703 2 50       14 $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   21 { my $self = shift;
710 16         34 my ($op, $right, $context) = @_;
711              
712 16 100 100     72 if($op eq '+' || $op eq '-')
    100          
    100          
713 4 50       29 { my $r = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION', $context);
714 4         9 my $v = $self->value->clone;
715 4 100       47 my $dt = ! $r ? undef : $op eq '+' ? $v->add_duration($r->value) : $v->subtract_duration($r->value);
    50          
716 4 50       336 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       15 return MF::DURATION->new(undef, $self->value->clone->multiply($r->value)) if $r;
721             }
722             elsif($op eq '<=>')
723 6 50       20 { my $r = $right->isa('MF::DURATION') ? $right : $right->cast('MF::DURATION', $context);
724 6 50       21 return MF::INTEGER->new(undef, DateTime::Duration->compare($self->value, $r->value)) if $r;
725             }
726              
727 2         13 $self->SUPER::infix(@_);
728             }
729              
730              
731             sub inSeconds()
732 5     5   17 { my $d = $_[0]->value;
733 5         19 ($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   240 use base 'Math::Formula::Type';
  28         106  
  28         3668  
748              
749 28     28   221 use Log::Report 'math-formula', import => [ qw/error __x/ ];
  28         98  
  28         248  
750              
751             my $pattern = '[_\p{Alpha}][_\p{AlNum}]*';
752 28     28   85 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   8 { my ($self, $type, $context) = @_;
769              
770 2 50       16 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         9 $context->evaluate($self->token, expect => $type);
776             }
777              
778             sub prefix($$)
779 4     4   12 { my ($self, $op, $context) = @_;
780              
781 4 50       20 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   37 { my $self = shift;
789 21         40 my ($op, $right, $context) = @_;
790 21         68 my $name = $self->token;
791              
792 21 100       60 if($op eq '.')
793 4 100       15 { my $left = $name eq '' ? MF::FRAGMENT->new($context->name, $context) : $context->evaluate($name);
794 4 50       15 return $left->infix(@_) if $left;
795             }
796              
797 17 100       50 if($op eq '#')
798 7 50       24 { my $left = $name eq '' ? MF::FRAGMENT->new($context->name, $context) : $context->fragment($name);
799 7 50       21 return $left->infix(@_) if $left;
800             }
801              
802 10 100       26 if($op eq '//')
803 5 100       13 { return defined $context->formula($name) ? $context->evaluate($name) : $right->compute($context);
804             }
805              
806 5         19 my $left = $context->evaluate($name);
807 5 50       32 $left ? $left->infix($op, $right, $context): undef;
808             }
809              
810              
811             #-----------------
812              
813             package
814             MF::PATTERN;
815 28     28   43894 use base 'MF::STRING';
  28         77  
  28         9673  
816              
817 28     28   242 use Log::Report 'math-formula', import => [ qw/warning __x/ ];
  28         88  
  28         219  
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   24 { my ($class, $string) = @_;
826 11         36 $string->token; # be sure the pattern is kept as token: cannot be recovered
827 11         28 bless $string, $class;
828             }
829              
830             sub _to_regexp($)
831 24     24   18838 { my @chars = $_[0] =~ m/( \\. | . )/gxu;
832 24         50 my (@regexp, $in_alts, $in_range);
833              
834 24         52 foreach my $char (@chars)
835 106 100       202 { if(length $char==2) { push @regexp, $char; next }
  2         9  
  2         51  
836 104 100       392 if($char !~ /^[\[\]*?{},!]$/) { push @regexp, $in_range ? $char : quotemeta $char }
  63 100       140  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
837 12         29 elsif($char eq '*') { push @regexp, '.*' }
838 1         3 elsif($char eq '?') { push @regexp, '.' }
839 3         7 elsif($char eq '[') { push @regexp, '['; $in_range++ }
  3         6  
840 3         7 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       16 elsif($char eq '{') { push @regexp, $in_range ? '{' : '(?:'; $in_range or $in_alts++ }
  5 100       13  
843 5 100       12 elsif($char eq '}') { push @regexp, $in_range ? '}' : ')'; $in_range or $in_alts=0 }
  5 100       15  
844 10 100       25 elsif($char eq ',') { push @regexp, $in_alts ? '|' : '\,' }
845 0         0 else {die}
846             }
847 24         64 my $regexp = join '', @regexp;
848 24         461 qr/^${regexp}$/u;
849             }
850              
851              
852 12   66 12   64 sub regexp() { $_[0][2] //= _to_regexp($_[0]->value) }
853              
854             #-----------------
855              
856             package
857             MF::REGEXP;
858 28     28   18891 use base 'MF::STRING';
  28         109  
  28         10970  
859              
860             sub _from_string($)
861 14     14   33 { my ($class, $string) = @_;
862 14         47 bless $string, $class;
863             }
864              
865              
866             sub regexp
867 17     17   37 { my $self = shift;
868 17 100       53 return $self->[2] if defined $self->[2];
869 15         40 my $value = $self->value =~ s!/!\\/!gr;
870 15         291 $self->[2] = qr/$value/xu;
871             }
872              
873             #-----------------
874              
875             package
876             MF::FRAGMENT;
877 28     28   256 use base 'Math::Formula::Type';
  28         91  
  28         3467  
878              
879 28     28   219 use Log::Report 'math-formula', import => [ qw/panic error __x/ ];
  28         88  
  28         162  
880              
881 0     0   0 sub name { $_[0][0] }
882 15     15   39 sub context { $_[0][1] }
883              
884             sub infix($$@)
885 15     15   62 { my $self = shift;
886 15         26 my ($op, $right, $context) = @_;
887 15         30 my $name = $right->token;
888              
889 15 100 66     56 if($op eq '#' && $right->isa('MF::NAME'))
890 7 50       16 { 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         27 return $fragment;
895             }
896              
897 8 50 33     35 if($op eq '.' && $right->isa('MF::NAME'))
898 8         17 { my $result = $self->context->evaluate($name);
899 8 50       33 return $result if $result;
900             }
901              
902 0           $self->SUPER::infix(@_);
903             }
904              
905             1;