File Coverage

blib/lib/Date/Holidays/KZ.pm
Criterion Covered Total %
statement 61 70 87.1
branch 25 38 65.7
condition 7 15 46.6
subroutine 14 16 87.5
pod 5 5 100.0
total 112 144 77.7


line stmt bran cond sub pod time code
1             package Date::Holidays::KZ;
2             our $VERSION = '0.2020.0'; # VERSION
3              
4             =encoding utf8
5              
6             =head1 NAME
7              
8             Date::Holidays::KZ - Determine Kazakhstan official holidays and business days.
9              
10             =head1 SYNOPSIS
11              
12             use Date::Holidays::KZ qw( is_holiday holidays is_business_day );
13              
14             my ( $year, $month, $day ) = ( localtime )[ 5, 4, 3 ];
15             $year += 1900;
16             $month += 1;
17              
18             if ( my $holidayname = is_holiday( $year, $month, $day ) ) {
19             print "Today is a holiday: $holidayname\n";
20             }
21              
22             my $ref = holidays( $year );
23             while ( my ( $md, $name ) = each %$ref ) {
24             print "On $md there is a holiday named $name\n";
25             }
26              
27             if ( is_business_day( 2012, 03, 11 ) ) {
28             print "2012-03-11 is business day on weekend\n";
29             }
30              
31             if ( is_short_business_day( 2015, 04, 30 ) ) {
32             print "2015-04-30 is short business day\n";
33             }
34              
35             $Date::Holidays::KZ::strict=1;
36             # here we die because time outside from $HOLIDAYS_VALID_SINCE to $INACCURATE_TIMES_SINCE
37             holidays( 9001 );
38              
39             =cut
40              
41 5     5   277177 use warnings;
  5         48  
  5         145  
42 5     5   50 use strict;
  5         11  
  5         86  
43 5     5   20 use utf8;
  5         7  
  5         27  
44 5     5   119 use base 'Exporter';
  5         6  
  5         756  
45              
46             our @EXPORT_OK = qw(
47             is_holiday
48             is_kz_holiday
49             holidays
50             is_business_day
51             is_short_business_day
52             );
53              
54             =head2 $Date::Holidays::KZ::HOLIDAYS_VALID_SINCE, $Date::Holidays::KZ::INACCURATE_TIMES_SINCE
55              
56             HOLIDAYS_VALID_SINCE before this year package doesn't matter
57             INACCURATE_TIMES_SINCE after this year dates of holidays and working day shift are not accurate, but you can most likely be sure of historical holidays
58              
59             =cut
60              
61             our $HOLIDAYS_VALID_SINCE = 2017; # TODO add all old
62             our $INACCURATE_TIMES_SINCE = 2021;
63              
64              
65             =head2 $Date::Holidays::KZ::strict
66              
67             Allows you to return an error if the requested date is outside the determined times.
68             Default is 0.
69              
70             =cut
71              
72             our $strict = 0;
73              
74 5     5   29 use Carp;
  5         7  
  5         318  
75 5     5   2181 use Time::Piece;
  5         48948  
  5         27  
76 5     5   366 use List::Util qw/ first /;
  5         21  
  5         4342  
77              
78             # internal date formatting alike ISO 8601: MMDD
79             my @REGULAR_HOLIDAYS = (
80             {
81             name => 'Новый год',
82             days => [ qw( 0101 0102 ) ],
83             },
84             {
85             name => 'Православное рождество',
86             days => '0107',
87             },
88             {
89             name => 'Международный женский день',
90             days => '0308',
91             },
92             {
93             name => 'Наурыз мейрамы',
94             days => [ qw( 0321 0322 0323 ) ],
95             },
96             {
97             name => 'Праздник единства народа Казахстана',
98             days => '0501',
99             },
100             {
101             name => 'День защитника Отечества',
102             days => '0507',
103             },
104             {
105             name => 'День Победы',
106             days => '0509',
107             },
108             {
109             name => 'День Столицы',
110             days => '0706',
111             },
112             {
113             name => 'День Конституции Республики Казахстан',
114             days => '0830',
115             },
116             {
117             name => 'День Первого Президента Республики Казахстан',
118             days => '1201',
119             },
120             {
121             name => 'День Независимости',
122             days => [ qw( 1216 1217 ) ],
123             },
124             # Курбан-айта goes to HOLIDAYS_SPECIAL because based on the muslim calendar
125             );
126              
127             my %HOLIDAYS_SPECIAL = (
128             2017 => [ qw( 0103 0320 0508 0707 0901 1218 1219 ) ],
129             2018 => [ qw( 0821 0309 0508 0430 0831 1203 1218 1231 ) ],
130             2019 => [ qw( 0325 0510 0708 1202 0811 ) ],
131             2020 => [ qw( 0103 0309 0324 0325 0508 0831 1218 ) ],
132             );
133              
134              
135             my %BUSINESS_DAYS_ON_WEEKENDS = (
136             2017 => [ qw( 0318 0701 ) ],
137             2018 => [ qw( 0303 0505 0825 1229 ) ],
138             2019 => [ qw( 0504 ) ],
139             2020 => [ qw( 0105 1220 ) ],
140             );
141              
142             my %SHORT_BUSINESS_DAYS = (
143             );
144              
145             =head2 is_holiday( $year, $month, $day )
146              
147             Determine whether this date is a KZ holiday. Returns holiday name or undef.
148              
149             =cut
150              
151             sub is_holiday {
152 14     14 1 2700 my ( $year, $month, $day ) = @_;
153 14 50 33     75 croak 'Bad params' unless $year && $month && $day;
      33        
154              
155 14         37 return holidays( $year )->{ _get_date_key($month, $day) };
156             }
157              
158             =head2 is_kz_holiday( $year, $month, $day )
159              
160             Alias for is_holiday().
161              
162             =cut
163              
164             sub is_kz_holiday {
165 2     2 1 952 goto &is_holiday;
166             }
167              
168             =head2 holidays( $year )
169              
170             Returns hash ref of all KZ holidays in the year.
171              
172             =cut
173              
174             my %cache;
175             sub holidays {
176 16 50   16 1 740 my $year = shift or croak 'Bad year';
177              
178 16 100       57 return $cache{ $year } if $cache{ $year };
179              
180 9         24 my $holidays = _get_regular_holidays_by_year($year);
181              
182 6 100       16 if ( my $spec = $HOLIDAYS_SPECIAL{ $year } ) {
183 4         38 $holidays->{ $_ } = 'Перенос праздничного дня' for @$spec;
184             }
185              
186 6         22 return $cache{ $year } = $holidays;
187             }
188              
189             sub _get_regular_holidays_by_year {
190 9     9   17 my ($year) = @_;
191 9 100       59 croak "KZ holidays is not valid before $HOLIDAYS_VALID_SINCE" if $year < $HOLIDAYS_VALID_SINCE;
192 7 100       21 if ($strict) {
193 1 50       2 croak "KZ holidays is not valid after @{[ $INACCURATE_TIMES_SINCE - 1 ]}" if $year >= $INACCURATE_TIMES_SINCE;
  1         11  
194             }
195              
196 6         8 my %day;
197 6         14 for my $holiday (@REGULAR_HOLIDAYS) {
198 66         111 my $days = _resolve_yhash_value($holiday->{days}, $year);
199 66 50       109 next if !$days;
200 66 100       100 $days = [$days] if !ref $days;
201 66 50       97 next if !@$days;
202              
203 66         85 my $name = _resolve_yhash_value($holiday->{name}, $year);
204 66 50       102 croak "Name is not defined" if !$name; # assertion
205              
206 66         150 $day{$_} = $name for @$days;
207             }
208              
209 6         13 return \%day;
210             }
211              
212             sub _resolve_yhash_value {
213 132     132   199 my ($value, $year) = @_;
214 132 50       233 return $value if ref $value ne 'HASH';
215              
216 0     0   0 my $ykey = first {$year >= $_} reverse sort keys %$value;
  0         0  
217 0 0       0 return if !$ykey;
218 0         0 return $value->{$ykey};
219             }
220              
221              
222             =head2 is_business_day( $year, $month, $day )
223              
224             Returns true if date is a business day in KZ taking holidays and weekends into account.
225              
226             =cut
227              
228             sub is_business_day {
229 5     5 1 79 my ( $year, $month, $day ) = @_;
230              
231 5 50 33     25 croak 'Bad params' unless $year && $month && $day;
      33        
232              
233 5 100       14 return 0 if is_holiday( $year, $month, $day );
234              
235             # check if date is a weekend
236 4         19 my $t = Time::Piece->strptime( "$year-$month-$day", '%Y-%m-%d' );
237 4         252 my $wday = $t->day;
238 4 100 100     133 return 1 unless $wday eq 'Sat' || $wday eq 'Sun';
239              
240             # check if date is a business day on weekend
241 3 50       9 my $ref = $BUSINESS_DAYS_ON_WEEKENDS{ $year } or return 0;
242              
243 3         5 my $md = _get_date_key($month, $day);
244 3         7 for ( @$ref ) {
245 5 100       12 return 1 if $_ eq $md;
246             }
247              
248 2         9 return 0;
249             }
250              
251             =head2 is_short_business_day( $year, $month, $day )
252              
253             Returns true if date is a shortened business day in KZ.
254              
255             =cut
256              
257             sub is_short_business_day {
258 0     0 1 0 my ( $year, $month, $day ) = @_;
259              
260 0 0       0 my $short_days_ref = $SHORT_BUSINESS_DAYS{ $year } or return 0;
261              
262 0         0 my $date_key = _get_date_key($month, $day);
263 0         0 return !!grep { $_ eq $date_key } @$short_days_ref;
  0         0  
264             }
265              
266              
267             sub _get_date_key {
268 14     14   24 my ($month, $day) = @_;
269 14         71 return sprintf '%02d%02d', $month, $day;
270             }
271              
272             =head1 LICENSE
273              
274             This software is copyright (c) 2019 by Vladimir Varlamov.
275              
276             This is free software; you can redistribute it and/or modify it under
277             the same terms as the Perl 5 programming language system itself.
278              
279             Terms of the Perl programming language system itself
280              
281             a) the GNU General Public License as published by the Free
282             Software Foundation; either version 1, or (at your option) any
283             later version, or
284             b) the "Artistic License"
285              
286             =cut
287              
288              
289             =head1 AUTHOR
290              
291             Vladimir Varlamov, C<< >>
292              
293             =cut
294              
295              
296              
297             1;