File Coverage

blib/lib/App/Dochazka/REST/Holiday.pm
Criterion Covered Total %
statement 92 125 73.6
branch 19 36 52.7
condition 7 30 23.3
subroutine 19 21 90.4
pod 9 9 100.0
total 146 221 66.0


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2017, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32              
33             package App::Dochazka::REST::Holiday;
34              
35 42     42   109165 use 5.012;
  42         158  
36 42     42   246 use strict;
  42         138  
  42         882  
37 42     42   221 use warnings;
  42         93  
  42         1189  
38              
39 42     42   241 use App::CELL qw( $CELL $log );
  42         116  
  42         4481  
40 42         3636 use Date::Calc qw(
41             Add_Delta_Days
42             Date_to_Days
43             Day_of_Week
44 42     42   17672 );
  42         229244  
45 42     42   19957 use Date::Holidays::CZ qw( holidays );
  42         75655  
  42         2430  
46 42     42   332 use Params::Validate qw( :all );
  42         100  
  42         6719  
47              
48              
49              
50              
51             =head1 NAME
52              
53             App::Dochazka::REST::Holiday - non-database holiday and date routines
54              
55              
56              
57              
58             =head1 SYNOPSIS
59              
60             use App::Dochazka::REST::Holiday qw( holidays_in_daterange );
61              
62             my $holidays1 = holidays_in_daterange(
63             begin => '2001-01-02',
64             end => '2001-12-24',
65             );
66             my $holidays2 = holidays_in_daterange(
67             begin => '2001-01-02',
68             end => '2002-12-24',
69             );
70              
71             *WARNING*: C<holidays_in_daterange()> makes no attempt to validate the date
72             range. It assumes this validation has already taken place, and that the dates
73             are in YYYY-MM-DD format!
74              
75              
76              
77              
78             =head1 EXPORTS
79              
80             =cut
81              
82 42     42   311 use Exporter qw( import );
  42         100  
  42         73695  
83             our @EXPORT_OK = qw(
84             calculate_hours
85             canon_date_diff
86             canon_to_ymd
87             get_tomorrow
88             holidays_and_weekends
89             holidays_in_daterange
90             is_weekend
91             tsrange_to_dates_and_times
92             ymd_to_canon
93             );
94              
95              
96              
97             =head1 FUNCTIONS
98              
99              
100             =head2 calculate_hours
101              
102             Given a canonicalized tsrange, return the number of hours. For example, if
103             the range is [ 2016-01-06 08:00, 2016-01-06 09:00 ), the return value will
104             be 1. If the range is [ 2016-01-06 08:00, 2016-01-07 09:00 ), the return
105             value will 25.
106              
107             Returns 0 if there's a problem with the tsrange argument.
108              
109             =cut
110              
111             sub calculate_hours {
112 4     4 1 6018 my $tsr = shift;
113 4         34 $log->debug( "Entering " . __PACKAGE__ . "::calculate_hours with tsr $tsr" );
114              
115 4         293 my ( $begin_date, $begin_time, $end_date, $end_time ) =
116             $tsr =~ m/(\d{4}-\d{2}-\d{2}).+(\d{2}:\d{2}):\d{2}.+(\d{4}-\d{2}-\d{2}).+(\d{2}:\d{2}):\d{2}/;
117              
118 4 50 33     34 return 0 unless $begin_date and $begin_time and $end_date and $end_time;
      33        
      33        
119              
120 4         12 my $days = canon_date_diff( $begin_date, $end_date );
121              
122 4 100       11 if ( $days == 0 ) {
123 2         7 return _single_day_hours( $begin_time, $end_time )
124             }
125            
126 2         6 return _single_day_hours( $begin_time, '24:00' ) +
127             ( ( $days - 1 ) * 24 ) +
128             _single_day_hours( '00:00', $end_time );
129             }
130              
131              
132             =head2 canon_date_diff
133              
134             Compute difference (in days) between two canonical dates
135              
136             =cut
137              
138             sub canon_date_diff {
139 4     4 1 8 my ( $date, $date1 ) = @_;
140 4         11 my ( $date_days, $date1_days ) = (
141             Date_to_Days( canon_to_ymd( $date ) ),
142             Date_to_Days( canon_to_ymd( $date1 ) ),
143             );
144 4         14 return abs( $date_days - $date1_days );
145             }
146              
147              
148             =head2 canon_to_ymd
149              
150             Takes canonical date YYYY-MM-DD and returns $y, $m, $d
151              
152             =cut
153              
154             sub canon_to_ymd {
155 8     8 1 17 my ( $date ) = @_;
156 8 50       18 return unless $date;
157              
158 8         50 return ( $date =~ m/(\d+)-(\d+)-(\d+)/ );
159             }
160              
161              
162             =head2 holidays_in_daterange
163              
164             Given a PARAMHASH containing two properties, C<begin> and C<end>, the values of
165             which are canonicalized dates (possibly produced by the C<split_tsrange()>
166             function), determine the holidays that fall within this range. The function will
167             always return a status object. Upon success, the payload will contain a hashref
168             with the following structure:
169              
170             {
171             '2015-01-01' => '',
172             '2015-05-01' => '',
173             }
174              
175             The idea is that this hash can be used to quickly look up if a given date is a
176             holiday.
177              
178             =cut
179              
180             sub holidays_in_daterange {
181 4     4 1 2631 my ( %ARGS ) = validate( @_, {
182             begin => { type => SCALAR },
183             end => { type => SCALAR },
184             } );
185              
186 4         25 my $begin_year = _extract_year( $ARGS{begin} );
187 4         11 my $end_year = _extract_year( $ARGS{end} );
188              
189             # transform daterange into an array of hashes containing "begin", "end"
190             # in other words:
191             # INPUT: { begin => '1901-06-30', end => '1903-03-15' }
192             # becomes
193             # OUTPUT: [
194             # { begin => '1901-06-30', end => '1901-12-31' },
195             # { begin => '1902-01-01', end => '1902-12-31' },
196             # { begin => '1903-01-01', end => '1903-03-15' },
197             # ]
198             my $daterange_by_year = _daterange_by_year(
199             begin_year => $begin_year,
200             end_year => $end_year,
201             begin_date => $ARGS{begin},
202             end_date => $ARGS{end},
203 4         15 );
204            
205 4         7 my %retval;
206              
207 4         6 foreach my $year ( sort( keys %{ $daterange_by_year } ) ) {
  4         15  
208 4         13 my $holidays = holidays( YEAR => $year, FORMAT => '%Y-%m-%d', WEEKENDS => 1 );
209 4 50 33     5290 if ( $year eq $begin_year and $year eq $end_year ) {
    0          
    0          
210 4         16 my $tmp_holidays = _eliminate_dates( $holidays, $ARGS{begin}, "before" );
211 4         10 $holidays = _eliminate_dates( $tmp_holidays, $ARGS{end}, "after" );
212 4         10 map { $retval{$_} = ''; } @$holidays;
  10         35  
213             } elsif ( $year eq $begin_year ) {
214 0         0 map { $retval{$_} = ''; } @{ _eliminate_dates( $holidays, $ARGS{begin}, "before" ) };
  0         0  
  0         0  
215             } elsif ( $year eq $end_year ) {
216 0         0 map { $retval{$_} = ''; } @{ _eliminate_dates( $holidays, $ARGS{end}, "after" ) };
  0         0  
  0         0  
217             } else {
218 0         0 map { $retval{$_} = ''; } @$holidays;
  0         0  
219             }
220             }
221              
222 4         26 return \%retval;
223             }
224              
225              
226             =head2 is_weekend
227              
228             Simple function that takes a canonicalized date string in
229             the format YYYY-MM-DD and returns a true or false value
230             indicating whether or not the date falls on a weekend.
231              
232             =cut
233              
234             sub is_weekend {
235 240     240 1 835 my $cdate = shift; # cdate == Canonicalized Date String YYYY-MM-DD
236 240         888 my ( $year, $month, $day ) = $cdate =~ m/(\d{4})-(\d{2})-(\d{2})/;
237 240         641 my $dow = Day_of_Week( $year, $month, $day );
238 240 100 100     839 return ( $dow == 6 or $dow == 7 )
239             ? 1
240             : 0;
241             }
242              
243              
244             =head2 get_tomorrow
245              
246             Given a canonicalized date string in the format YYYY-MM-DD, return
247             the next date (i.e. "tomorrow" from the perspective of the given date).
248              
249             =cut
250              
251             sub get_tomorrow {
252 545     545 1 5293 my $cdate = shift; # cdate == Canonicalized Date String YYYY-MM-DD
253 545         2107 my ( $year, $month, $day ) = $cdate =~ m/(\d{4})-(\d{2})-(\d{2})/;
254 545         1705 my ( $tyear, $tmonth, $tday ) = Add_Delta_Days( $year, $month, $day, 1 );
255 545         2071 return "$tyear-" . sprintf( "%02d", $tmonth ) . "-" . sprintf( "%02d", $tday );
256             }
257              
258              
259             =head2 holidays_and_weekends
260              
261             Given a date range (same as in C<holidays_in_daterange>, above), return
262             a reference to a hash of hashes that looks like this (for sample dates):
263              
264             {
265             '2015-01-01' => { holiday => 1 },
266             '2015-01-02' => {},
267             '2015-01-03' => { weekend => 1 },
268             '2015-01-04' => { weekend => 1 },
269             '2015-01-05' => {},
270             '2015-01-06' => {},
271             }
272              
273             Note that the range is always considered inclusive -- i.e. the bounding
274             dates of the range will be included in the hash.
275              
276             =cut
277              
278             sub holidays_and_weekends {
279 1     1 1 1334 my ( %ARGS ) = validate( @_, {
280             begin => { type => SCALAR },
281             end => { type => SCALAR },
282             } );
283 1         9 my $holidays = holidays_in_daterange( %ARGS );
284 1         3 my $res = {};
285 1         3 my $d = $ARGS{begin};
286 1         18 $log->debug( "holidays_and_weekends \$d == $d" );
287 1         2501 while ( $d ne get_tomorrow( $ARGS{end} ) ) {
288 30         74 $res->{ $d } = {};
289 30 100       55 if ( is_weekend( $d ) ) {
290 8         17 $res->{ $d }->{ 'weekend' } = 1;
291             }
292 30 100       65 if ( exists( $holidays->{ $d } ) ) {
293 1         4 $res->{ $d }->{ 'holiday' } = 1;
294             }
295 30         54 $d = get_tomorrow( $d );
296             }
297 1         10 return $res;
298             }
299              
300              
301             =head2 tsrange_to_dates_and_times
302              
303             Takes a string that might be a canonicalized tsrange. Attempts to extract
304             beginning and ending dates (YYYY-MM-DD) from it. If this succeeds, an OK status
305             object is returned, the payload of which is a hash suitable for passing to
306             holidays_and_weekends().
307              
308             =cut
309              
310             sub tsrange_to_dates_and_times {
311 0     0 1 0 my ( $tsrange ) = @_;
312              
313 0         0 my ( $begin_date, $begin_time, $end_date, $end_time ) =
314             $tsrange =~ m/(\d{4}-\d{2}-\d{2}).+(\d{2}:\d{2}):\d{2}.+(\d{4}-\d{2}-\d{2}).+(\d{2}:\d{2}):\d{2}/;
315              
316             # if begin_time is 24:00 convert it to 00:00
317 0 0       0 if ( $begin_time eq '24:00' ) {
318 0         0 my ( $y, $m, $d ) = canon_to_ymd( $begin_date );
319 0         0 $log->debug( "Before Add_Delta_Days $y $m $d" );
320 0         0 ( $y, $m, $d ) = Add_Delta_Days( $y, $m, $d, 1 );
321 0         0 $begin_date = ymd_to_canon( $y, $m, $d );
322             }
323             # if end_time is 00:00 convert it to 24:00
324 0 0       0 if ( $end_time eq '00:00' ) {
325 0         0 my ( $y, $m, $d ) = canon_to_ymd( $end_date );
326 0         0 $log->debug( "Before Add_Delta_Days $y-$m-$d" );
327 0         0 ( $y, $m, $d ) = Add_Delta_Days( $y, $m, $d, -1 );
328 0         0 $end_date = ymd_to_canon( $y, $m, $d );
329             }
330              
331 0         0 return $CELL->status_ok( 'DOCHAZKA_NORMAL_COMPLETION',
332             payload => { begin => [ $begin_date, $begin_time ],
333             end => [ $end_date, $end_time ] } );
334             }
335              
336              
337             =head2 ymd_to_canon
338              
339             Takes $y, $m, $d and returns canonical date YYYY-MM-DD
340              
341             =cut
342              
343             sub ymd_to_canon {
344 0     0 1 0 my ( $y, $m, $d ) = @_;
345              
346 0 0 0     0 if ( $y < 1 or $y > 9999 or $m < 1 or $m > 99 or $d < 1 or $d > 99 ) {
      0        
      0        
      0        
      0        
347 0         0 die "AUCKLANDERS! ymd out of range!!";
348             }
349              
350 0         0 return sprintf( "%04d-%02d-%02d", $y, $m, $d );
351             }
352              
353              
354             # HELPER FUNCTIONS
355              
356             sub _daterange_by_year {
357 4     4   93 my ( %ARGS ) = validate( @_, {
358             begin_year => { type => SCALAR },
359             end_year => { type => SCALAR },
360             begin_date => { type => SCALAR },
361             end_date => { type => SCALAR },
362             } );
363 4         31 my $year_delta = $ARGS{end_year} - $ARGS{begin_year};
364 4 50       14 if ( $year_delta == 0 ) {
365 4         23 return { $ARGS{begin_year} => { begin => $ARGS{begin}, end => $ARGS{end} } };
366             }
367 0 0       0 if ( $year_delta == 1 ) {
368             return {
369             $ARGS{begin_year} => { begin => $ARGS{begin}, end => "$ARGS{begin_year}-12-31" },
370             $ARGS{end_year} => { begin => "$ARGS{end_year}-01-01", end => $ARGS{end} },
371 0         0 };
372             }
373 0         0 my @intervening_years = ( ($ARGS{begin_year}+1)..($ARGS{end_year}-1) );
374             my %retval = (
375             $ARGS{begin_year} => { begin => $ARGS{begin}, end => "$ARGS{begin_year}-12-31" },
376             $ARGS{end_year} => { begin => "$ARGS{end_year}-01-01", end => $ARGS{end} },
377 0         0 );
378 0         0 foreach my $year ( @intervening_years ) {
379 0         0 $retval{ $year } = { begin => "$year-01-01", end => "$year-12-31" };
380             }
381 0         0 return \%retval;
382             }
383              
384             # $inequality can be "before" or "after"
385             sub _eliminate_dates {
386 8     8   20 my ( $holidays, $date, $inequality ) = @_;
387 8         11 my @retval;
388 8         17 foreach my $holiday ( @$holidays ) {
389 94 100       160 if ( $inequality eq 'before' ) {
    50          
390 52 100       120 push @retval, $holiday if $holiday ge $date;
391             } elsif ( $inequality eq 'after' ) {
392 42 100       88 push @retval, $holiday if $holiday le $date;
393             } else {
394 0         0 die 'AG@D##KDW####!!!';
395             }
396             }
397 8         22 return \@retval;
398             }
399              
400             sub _extract_year {
401 8     8   13 my $date = shift;
402 8         48 my ( $year ) = $date =~ m/(\d+)-\d+-\d+/;
403 8         19 return $year;
404             }
405              
406             # Given two strings in the format HH:MM representing a starting and an ending
407             # time, calculate and return the number of hours.
408             sub _single_day_hours {
409 6     6   15 my ( $begin, $end ) = @_;
410 6         23 my ( $bh, $begin_minutes ) = $begin =~ m/(\d+):(\d+)/;
411 6         16 my $begin_hours = $bh + $begin_minutes / 60;
412 6         20 my ( $eh, $end_minutes ) = $end =~ m/(\d+):(\d+)/;
413 6         16 my $end_hours = $eh + $end_minutes / 60;
414 6         30 return $end_hours - $begin_hours;
415             }
416              
417             1;