File Coverage

blib/lib/Math/Formula/Type.pm
Criterion Covered Total %
statement 361 387 93.2
branch 252 324 77.7
condition 83 105 79.0
subroutine 94 102 92.1
pod 3 6 50.0
total 793 924 85.8


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