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.2021.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   329453 use warnings;
  5         51  
  5         197  
42 5     5   28 use strict;
  5         11  
  5         93  
43 5     5   21 use utf8;
  5         10  
  5         28  
44 5     5   135 use base 'Exporter';
  5         9  
  5         920  
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 = 2016; # TODO add all old
62             our $INACCURATE_TIMES_SINCE = 2022;
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   35 use Carp;
  5         7  
  5         328  
75 5     5   2651 use Time::Piece;
  5         57132  
  5         25  
76 5     5   426 use List::Util qw/ first /;
  5         11  
  5         5123  
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             2016 => [ qw( 0104 0307 0502 0510 0912 1219 ) ],
129             2017 => [ qw( 0103 0320 0508 0707 0901 1218 1219 ) ],
130             2018 => [ qw( 0821 0309 0508 0430 0831 1203 1218 1231 ) ],
131             2019 => [ qw( 0325 0510 0708 1202 0811 ) ],
132             2020 => [ qw( 0103 0309 0324 0325 0508 0831 1218 ) ],
133             2021 => [ qw( 0104 0303 0324 0720 0910 ) ],
134             );
135              
136              
137             my %BUSINESS_DAYS_ON_WEEKENDS = (
138             2016 => [ qw( 0305 ) ],
139             2017 => [ qw( 0318 0701 ) ],
140             2018 => [ qw( 0303 0505 0825 1229 ) ],
141             2019 => [ qw( 0504 ) ],
142             2020 => [ qw( 0105 1220 ) ],
143             );
144              
145             my %SHORT_BUSINESS_DAYS = (
146             );
147              
148             =head2 is_holiday( $year, $month, $day )
149              
150             Determine whether this date is a KZ holiday. Returns holiday name or undef.
151              
152             =cut
153              
154             sub is_holiday {
155 14     14 1 3330 my ( $year, $month, $day ) = @_;
156 14 50 33     90 croak 'Bad params' unless $year && $month && $day;
      33        
157              
158 14         57 return holidays( $year )->{ _get_date_key($month, $day) };
159             }
160              
161             =head2 is_kz_holiday( $year, $month, $day )
162              
163             Alias for is_holiday().
164              
165             =cut
166              
167             sub is_kz_holiday {
168 2     2 1 1011 goto &is_holiday;
169             }
170              
171             =head2 holidays( $year )
172              
173             Returns hash ref of all KZ holidays in the year.
174              
175             =cut
176              
177             my %cache;
178             sub holidays {
179 16 50   16 1 749 my $year = shift or croak 'Bad year';
180              
181 16 100       63 return $cache{ $year } if $cache{ $year };
182              
183 9         37 my $holidays = _get_regular_holidays_by_year($year);
184              
185 6 100       21 if ( my $spec = $HOLIDAYS_SPECIAL{ $year } ) {
186 4         32 $holidays->{ $_ } = 'Перенос праздничного дня' for @$spec;
187             }
188              
189 6         25 return $cache{ $year } = $holidays;
190             }
191              
192             sub _get_regular_holidays_by_year {
193 9     9   22 my ($year) = @_;
194 9 100       73 croak "KZ holidays is not valid before $HOLIDAYS_VALID_SINCE" if $year < $HOLIDAYS_VALID_SINCE;
195 7 100       21 if ($strict) {
196 1 50       15 croak "KZ holidays is not valid after @{[ $INACCURATE_TIMES_SINCE - 1 ]}" if $year >= $INACCURATE_TIMES_SINCE;
  1         16  
197             }
198              
199 6         12 my %day;
200 6         16 for my $holiday (@REGULAR_HOLIDAYS) {
201 66         120 my $days = _resolve_yhash_value($holiday->{days}, $year);
202 66 50       113 next if !$days;
203 66 100       123 $days = [$days] if !ref $days;
204 66 50       109 next if !@$days;
205              
206 66         94 my $name = _resolve_yhash_value($holiday->{name}, $year);
207 66 50       108 croak "Name is not defined" if !$name; # assertion
208              
209 66         181 $day{$_} = $name for @$days;
210             }
211              
212 6         13 return \%day;
213             }
214              
215             sub _resolve_yhash_value {
216 132     132   199 my ($value, $year) = @_;
217 132 50       254 return $value if ref $value ne 'HASH';
218              
219 0     0   0 my $ykey = first {$year >= $_} reverse sort keys %$value;
  0         0  
220 0 0       0 return if !$ykey;
221 0         0 return $value->{$ykey};
222             }
223              
224              
225             =head2 is_business_day( $year, $month, $day )
226              
227             Returns true if date is a business day in KZ taking holidays and weekends into account.
228              
229             =cut
230              
231             sub is_business_day {
232 5     5 1 98 my ( $year, $month, $day ) = @_;
233              
234 5 50 33     33 croak 'Bad params' unless $year && $month && $day;
      33        
235              
236 5 100       15 return 0 if is_holiday( $year, $month, $day );
237              
238             # check if date is a weekend
239 4         25 my $t = Time::Piece->strptime( "$year-$month-$day", '%Y-%m-%d' );
240 4         297 my $wday = $t->day;
241 4 100 100     118 return 1 unless $wday eq 'Sat' || $wday eq 'Sun';
242              
243             # check if date is a business day on weekend
244 3 50       10 my $ref = $BUSINESS_DAYS_ON_WEEKENDS{ $year } or return 0;
245              
246 3         6 my $md = _get_date_key($month, $day);
247 3         8 for ( @$ref ) {
248 5 100       16 return 1 if $_ eq $md;
249             }
250              
251 2         10 return 0;
252             }
253              
254             =head2 is_short_business_day( $year, $month, $day )
255              
256             Returns true if date is a shortened business day in KZ.
257              
258             =cut
259              
260             sub is_short_business_day {
261 0     0 1 0 my ( $year, $month, $day ) = @_;
262              
263 0 0       0 my $short_days_ref = $SHORT_BUSINESS_DAYS{ $year } or return 0;
264              
265 0         0 my $date_key = _get_date_key($month, $day);
266 0         0 return !!grep { $_ eq $date_key } @$short_days_ref;
  0         0  
267             }
268              
269              
270             sub _get_date_key {
271 14     14   28 my ($month, $day) = @_;
272 14         103 return sprintf '%02d%02d', $month, $day;
273             }
274              
275             =head1 LICENSE
276              
277             This software is copyright (c) 2021 by Vladimir Varlamov.
278              
279             This is free software; you can redistribute it and/or modify it under
280             the same terms as the Perl 5 programming language system itself.
281              
282             Terms of the Perl programming language system itself
283              
284             a) the GNU General Public License as published by the Free
285             Software Foundation; either version 1, or (at your option) any
286             later version, or
287             b) the "Artistic License"
288              
289             =cut
290              
291              
292             =head1 AUTHOR
293              
294             Vladimir Varlamov, C<< >>
295              
296             =cut
297              
298              
299              
300             1;