File Coverage

blib/lib/App/datecalc.pm
Criterion Covered Total %
statement 129 136 94.8
branch 101 118 85.5
condition 10 12 83.3
subroutine 25 25 100.0
pod 2 2 100.0
total 267 293 91.1


line stmt bran cond sub pod time code
1             package App::datecalc;
2              
3             our $DATE = '2016-08-15'; # DATE
4             our $VERSION = '0.07'; # VERSION
5              
6 1     1   441 use 5.010001;
  1         2  
7 1     1   3 use strict;
  1         1  
  1         13  
8 1     1   2 use warnings;
  1         1  
  1         15  
9              
10 1     1   726 use DateTime;
  1         85271  
  1         32  
11 1     1   452 use DateTime::Format::ISO8601;
  1         31712  
  1         46  
12 1     1   608 use MarpaX::Simple qw(gen_parser);
  1         99058  
  1         62  
13 1     1   7 use Scalar::Util qw(blessed);
  1         2  
  1         1392  
14              
15             # XXX there should already be an existing module that does this
16             sub __fmtduriso {
17 10     10   10 my $dur = shift;
18 10 50       26 my $res = join(
    50          
    100          
    100          
19             '',
20             "P",
21             ($dur->years ? $dur->years . "Y" : ""),
22             ($dur->months ? $dur->months . "M" : ""),
23             ($dur->weeks ? $dur->weeks . "W" : ""),
24             ($dur->days ? $dur->days . "D" : ""),
25             );
26 10 100 100     813 if ($dur->hours || $dur->minutes || $dur->seconds) {
      66        
27 3 100       63 $res .= join(
    100          
    100          
28             '',
29             'T',
30             ($dur->hours ? $dur->hours . "H" : ""),
31             ($dur->minutes ? $dur->minutes . "M" : ""),
32             ($dur->seconds ? $dur->seconds . "S" : ""),
33             );
34             }
35              
36 10 50       502 $res = "P0Y" if $res eq 'P';
37              
38 10         71 $res;
39             }
40              
41             sub new {
42             state $parser = gen_parser(
43             grammar => <<'_',
44             :default ::= action=>::first
45             lexeme default = latm=>1
46             :start ::= answer
47              
48             answer ::= date_expr
49             | dur_expr
50             # | str_expr
51             | num_expr
52              
53             num_expr ::= num_add
54             num_add ::= num_mult
55             | num_add op_addsub num_add action=>num_add
56             num_mult ::= num_unary
57             | num_mult op_multdiv num_mult action=>num_mult
58             num_unary ::= num_pow
59             || op_unary num_unary action=>num_unary assoc=>right
60             num_pow ::= num_term
61             || num_pow '**' num_pow action=>num_pow assoc=>right
62             num_term ::= num_literal
63             | func_inum_onum
64             | func_idate_onum
65             | func_idur_onum
66             | ('(') num_expr (')')
67              
68             date_expr ::= date_add_dur
69             date_add_dur ::= date_term
70             | date_add_dur op_addsub dur_term action=>date_add_dur
71             date_term ::= date_literal
72             # | date_variable
73             # | func_idate_odate
74             | ('(') date_expr (')')
75              
76             func_inum_onum_names ~ 'abs' | 'round'
77             func_inum_onum ::= func_inum_onum_names ('(') num_expr (')') action=>func_inum_onum
78              
79             func_idate_onum_names ~ 'year' | 'month' | 'day' | 'dow' | 'quarter'
80             | 'doy' | 'wom' | 'woy' | 'doq'
81             | 'hour' | 'minute' | 'second'
82             func_idate_onum ::= func_idate_onum_names ('(') date_expr (')') action=>func_idate_onum
83              
84             func_idur_onum_names ~ 'years' | 'months' | 'weeks' | 'days'
85             | 'hours' | 'minutes' | 'seconds'
86             | 'totdays'
87             func_idur_onum ::= func_idur_onum_names ('(') dur_expr (')') action=>func_idur_onum
88              
89             date_literal ::= iso_date_literal action=>datelit_iso
90             | 'now' action=>datelit_special
91             | 'today' action=>datelit_special
92             | 'yesterday' action=>datelit_special
93             | 'tomorrow' action=>datelit_special
94              
95             year4 ~ [\d][\d][\d][\d]
96             mon2 ~ [\d][\d]
97             day2 ~ [\d][\d]
98             iso_date_literal ~ year4 '-' mon2 '-' day2
99              
100             dur_expr ::= dur_add_dur
101             | date_sub_date
102             dur_add_dur ::= dur_mult_num
103             | dur_add_dur op_addsub dur_add_dur action=>dur_add_dur
104             date_sub_date ::= date_add_dur
105             | date_sub_date '-' date_sub_date action=>date_sub_date
106              
107             dur_mult_num ::= dur_term
108             | dur_mult_num op_multdiv num_expr action=>dur_mult_num
109             | num_expr op_mult dur_mult_num action=>dur_mult_num
110             dur_term ::= dur_literal
111             # | dur_variable
112             | '(' dur_expr ')'
113             dur_literal ::= nat_dur_literal
114             | iso_dur_literal
115              
116             unit_year ~ 'year' | 'years' | 'y'
117             unit_month ~ 'month' | 'months' | 'mon' | 'mons'
118             unit_week ~ 'week' | 'weeks' | 'w'
119             unit_day ~ 'day' | 'days' | 'd'
120             unit_hour ~ 'hour' | 'hours' | 'h'
121             unit_minute ~ 'minute' | 'minutes' | 'min' | 'mins'
122             unit_second ~ 'second' | 'seconds' | 'sec' | 'secs' | 's'
123              
124             ndl_year ~ num ws_opt unit_year
125             ndl_year_opt ~ num ws_opt unit_year
126             ndl_year_opt ~
127              
128             ndl_month ~ num ws_opt unit_month
129             ndl_month_opt ~ num ws_opt unit_month
130             ndl_month_opt ~
131              
132             ndl_week ~ num ws_opt unit_week
133             ndl_week_opt ~ num ws_opt unit_week
134             ndl_week_opt ~
135              
136             ndl_day ~ num ws_opt unit_day
137             ndl_day_opt ~ num ws_opt unit_day
138             ndl_day_opt ~
139              
140             ndl_hour ~ num ws_opt unit_hour
141             ndl_hour_opt ~ num ws_opt unit_hour
142             ndl_hour_opt ~
143              
144             ndl_minute ~ num ws_opt unit_minute
145             ndl_minute_opt ~ num ws_opt unit_minute
146             ndl_minute_opt ~
147              
148             ndl_second ~ num ws_opt unit_second
149             ndl_second_opt ~ num ws_opt unit_second
150             ndl_second_opt ~
151              
152             # need at least one element specified. XXX not happy with this
153             nat_dur_literal ::= nat_dur_literal0 action=>durlit_nat
154             nat_dur_literal0 ~ ndl_year ws_opt ndl_month_opt ws_opt ndl_week_opt ws_opt ndl_day_opt ws_opt ndl_hour_opt ws_opt ndl_minute_opt ws_opt ndl_second_opt
155             | ndl_year_opt ws_opt ndl_month ws_opt ndl_week_opt ws_opt ndl_day_opt ws_opt ndl_hour_opt ws_opt ndl_minute_opt ws_opt ndl_second_opt
156             | ndl_year_opt ws_opt ndl_month_opt ws_opt ndl_week ws_opt ndl_day_opt ws_opt ndl_hour_opt ws_opt ndl_minute_opt ws_opt ndl_second_opt
157             | ndl_year_opt ws_opt ndl_month_opt ws_opt ndl_week_opt ws_opt ndl_day ws_opt ndl_hour_opt ws_opt ndl_minute_opt ws_opt ndl_second_opt
158             | ndl_year_opt ws_opt ndl_month_opt ws_opt ndl_week_opt ws_opt ndl_day_opt ws_opt ndl_hour ws_opt ndl_minute_opt ws_opt ndl_second_opt
159             | ndl_year_opt ws_opt ndl_month_opt ws_opt ndl_week_opt ws_opt ndl_day_opt ws_opt ndl_hour_opt ws_opt ndl_minute ws_opt ndl_second_opt
160             | ndl_year_opt ws_opt ndl_month_opt ws_opt ndl_week_opt ws_opt ndl_day_opt ws_opt ndl_hour_opt ws_opt ndl_minute_opt ws_opt ndl_second
161              
162             idl_year ~ posnum 'Y'
163             idl_year_opt ~ posnum 'Y'
164             idl_year_opt ~
165              
166             idl_month ~ posnum 'M'
167             idl_month_opt ~ posnum 'M'
168             idl_month_opt ~
169              
170             idl_week ~ posnum 'W'
171             idl_week_opt ~ posnum 'W'
172             idl_week_opt ~
173              
174             idl_day ~ posnum 'D'
175             idl_day_opt ~ posnum 'D'
176             idl_day_opt ~
177              
178             idl_hour ~ posnum 'H'
179             idl_hour_opt ~ posnum 'H'
180             idl_hour_opt ~
181              
182             idl_minute ~ posnum 'M'
183             idl_minute_opt ~ posnum 'M'
184             idl_minute_opt ~
185              
186             idl_second ~ posnum 'S'
187             idl_second_opt ~ posnum 'S'
188             idl_second_opt ~
189              
190             # also need at least one element specified like in nat_dur_literal
191             iso_dur_literal ::= iso_dur_literal0 action=>durlit_iso
192             iso_dur_literal0 ~ 'P' idl_year idl_month_opt idl_week_opt idl_day_opt
193             | 'P' idl_year_opt idl_month idl_week_opt idl_day_opt
194             | 'P' idl_year_opt idl_month_opt idl_week idl_day_opt
195             | 'P' idl_year_opt idl_month_opt idl_week_opt idl_day
196             | 'P' idl_year_opt idl_month_opt idl_week_opt idl_day_opt 'T' idl_hour idl_minute_opt idl_second_opt
197             | 'P' idl_year_opt idl_month_opt idl_week_opt idl_day_opt 'T' idl_hour_opt idl_minute idl_second_opt
198             | 'P' idl_year_opt idl_month_opt idl_week_opt idl_day_opt 'T' idl_hour_opt idl_minute_opt idl_second
199              
200             sign ~ [+-]
201             digits ~ [\d]+
202             num_literal ~ num
203             num ~ digits
204             | sign digits
205             | digits '.' digits
206             | sign digits '.' digits
207             posnum ~ digits
208             | digits '.' digits
209              
210             op_unary ~ [+-]
211             op_addsub ~ [+-]
212              
213             op_mult ~ [*]
214             op_multdiv ~ [*/]
215              
216             :discard ~ ws
217             ws ~ [\s]+
218             ws_opt ~ [\s]*
219              
220             _
221             actions => {
222             datelit_iso => sub {
223 15     15   81509 my $h = shift;
224 15         45 my @date = split /-/, $_[0];
225 15         60 DateTime->new(year=>$date[0], month=>$date[1], day=>$date[2]);
226             },
227             date_sub_date => sub {
228 1     1   147 my $h = shift;
229 1         5 $_[0]->delta_days($_[2]);
230             },
231             datelit_special => sub {
232 4     4   23990 my $h = shift;
233 4 100       18 if ($_[0] eq 'now') {
    100          
    100          
    50          
234 1         7 DateTime->now;
235             } elsif ($_[0] eq 'today') {
236 1         6 DateTime->today;
237             } elsif ($_[0] eq 'yesterday') {
238 1         3 DateTime->today->subtract(days => 1);
239             } elsif ($_[0] eq 'tomorrow') {
240 1         4 DateTime->today->add(days => 1);
241             } else {
242 0         0 die "BUG: Unknown date literal '$_[0]'";
243             }
244             },
245             date_add_dur => sub {
246 2     2   141 my $h = shift;
247 2 100       6 if ($_[1] eq '+') {
248 1         5 $_[0] + $_[2];
249             } else {
250 1         4 $_[0] - $_[2];
251             }
252             },
253             dur_add_dur => sub {
254 1     1   68 my $h = shift;
255 1         5 $_[0] + $_[2];
256             },
257             dur_mult_num => sub {
258 4     4   320 my $h = shift;
259 4 100       9 if (ref $_[0]) {
260 2         2 my $d0 = $_[0];
261 2 100       5 if ($_[1] eq '*') {
262             # dur*num
263 1         4 DateTime::Duration->new(
264             years => $d0->years * $_[2],
265             months => $d0->months * $_[2],
266             weeks => $d0->weeks * $_[2],
267             days => $d0->days * $_[2],
268             hours => $d0->hours * $_[2],
269             minutes => $d0->minutes * $_[2],
270             seconds => $d0->seconds * $_[2],
271             );
272             } else {
273             # dur/num
274 1         3 DateTime::Duration->new(
275             years => $d0->years / $_[2],
276             months => $d0->months / $_[2],
277             weeks => $d0->weeks / $_[2],
278             days => $d0->days / $_[2],
279             hours => $d0->hours / $_[2],
280             minutes => $d0->minutes / $_[2],
281             seconds => $d0->seconds / $_[2],
282             );
283             }
284             } else {
285 2         4 my $d0 = $_[2];
286             # num * dur
287 2         8 DateTime::Duration->new(
288             years => $d0->years * $_[0],
289             months => $d0->months * $_[0],
290             weeks => $d0->weeks * $_[0],
291             days => $d0->days * $_[0],
292             hours => $d0->hours * $_[0],
293             minutes => $d0->minutes * $_[0],
294             seconds => $d0->seconds * $_[0],
295             );
296             }
297             },
298             durlit_nat => sub {
299 4     4   11866 my $h = shift;
300 4         8 local $_ = $_[0];
301 4         3 my %params;
302 4 50       30 $params{years} = $1 if /(-?\d+(?:\.\d+)?)\s*(years?|y)/;
303 4 50       16 $params{months} = $1 if /(-?\d+(?:\.\d+)?)\s*(mons?|months?)/;
304 4 100       18 $params{weeks} = $1 if /(-?\d+(?:\.\d+)?)\s*(weeks?|w)/;
305 4 100       20 $params{days} = $1 if /(-?\d+(?:\.\d+)?)\s*(days?|d)/;
306 4 100       17 $params{hours} = $1 if /(-?\d+(?:\.\d+)?)\s*(hours?|h)/;
307 4 100       16 $params{minutes} = $1 if /(-?\d+(?:\.\d+)?)\s*(mins?|minutes?)/;
308 4 100       16 $params{seconds} = $1 if /(-?\d+(?:\.\d+)?)\s*(s|secs?|seconds?)/;
309 4         17 DateTime::Duration->new(%params);
310             },
311             durlit_iso => sub {
312 16     16   82272 my $h = shift;
313             # split between date and time
314 16 50       107 my $d = $_[0] =~ /P(.+?)(?:T|\z)/ ? $1 : '';
315 16 100       37 my $t = $_[0] =~ /T(.*)/ ? $1 : '';
316             #say "D = $d, T = $t";
317 16         16 my %params;
318 16 100       71 $params{years} = $1 if $d =~ /(-?\d+(?:\.\d+)?)Y/i;
319 16 100       44 $params{months} = $1 if $d =~ /(-?\d+(?:\.\d+)?)M/i;
320 16 50       30 $params{weeks} = $1 if $d =~ /(-?\d+(?:\.\d+)?)W/;
321 16 100       60 $params{days} = $1 if $d =~ /(-?\d+(?:\.\d+)?)D/;
322 16 100       31 $params{hours} = $1 if $t =~ /(-?\d+(?:\.\d+)?)H/i;
323 16 100       48 $params{minutes} = $1 if $t =~ /(-?\d+(?:\.\d+)?)M/i;
324 16 100       49 $params{seconds} = $1 if $t =~ /(-?\d+(?:\.\d+)?)S/i;
325 16         70 DateTime::Duration->new(%params);
326             },
327             func_inum_onum => sub {
328 3     3   17586 my $h = shift;
329 3         3 my $fn = $_[0];
330 3         4 my $num = $_[1];
331 3 100       10 if ($fn eq 'abs') {
    50          
332 1         4 abs($num);
333             } elsif ($fn eq 'round') {
334 2         11 sprintf("%.0f", $num);
335             } else {
336 0         0 die "BUG: Unknown number function $fn";
337             }
338             },
339             func_idate_onum => sub {
340 9     9   1643 my $h = shift;
341 9         12 my $fn = $_[0];
342 9         10 my $d = $_[1];
343 9 100       48 if ($fn eq 'year') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
    0          
    0          
344 1         4 $d->year;
345             } elsif ($fn eq 'month') {
346 1         5 $d->month;
347             } elsif ($fn eq 'day') {
348 1         6 $d->day;
349             } elsif ($fn eq 'dow') {
350 1         4 $d->day_of_week;
351             } elsif ($fn eq 'quarter') {
352 1         5 $d->quarter;
353             } elsif ($fn eq 'doy') {
354 1         4 $d->day_of_year;
355             } elsif ($fn eq 'wom') {
356 1         5 $d->week_of_month;
357             } elsif ($fn eq 'woy') {
358 1         5 $d->week_number;
359             } elsif ($fn eq 'doq') {
360 1         4 $d->day_of_quarter;
361             } elsif ($fn eq 'hour') {
362 0         0 $d->hour;
363             } elsif ($fn eq 'minute') {
364 0         0 $d->minute;
365             } elsif ($fn eq 'second') {
366 0         0 $d->second;
367             } else {
368 0         0 die "BUG: Unknown date function $fn";
369             }
370             },
371             func_idur_onum => sub {
372 8     8   685 my $h = shift;
373 8         10 my $fn = $_[0];
374 8         8 my $dur = $_[1];
375 8 100       40 if ($fn eq 'years') {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
376 1         4 $dur->years;
377             } elsif ($fn eq 'months') {
378 1         4 $dur->months;
379             } elsif ($fn eq 'weeks') {
380 1         3 $dur->weeks;
381             } elsif ($fn eq 'days') {
382 1         3 $dur->days;
383             } elsif ($fn eq 'totdays') {
384 1         4 $dur->in_units("days");
385             } elsif ($fn eq 'hours') {
386 1         4 $dur->hours;
387             } elsif ($fn eq 'minutes') {
388 1         3 $dur->minutes;
389             } elsif ($fn eq 'seconds') {
390 1         4 $dur->seconds;
391             } else {
392 0         0 die "BUG: Unknown duration function $fn";
393             }
394             },
395             num_add => sub {
396 2     2   11349 my $h = shift;
397 2 100       6 if ($_[1] eq '+') {
398 1         4 $_[0] + $_[2];
399             } else {
400 1         4 $_[0] - $_[2];
401             }
402             },
403             num_mult => sub {
404 6     6   17494 my $h = shift;
405 6 100       11 if ($_[1] eq '*') {
406 5         13 $_[0] * $_[2];
407             } else {
408 1         4 $_[0] / $_[2];
409             }
410             },
411             num_unary => sub {
412 2     2   5674 my $h = shift;
413 2         3 my $op = $_[0];
414 2         1 my $num = $_[1];
415 2 100       6 if ($op eq '+') {
416 1         2 $num;
417             } else {
418             # -
419 1         2 -$num;
420             }
421             },
422             num_pow => sub {
423 2     2   5692 my $h = shift;
424 2         6 $_[0] ** $_[2];
425             },
426             },
427             trace_terminals => $ENV{DEBUG},
428             trace_values => $ENV{DEBUG},
429 1     1 1 81 );
430              
431 1         182519 bless {parser=>$parser}, shift;
432             }
433              
434             sub eval {
435 46     46 1 20048 my ($self, $str) = @_;
436 46         130 my $res = $self->{parser}->($str);
437              
438 43 100 100     7095 if (blessed($res) && $res->isa('DateTime::Duration')) {
    100 66        
439 10         28 __fmtduriso($res);
440             } elsif (blessed($res) && $res->isa('DateTime')) {
441 7         20 $res->ymd . "#".$res->day_abbr;
442             } else {
443 26         153 "$res";
444             }
445             }
446              
447             1;
448             # ABSTRACT: Date calculator
449              
450             __END__