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              
34             use 5.012;
35 42     42   87023 use strict;
  42         125  
36 42     42   245 use warnings;
  42         129  
  42         843  
37 42     42   193  
  42         76  
  42         1060  
38             use App::CELL qw( $CELL $log );
39 42     42   199 use Date::Calc qw(
  42         79  
  42         3823  
40 42         3165 Add_Delta_Days
41             Date_to_Days
42             Day_of_Week
43             );
44 42     42   15061 use Date::Holidays::CZ qw( holidays );
  42         192342  
45 42     42   17260 use Params::Validate qw( :all );
  42         62378  
  42         2017  
46 42     42   285  
  42         87  
  42         5704  
47              
48              
49              
50             =head1 NAME
51              
52             App::Dochazka::REST::Holiday - non-database holiday and date routines
53              
54              
55              
56              
57             =head1 SYNOPSIS
58              
59             use App::Dochazka::REST::Holiday qw( holidays_in_daterange );
60              
61             my $holidays1 = holidays_in_daterange(
62             begin => '2001-01-02',
63             end => '2001-12-24',
64             );
65             my $holidays2 = holidays_in_daterange(
66             begin => '2001-01-02',
67             end => '2002-12-24',
68             );
69              
70             *WARNING*: C<holidays_in_daterange()> makes no attempt to validate the date
71             range. It assumes this validation has already taken place, and that the dates
72             are in YYYY-MM-DD format!
73              
74              
75              
76              
77             =head1 EXPORTS
78              
79             =cut
80              
81             use Exporter qw( import );
82 42     42   254 our @EXPORT_OK = qw(
  42         102  
  42         63533  
83             calculate_hours
84             canon_date_diff
85             canon_to_ymd
86             get_tomorrow
87             holidays_and_weekends
88             holidays_in_daterange
89             is_weekend
90             tsrange_to_dates_and_times
91             ymd_to_canon
92             );
93              
94              
95              
96             =head1 FUNCTIONS
97              
98              
99             =head2 calculate_hours
100              
101             Given a canonicalized tsrange, return the number of hours. For example, if
102             the range is [ 2016-01-06 08:00, 2016-01-06 09:00 ), the return value will
103             be 1. If the range is [ 2016-01-06 08:00, 2016-01-07 09:00 ), the return
104             value will 25.
105              
106             Returns 0 if there's a problem with the tsrange argument.
107              
108             =cut
109              
110             my $tsr = shift;
111             $log->debug( "Entering " . __PACKAGE__ . "::calculate_hours with tsr $tsr" );
112 4     4 1 4516  
113 4         28 my ( $begin_date, $begin_time, $end_date, $end_time ) =
114             $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}/;
115 4         228  
116             return 0 unless $begin_date and $begin_time and $end_date and $end_time;
117              
118 4 50 33     53 my $days = canon_date_diff( $begin_date, $end_date );
      33        
      33        
119              
120 4         11 if ( $days == 0 ) {
121             return _single_day_hours( $begin_time, $end_time )
122 4 100       10 }
123 2         4
124             return _single_day_hours( $begin_time, '24:00' ) +
125             ( ( $days - 1 ) * 24 ) +
126 2         5 _single_day_hours( '00:00', $end_time );
127             }
128              
129              
130             =head2 canon_date_diff
131              
132             Compute difference (in days) between two canonical dates
133              
134             =cut
135              
136             my ( $date, $date1 ) = @_;
137             my ( $date_days, $date1_days ) = (
138             Date_to_Days( canon_to_ymd( $date ) ),
139 4     4 1 9 Date_to_Days( canon_to_ymd( $date1 ) ),
140 4         7 );
141             return abs( $date_days - $date1_days );
142             }
143              
144 4         18  
145             =head2 canon_to_ymd
146              
147             Takes canonical date YYYY-MM-DD and returns $y, $m, $d
148              
149             =cut
150              
151             my ( $date ) = @_;
152             return unless $date;
153              
154             return ( $date =~ m/(\d+)-(\d+)-(\d+)/ );
155 8     8 1 14 }
156 8 50       12  
157              
158 8         42 =head2 holidays_in_daterange
159              
160             Given a PARAMHASH containing two properties, C<begin> and C<end>, the values of
161             which are canonicalized dates (possibly produced by the C<split_tsrange()>
162             function), determine the holidays that fall within this range. The function will
163             always return a status object. Upon success, the payload will contain a hashref
164             with the following structure:
165              
166             {
167             '2015-01-01' => '',
168             '2015-05-01' => '',
169             }
170              
171             The idea is that this hash can be used to quickly look up if a given date is a
172             holiday.
173              
174             =cut
175              
176             my ( %ARGS ) = validate( @_, {
177             begin => { type => SCALAR },
178             end => { type => SCALAR },
179             } );
180              
181 4     4 1 2140 my $begin_year = _extract_year( $ARGS{begin} );
182             my $end_year = _extract_year( $ARGS{end} );
183              
184             # transform daterange into an array of hashes containing "begin", "end"
185             # in other words:
186 4         20 # INPUT: { begin => '1901-06-30', end => '1903-03-15' }
187 4         7 # becomes
188             # OUTPUT: [
189             # { begin => '1901-06-30', end => '1901-12-31' },
190             # { begin => '1902-01-01', end => '1902-12-31' },
191             # { begin => '1903-01-01', end => '1903-03-15' },
192             # ]
193             my $daterange_by_year = _daterange_by_year(
194             begin_year => $begin_year,
195             end_year => $end_year,
196             begin_date => $ARGS{begin},
197             end_date => $ARGS{end},
198             );
199            
200             my %retval;
201              
202             foreach my $year ( sort( keys %{ $daterange_by_year } ) ) {
203 4         9 my $holidays = holidays( YEAR => $year, FORMAT => '%Y-%m-%d', WEEKENDS => 1 );
204             if ( $year eq $begin_year and $year eq $end_year ) {
205 4         6 my $tmp_holidays = _eliminate_dates( $holidays, $ARGS{begin}, "before" );
206             $holidays = _eliminate_dates( $tmp_holidays, $ARGS{end}, "after" );
207 4         5 map { $retval{$_} = ''; } @$holidays;
  4         11  
208 4         21 } elsif ( $year eq $begin_year ) {
209 4 50 33     4713 map { $retval{$_} = ''; } @{ _eliminate_dates( $holidays, $ARGS{begin}, "before" ) };
    0          
    0          
210 4         11 } elsif ( $year eq $end_year ) {
211 4         6 map { $retval{$_} = ''; } @{ _eliminate_dates( $holidays, $ARGS{end}, "after" ) };
212 4         17 } else {
  10         27  
213             map { $retval{$_} = ''; } @$holidays;
214 0         0 }
  0         0  
  0         0  
215             }
216 0         0  
  0         0  
  0         0  
217             return \%retval;
218 0         0 }
  0         0  
219              
220              
221             =head2 is_weekend
222 4         23  
223             Simple function that takes a canonicalized date string in
224             the format YYYY-MM-DD and returns a true or false value
225             indicating whether or not the date falls on a weekend.
226              
227             =cut
228              
229             my $cdate = shift; # cdate == Canonicalized Date String YYYY-MM-DD
230             my ( $year, $month, $day ) = $cdate =~ m/(\d{4})-(\d{2})-(\d{2})/;
231             my $dow = Day_of_Week( $year, $month, $day );
232             return ( $dow == 6 or $dow == 7 )
233             ? 1
234             : 0;
235 240     240 1 693 }
236 240         725  
237 240         534  
238 240 100 100     652 =head2 get_tomorrow
239              
240             Given a canonicalized date string in the format YYYY-MM-DD, return
241             the next date (i.e. "tomorrow" from the perspective of the given date).
242              
243             =cut
244              
245             my $cdate = shift; # cdate == Canonicalized Date String YYYY-MM-DD
246             my ( $year, $month, $day ) = $cdate =~ m/(\d{4})-(\d{2})-(\d{2})/;
247             my ( $tyear, $tmonth, $tday ) = Add_Delta_Days( $year, $month, $day, 1 );
248             return "$tyear-" . sprintf( "%02d", $tmonth ) . "-" . sprintf( "%02d", $tday );
249             }
250              
251              
252 545     545 1 4386 =head2 holidays_and_weekends
253 545         1672  
254 545         1312 Given a date range (same as in C<holidays_in_daterange>, above), return
255 545         1713 a reference to a hash of hashes that looks like this (for sample dates):
256              
257             {
258             '2015-01-01' => { holiday => 1 },
259             '2015-01-02' => {},
260             '2015-01-03' => { weekend => 1 },
261             '2015-01-04' => { weekend => 1 },
262             '2015-01-05' => {},
263             '2015-01-06' => {},
264             }
265              
266             Note that the range is always considered inclusive -- i.e. the bounding
267             dates of the range will be included in the hash.
268              
269             =cut
270              
271             my ( %ARGS ) = validate( @_, {
272             begin => { type => SCALAR },
273             end => { type => SCALAR },
274             } );
275             my $holidays = holidays_in_daterange( %ARGS );
276             my $res = {};
277             my $d = $ARGS{begin};
278             $log->debug( "holidays_and_weekends \$d == $d" );
279 1     1 1 1286 while ( $d ne get_tomorrow( $ARGS{end} ) ) {
280             $res->{ $d } = {};
281             if ( is_weekend( $d ) ) {
282             $res->{ $d }->{ 'weekend' } = 1;
283 1         6 }
284 1         2 if ( exists( $holidays->{ $d } ) ) {
285 1         3 $res->{ $d }->{ 'holiday' } = 1;
286 1         13 }
287 1         1830 $d = get_tomorrow( $d );
288 30         63 }
289 30 100       43 return $res;
290 8         16 }
291              
292 30 100       52  
293 1         11 =head2 tsrange_to_dates_and_times
294              
295 30         38 Takes a string that might be a canonicalized tsrange. Attempts to extract
296             beginning and ending dates (YYYY-MM-DD) from it. If this succeeds, an OK status
297 1         7 object is returned, the payload of which is a hash suitable for passing to
298             holidays_and_weekends().
299              
300             =cut
301              
302             my ( $tsrange ) = @_;
303              
304             my ( $begin_date, $begin_time, $end_date, $end_time ) =
305             $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}/;
306              
307             # if begin_time is 24:00 convert it to 00:00
308             if ( $begin_time eq '24:00' ) {
309             my ( $y, $m, $d ) = canon_to_ymd( $begin_date );
310             $log->debug( "Before Add_Delta_Days $y $m $d" );
311 0     0 1 0 ( $y, $m, $d ) = Add_Delta_Days( $y, $m, $d, 1 );
312             $begin_date = ymd_to_canon( $y, $m, $d );
313 0         0 }
314             # if end_time is 00:00 convert it to 24:00
315             if ( $end_time eq '00:00' ) {
316             my ( $y, $m, $d ) = canon_to_ymd( $end_date );
317 0 0       0 $log->debug( "Before Add_Delta_Days $y-$m-$d" );
318 0         0 ( $y, $m, $d ) = Add_Delta_Days( $y, $m, $d, -1 );
319 0         0 $end_date = ymd_to_canon( $y, $m, $d );
320 0         0 }
321 0         0  
322             return $CELL->status_ok( 'DOCHAZKA_NORMAL_COMPLETION',
323             payload => { begin => [ $begin_date, $begin_time ],
324 0 0       0 end => [ $end_date, $end_time ] } );
325 0         0 }
326 0         0  
327 0         0  
328 0         0 =head2 ymd_to_canon
329              
330             Takes $y, $m, $d and returns canonical date YYYY-MM-DD
331 0         0  
332             =cut
333              
334             my ( $y, $m, $d ) = @_;
335              
336             if ( $y < 1 or $y > 9999 or $m < 1 or $m > 99 or $d < 1 or $d > 99 ) {
337             die "AUCKLANDERS! ymd out of range!!";
338             }
339              
340             return sprintf( "%04d-%02d-%02d", $y, $m, $d );
341             }
342              
343              
344 0     0 1 0 # HELPER FUNCTIONS
345              
346 0 0 0     0 my ( %ARGS ) = validate( @_, {
      0        
      0        
      0        
      0        
347 0         0 begin_year => { type => SCALAR },
348             end_year => { type => SCALAR },
349             begin_date => { type => SCALAR },
350 0         0 end_date => { type => SCALAR },
351             } );
352             my $year_delta = $ARGS{end_year} - $ARGS{begin_year};
353             if ( $year_delta == 0 ) {
354             return { $ARGS{begin_year} => { begin => $ARGS{begin}, end => $ARGS{end} } };
355             }
356             if ( $year_delta == 1 ) {
357 4     4   76 return {
358             $ARGS{begin_year} => { begin => $ARGS{begin}, end => "$ARGS{begin_year}-12-31" },
359             $ARGS{end_year} => { begin => "$ARGS{end_year}-01-01", end => $ARGS{end} },
360             };
361             }
362             my @intervening_years = ( ($ARGS{begin_year}+1)..($ARGS{end_year}-1) );
363 4         25 my %retval = (
364 4 50       10 $ARGS{begin_year} => { begin => $ARGS{begin}, end => "$ARGS{begin_year}-12-31" },
365 4         19 $ARGS{end_year} => { begin => "$ARGS{end_year}-01-01", end => $ARGS{end} },
366             );
367 0 0       0 foreach my $year ( @intervening_years ) {
368             $retval{ $year } = { begin => "$year-01-01", end => "$year-12-31" };
369             }
370             return \%retval;
371 0         0 }
372              
373 0         0 # $inequality can be "before" or "after"
374             my ( $holidays, $date, $inequality ) = @_;
375             my @retval;
376             foreach my $holiday ( @$holidays ) {
377 0         0 if ( $inequality eq 'before' ) {
378 0         0 push @retval, $holiday if $holiday ge $date;
379 0         0 } elsif ( $inequality eq 'after' ) {
380             push @retval, $holiday if $holiday le $date;
381 0         0 } else {
382             die 'AG@D##KDW####!!!';
383             }
384             }
385             return \@retval;
386 8     8   15 }
387 8         9  
388 8         14 my $date = shift;
389 94 100       138 my ( $year ) = $date =~ m/(\d+)-\d+-\d+/;
    50          
390 52 100       98 return $year;
391             }
392 42 100       68  
393             # Given two strings in the format HH:MM representing a starting and an ending
394 0         0 # time, calculate and return the number of hours.
395             my ( $begin, $end ) = @_;
396             my ( $bh, $begin_minutes ) = $begin =~ m/(\d+):(\d+)/;
397 8         16 my $begin_hours = $bh + $begin_minutes / 60;
398             my ( $eh, $end_minutes ) = $end =~ m/(\d+):(\d+)/;
399             my $end_hours = $eh + $end_minutes / 60;
400             return $end_hours - $begin_hours;
401 8     8   11 }
402 8         39  
403 8         16 1;