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 1     1   565 use 5.010001;
  1         8  
4 1     1   6 use strict;
  1         2  
  1         35  
5 1     1   5 use warnings;
  1         2  
  1         33  
6              
7 1     1   857 use DateTime;
  1         549804  
  1         51  
8 1     1   725 use DateTime::Format::ISO8601;
  1         440610  
  1         45  
9 1     1   717 use MarpaX::Simple qw(gen_parser);
  1         134450  
  1         79  
10 1     1   13 use Scalar::Util qw(blessed);
  1         2  
  1         2378  
11              
12             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
13             our $DATE = '2023-02-18'; # DATE
14             our $DIST = 'App-datecalc'; # DIST
15             our $VERSION = '0.090'; # VERSION
16              
17             # XXX there should already be an existing module that does this
18             sub __fmtduriso {
19 10     10   19 my $dur = shift;
20 10 50       33 my $res = join(
    50          
    100          
    100          
21             '',
22             "P",
23             ($dur->years ? $dur->years . "Y" : ""),
24             ($dur->months ? $dur->months . "M" : ""),
25             ($dur->weeks ? $dur->weeks . "W" : ""),
26             ($dur->days ? $dur->days . "D" : ""),
27             );
28 10 100 100     1738 if ($dur->hours || $dur->minutes || $dur->seconds) {
      66        
29 3 100       125 $res .= join(
    100          
    100          
30             '',
31             'T',
32             ($dur->hours ? $dur->hours . "H" : ""),
33             ($dur->minutes ? $dur->minutes . "M" : ""),
34             ($dur->seconds ? $dur->seconds . "S" : ""),
35             );
36             }
37              
38 10 50       1078 $res = "P0Y" if $res eq 'P';
39              
40 10         102 $res;
41             }
42              
43             sub new {
44             state $parser = gen_parser(
45             grammar => <<'_',
46             :default ::= action=>::first
47             lexeme default = latm=>1
48             :start ::= answer
49              
50             answer ::= date_expr
51             | dur_expr
52             # | str_expr
53             | num_expr
54              
55             num_expr ::= num_add
56             num_add ::= num_mult
57             | num_add op_addsub num_add action=>num_add
58             num_mult ::= num_unary
59             | num_mult op_multdiv num_mult action=>num_mult
60             num_unary ::= num_pow
61             || op_unary num_unary action=>num_unary assoc=>right
62             num_pow ::= num_term
63             || num_pow '**' num_pow action=>num_pow assoc=>right
64             num_term ::= num_literal
65             | func_inum_onum
66             | func_idate_onum
67             | func_idur_onum
68             | ('(') num_expr (')')
69              
70             date_expr ::= date_add_dur
71             date_add_dur ::= date_term
72             | date_add_dur op_addsub dur_term action=>date_add_dur
73             date_term ::= date_literal
74             # | date_variable
75             # | func_idate_odate
76             | ('(') date_expr (')')
77              
78             func_inum_onum_names ~ 'abs' | 'round'
79             func_inum_onum ::= func_inum_onum_names ('(') num_expr (')') action=>func_inum_onum
80              
81             func_idate_onum_names ~ 'year' | 'month' | 'day' | 'dow' | 'quarter'
82             | 'doy' | 'wom' | 'woy' | 'doq'
83             | 'hour' | 'minute' | 'second'
84             func_idate_onum ::= func_idate_onum_names ('(') date_expr (')') action=>func_idate_onum
85              
86             func_idur_onum_names ~ 'years' | 'months' | 'weeks' | 'days'
87             | 'hours' | 'minutes' | 'seconds'
88             | 'totdays'
89             func_idur_onum ::= func_idur_onum_names ('(') dur_expr (')') action=>func_idur_onum
90              
91             date_literal ::= iso_date_literal action=>datelit_iso
92             | 'now' action=>datelit_special
93             | 'today' action=>datelit_special
94             | 'yesterday' action=>datelit_special
95             | 'tomorrow' action=>datelit_special
96              
97             year4 ~ [\d][\d][\d][\d]
98             mon2 ~ [\d][\d]
99             day2 ~ [\d][\d]
100             iso_date_literal ~ year4 '-' mon2 '-' day2
101              
102             dur_expr ::= dur_add_dur
103             | date_sub_date
104             dur_add_dur ::= dur_mult_num
105             | dur_add_dur op_addsub dur_add_dur action=>dur_add_dur
106             date_sub_date ::= date_add_dur
107             | date_sub_date '-' date_sub_date action=>date_sub_date
108              
109             dur_mult_num ::= dur_term
110             | dur_mult_num op_multdiv num_expr action=>dur_mult_num
111             | num_expr op_mult dur_mult_num action=>dur_mult_num
112             dur_term ::= dur_literal
113             # | dur_variable
114             | '(' dur_expr ')'
115             dur_literal ::= nat_dur_literal
116             | iso_dur_literal
117              
118             unit_year ~ 'year' | 'years' | 'y'
119             unit_month ~ 'month' | 'months' | 'mon' | 'mons'
120             unit_week ~ 'week' | 'weeks' | 'w'
121             unit_day ~ 'day' | 'days' | 'd'
122             unit_hour ~ 'hour' | 'hours' | 'h'
123             unit_minute ~ 'minute' | 'minutes' | 'min' | 'mins'
124             unit_second ~ 'second' | 'seconds' | 'sec' | 'secs' | 's'
125              
126             ndl_year ~ num ws_opt unit_year
127             ndl_year_opt ~ num ws_opt unit_year
128             ndl_year_opt ~
129              
130             ndl_month ~ num ws_opt unit_month
131             ndl_month_opt ~ num ws_opt unit_month
132             ndl_month_opt ~
133              
134             ndl_week ~ num ws_opt unit_week
135             ndl_week_opt ~ num ws_opt unit_week
136             ndl_week_opt ~
137              
138             ndl_day ~ num ws_opt unit_day
139             ndl_day_opt ~ num ws_opt unit_day
140             ndl_day_opt ~
141              
142             ndl_hour ~ num ws_opt unit_hour
143             ndl_hour_opt ~ num ws_opt unit_hour
144             ndl_hour_opt ~
145              
146             ndl_minute ~ num ws_opt unit_minute
147             ndl_minute_opt ~ num ws_opt unit_minute
148             ndl_minute_opt ~
149              
150             ndl_second ~ num ws_opt unit_second
151             ndl_second_opt ~ num ws_opt unit_second
152             ndl_second_opt ~
153              
154             # need at least one element specified. XXX not happy with this
155             nat_dur_literal ::= nat_dur_literal0 action=>durlit_nat
156             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
157             | 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
158             | 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
159             | 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
160             | 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
161             | 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
162             | 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
163              
164             idl_year ~ posnum 'Y'
165             idl_year_opt ~ posnum 'Y'
166             idl_year_opt ~
167              
168             idl_month ~ posnum 'M'
169             idl_month_opt ~ posnum 'M'
170             idl_month_opt ~
171              
172             idl_week ~ posnum 'W'
173             idl_week_opt ~ posnum 'W'
174             idl_week_opt ~
175              
176             idl_day ~ posnum 'D'
177             idl_day_opt ~ posnum 'D'
178             idl_day_opt ~
179              
180             idl_hour ~ posnum 'H'
181             idl_hour_opt ~ posnum 'H'
182             idl_hour_opt ~
183              
184             idl_minute ~ posnum 'M'
185             idl_minute_opt ~ posnum 'M'
186             idl_minute_opt ~
187              
188             idl_second ~ posnum 'S'
189             idl_second_opt ~ posnum 'S'
190             idl_second_opt ~
191              
192             # also need at least one element specified like in nat_dur_literal
193             iso_dur_literal ::= iso_dur_literal0 action=>durlit_iso
194             iso_dur_literal0 ~ 'P' idl_year idl_month_opt idl_week_opt idl_day_opt
195             | 'P' idl_year_opt idl_month idl_week_opt idl_day_opt
196             | 'P' idl_year_opt idl_month_opt idl_week idl_day_opt
197             | 'P' idl_year_opt idl_month_opt idl_week_opt idl_day
198             | 'P' idl_year_opt idl_month_opt idl_week_opt idl_day_opt 'T' idl_hour idl_minute_opt idl_second_opt
199             | 'P' idl_year_opt idl_month_opt idl_week_opt idl_day_opt 'T' idl_hour_opt idl_minute idl_second_opt
200             | 'P' idl_year_opt idl_month_opt idl_week_opt idl_day_opt 'T' idl_hour_opt idl_minute_opt idl_second
201              
202             sign ~ [+-]
203             digits ~ [\d]+
204             num_literal ~ num
205             num ~ digits
206             | sign digits
207             | digits '.' digits
208             | sign digits '.' digits
209             posnum ~ digits
210             | digits '.' digits
211              
212             op_unary ~ [+-]
213             op_addsub ~ [+-]
214              
215             op_mult ~ [*]
216             op_multdiv ~ [*/]
217              
218             :discard ~ ws
219             ws ~ [\s]+
220             ws_opt ~ [\s]*
221              
222             _
223             actions => {
224             datelit_iso => sub {
225 15     15   199213 my $h = shift;
226 15         59 my @date = split /-/, $_[0];
227 15         79 DateTime->new(year=>$date[0], month=>$date[1], day=>$date[2]);
228             },
229             date_sub_date => sub {
230 1     1   379 my $h = shift;
231 1         6 $_[0]->delta_days($_[2]);
232             },
233             datelit_special => sub {
234 4     4   57570 my $h = shift;
235 4 100       54 if ($_[0] eq 'now') {
    100          
    100          
    50          
236 1         8 DateTime->now;
237             } elsif ($_[0] eq 'today') {
238 1         25 DateTime->today;
239             } elsif ($_[0] eq 'yesterday') {
240 1         8 DateTime->today->subtract(days => 1);
241             } elsif ($_[0] eq 'tomorrow') {
242 1         5 DateTime->today->add(days => 1);
243             } else {
244 0         0 die "BUG: Unknown date literal '$_[0]'";
245             }
246             },
247             date_add_dur => sub {
248 2     2   296 my $h = shift;
249 2 100       9 if ($_[1] eq '+') {
250 1         8 $_[0] + $_[2];
251             } else {
252 1         5 $_[0] - $_[2];
253             }
254             },
255             dur_add_dur => sub {
256 1     1   147 my $h = shift;
257 1         5 $_[0] + $_[2];
258             },
259             dur_mult_num => sub {
260 4     4   625 my $h = shift;
261 4 100       13 if (ref $_[0]) {
262 2         3 my $d0 = $_[0];
263 2 100       6 if ($_[1] eq '*') {
264             # dur*num
265 1         6 DateTime::Duration->new(
266             years => $d0->years * $_[2],
267             months => $d0->months * $_[2],
268             weeks => $d0->weeks * $_[2],
269             days => $d0->days * $_[2],
270             hours => $d0->hours * $_[2],
271             minutes => $d0->minutes * $_[2],
272             seconds => $d0->seconds * $_[2],
273             );
274             } else {
275             # dur/num
276 1         6 DateTime::Duration->new(
277             years => $d0->years / $_[2],
278             months => $d0->months / $_[2],
279             weeks => $d0->weeks / $_[2],
280             days => $d0->days / $_[2],
281             hours => $d0->hours / $_[2],
282             minutes => $d0->minutes / $_[2],
283             seconds => $d0->seconds / $_[2],
284             );
285             }
286             } else {
287 2         4 my $d0 = $_[2];
288             # num * dur
289 2         7 DateTime::Duration->new(
290             years => $d0->years * $_[0],
291             months => $d0->months * $_[0],
292             weeks => $d0->weeks * $_[0],
293             days => $d0->days * $_[0],
294             hours => $d0->hours * $_[0],
295             minutes => $d0->minutes * $_[0],
296             seconds => $d0->seconds * $_[0],
297             );
298             }
299             },
300             durlit_nat => sub {
301 4     4   29430 my $h = shift;
302 4         18 local $_ = $_[0];
303 4         9 my %params;
304 4 50       32 $params{years} = $1 if /(-?\d+(?:\.\d+)?)\s*(years?|y)/;
305 4 50       32 $params{months} = $1 if /(-?\d+(?:\.\d+)?)\s*(mons?|months?)/;
306 4 100       25 $params{weeks} = $1 if /(-?\d+(?:\.\d+)?)\s*(weeks?|w)/;
307 4 100       26 $params{days} = $1 if /(-?\d+(?:\.\d+)?)\s*(days?|d)/;
308 4 100       32 $params{hours} = $1 if /(-?\d+(?:\.\d+)?)\s*(hours?|h)/;
309 4 100       22 $params{minutes} = $1 if /(-?\d+(?:\.\d+)?)\s*(mins?|minutes?)/;
310 4 100       22 $params{seconds} = $1 if /(-?\d+(?:\.\d+)?)\s*(s|secs?|seconds?)/;
311 4         18 DateTime::Duration->new(%params);
312             },
313             durlit_iso => sub {
314 16     16   198915 my $h = shift;
315             # split between date and time
316 16 50       139 my $d = $_[0] =~ /P(.+?)(?:T|\z)/ ? $1 : '';
317 16 100       57 my $t = $_[0] =~ /T(.*)/ ? $1 : '';
318             #say "D = $d, T = $t";
319 16         23 my %params;
320 16 100       85 $params{years} = $1 if $d =~ /(-?\d+(?:\.\d+)?)Y/i;
321 16 100       59 $params{months} = $1 if $d =~ /(-?\d+(?:\.\d+)?)M/i;
322 16 50       38 $params{weeks} = $1 if $d =~ /(-?\d+(?:\.\d+)?)W/;
323 16 100       67 $params{days} = $1 if $d =~ /(-?\d+(?:\.\d+)?)D/;
324 16 100       48 $params{hours} = $1 if $t =~ /(-?\d+(?:\.\d+)?)H/i;
325 16 100       38 $params{minutes} = $1 if $t =~ /(-?\d+(?:\.\d+)?)M/i;
326 16 100       41 $params{seconds} = $1 if $t =~ /(-?\d+(?:\.\d+)?)S/i;
327 16         78 DateTime::Duration->new(%params);
328             },
329             func_inum_onum => sub {
330 3     3   43190 my $h = shift;
331 3         8 my $fn = $_[0];
332 3         6 my $num = $_[1];
333 3 100       13 if ($fn eq 'abs') {
    50          
334 1         5 abs($num);
335             } elsif ($fn eq 'round') {
336 2         16 sprintf("%.0f", $num);
337             } else {
338 0         0 die "BUG: Unknown number function $fn";
339             }
340             },
341             func_idate_onum => sub {
342 9     9   3397 my $h = shift;
343 9         18 my $fn = $_[0];
344 9         13 my $d = $_[1];
345 9 100       71 if ($fn eq 'year') {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
    0          
    0          
346 1         6 $d->year;
347             } elsif ($fn eq 'month') {
348 1         5 $d->month;
349             } elsif ($fn eq 'day') {
350 1         7 $d->day;
351             } elsif ($fn eq 'dow') {
352 1         10 $d->day_of_week;
353             } elsif ($fn eq 'quarter') {
354 1         5 $d->quarter;
355             } elsif ($fn eq 'doy') {
356 1         12 $d->day_of_year;
357             } elsif ($fn eq 'wom') {
358 1         5 $d->week_of_month;
359             } elsif ($fn eq 'woy') {
360 1         5 $d->week_number;
361             } elsif ($fn eq 'doq') {
362 1         7 $d->day_of_quarter;
363             } elsif ($fn eq 'hour') {
364 0         0 $d->hour;
365             } elsif ($fn eq 'minute') {
366 0         0 $d->minute;
367             } elsif ($fn eq 'second') {
368 0         0 $d->second;
369             } else {
370 0         0 die "BUG: Unknown date function $fn";
371             }
372             },
373             func_idur_onum => sub {
374 8     8   1272 my $h = shift;
375 8         16 my $fn = $_[0];
376 8         12 my $dur = $_[1];
377 8 100       60 if ($fn eq 'years') {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
378 1         5 $dur->years;
379             } elsif ($fn eq 'months') {
380 1         4 $dur->months;
381             } elsif ($fn eq 'weeks') {
382 1         5 $dur->weeks;
383             } elsif ($fn eq 'days') {
384 1         6 $dur->days;
385             } elsif ($fn eq 'totdays') {
386 1         6 $dur->in_units("days");
387             } elsif ($fn eq 'hours') {
388 1         5 $dur->hours;
389             } elsif ($fn eq 'minutes') {
390 1         4 $dur->minutes;
391             } elsif ($fn eq 'seconds') {
392 1         5 $dur->seconds;
393             } else {
394 0         0 die "BUG: Unknown duration function $fn";
395             }
396             },
397             num_add => sub {
398 2     2   27751 my $h = shift;
399 2 100       9 if ($_[1] eq '+') {
400 1         4 $_[0] + $_[2];
401             } else {
402 1         4 $_[0] - $_[2];
403             }
404             },
405             num_mult => sub {
406 6     6   42709 my $h = shift;
407 6 100       15 if ($_[1] eq '*') {
408 5         14 $_[0] * $_[2];
409             } else {
410 1         5 $_[0] / $_[2];
411             }
412             },
413             num_unary => sub {
414 2     2   14347 my $h = shift;
415 2         6 my $op = $_[0];
416 2         3 my $num = $_[1];
417 2 100       6 if ($op eq '+') {
418 1         3 $num;
419             } else {
420             # -
421 1         4 -$num;
422             }
423             },
424             num_pow => sub {
425 2     2   13976 my $h = shift;
426 2         8 $_[0] ** $_[2];
427             },
428             },
429             trace_terminals => $ENV{DEBUG},
430             trace_values => $ENV{DEBUG},
431 1     1 1 180 );
432              
433 1         388233 bless {parser=>$parser}, shift;
434             }
435              
436             sub eval {
437 46     46 1 47414 my ($self, $str) = @_;
438 46         161 my $res = $self->{parser}->($str);
439              
440 43 100 100     13362 if (blessed($res) && $res->isa('DateTime::Duration')) {
    100 66        
441 10         27 __fmtduriso($res);
442             } elsif (blessed($res) && $res->isa('DateTime')) {
443 7         29 $res->ymd . "#".$res->day_abbr;
444             } else {
445 26         206 "$res";
446             }
447             }
448              
449             1;
450             # ABSTRACT: Date calculator
451              
452             __END__
453              
454             =pod
455              
456             =encoding UTF-8
457              
458             =head1 NAME
459              
460             App::datecalc - Date calculator
461              
462             =head1 VERSION
463              
464             This document describes version 0.090 of App::datecalc (from Perl distribution App-datecalc), released on 2023-02-18.
465              
466             =head1 SYNOPSIS
467              
468             use App::datecalc;
469             my $calc = App::datecalc->new;
470             say $calc->eval('2014-05-13 + 2 days'); # -> 2014-05-15
471              
472             =head1 DESCRIPTION
473              
474             B<This is an early release. More features and documentation will follow in
475             subsequent releases.>
476              
477             This module provides a date calculator, for doing date-related calculations. You
478             can write date literals in ISO 8601 format (though not all format variants are
479             supported), e.g. C<2014-05-13>. Date duration can be specified using the natural
480             syntax e.g. C<2 days 13 hours> or using the ISO 8601 format e.g. C<P2DT13H>.
481              
482             Currently supported calculations:
483              
484             =over
485              
486             =item * date literals
487              
488             2014-05-19
489             now
490             today
491             tomorrow
492              
493             =item * (NOT YET) time and date-time literals
494              
495             =item * duration literals, either in ISO 8601 format or natural syntax
496              
497             P3M2D
498             3 months 2 days
499              
500             =item * date addition/subtraction with a duration
501              
502             2014-05-19 - 2 days
503             2014-05-19 + P29W
504              
505             =item * date subtraction with another date
506              
507             2014-05-19 - 2013-12-25
508              
509             =item * duration addition/subtraction with another duration
510              
511             1 week 1 day + P10D
512              
513             =item * duration multiplication/division with a number
514              
515             P2D * 2
516             2 * P2D
517              
518             =item * extract elements from date
519              
520             year(2014-05-20)
521             quarter(today)
522             month(today)
523             day(today)
524             dow(today)
525             doy(today)
526             doq(today)
527             wom(today)
528             woy(today)
529             hour(today)
530             minute(today)
531             second(today)
532              
533             =item * extract elements from duration
534              
535             years(P22D)
536             months(P22D)
537             weeks(P22D)
538             days(P22D) # 1, because P22D normalizes to P3W1D
539             totdays(P22D) # 22
540             days(P1M1D) # 1
541             totdays(P1M1D) # 1, because months cannot be converted to days
542             hours(P22D)
543             minutes(P22D)
544             seconds(P22D)
545              
546             =item * some simple number arithmetics
547              
548             3+4.5
549             2**3 * P1D
550             abs(2-5) # 3
551             round(1.6+3) # 5
552              
553             =item * (NOT YET) date comparison
554              
555             today >= 2014-05-20
556              
557             =item * (NOT YET) duration comparison
558              
559             P20D < P3W
560              
561             =back
562              
563             =head1 METHODS
564              
565             =head2 new
566              
567             =head2 eval
568              
569             =head1 HOMEPAGE
570              
571             Please visit the project's homepage at L<https://metacpan.org/release/App-datecalc>.
572              
573             =head1 SOURCE
574              
575             Source repository is at L<https://github.com/perlancar/perl-App-datecalc>.
576              
577             =head1 SEE ALSO
578              
579             L<DateTime> and L<DateTime::Format::ISO8601>, the backend modules used to do the
580             actual date calculation.
581              
582             L<Marpa::R2> is used to generate the parser.
583              
584             L<Date::Calc> another date module on CPAN. No relation except the similarity of
585             name.
586              
587             L<http://en.wikipedia.org/wiki/ISO_8601> for more information about the ISO 8601
588             format.
589              
590             =head1 AUTHOR
591              
592             perlancar <perlancar@cpan.org>
593              
594             =head1 CONTRIBUTORS
595              
596             =for stopwords Jeffrey Kegler Steven Haryanto
597              
598             =over 4
599              
600             =item *
601              
602             Jeffrey Kegler <JKEGL@cpan.org>
603              
604             =item *
605              
606             Steven Haryanto <stevenharyanto@gmail.com>
607              
608             =back
609              
610             =head1 CONTRIBUTING
611              
612              
613             To contribute, you can send patches by email/via RT, or send pull requests on
614             GitHub.
615              
616             Most of the time, you don't need to build the distribution yourself. You can
617             simply modify the code, then test via:
618              
619             % prove -l
620              
621             If you want to build the distribution (e.g. to try to install it locally on your
622             system), you can install L<Dist::Zilla>,
623             L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
624             L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
625             Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
626             that are considered a bug and can be reported to me.
627              
628             =head1 COPYRIGHT AND LICENSE
629              
630             This software is copyright (c) 2023, 2018, 2016, 2015, 2014 by perlancar <perlancar@cpan.org>.
631              
632             This is free software; you can redistribute it and/or modify it under
633             the same terms as the Perl 5 programming language system itself.
634              
635             =head1 BUGS
636              
637             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-datecalc>
638              
639             When submitting a bug or request, please include a test-file or a
640             patch to an existing test-file that illustrates the bug or desired
641             feature.
642              
643             =cut