File Coverage

blib/lib/Date/Holidays/BY.pm
Criterion Covered Total %
statement 81 81 100.0
branch 36 46 78.2
condition 12 18 66.6
subroutine 16 16 100.0
pod 5 5 100.0
total 150 166 90.3


line stmt bran cond sub pod time code
1             package Date::Holidays::BY;
2             our $VERSION = '1.2023.0'; # VERSION
3              
4             =encoding utf8
5              
6             =head1 NAME
7              
8             Date::Holidays::BY - Determine Belorussian official holidays and business days.
9              
10             =head1 SYNOPSIS
11              
12             use Date::Holidays::BY 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::BY::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   354798 use warnings;
  5         59  
  5         161  
42 5     5   28 use strict;
  5         9  
  5         90  
43 5     5   22 use utf8;
  5         7  
  5         27  
44 5     5   154 use base 'Exporter';
  5         37  
  5         676  
45 5     5   34 use Carp;
  5         20  
  5         488  
46              
47             our @EXPORT_OK = qw(
48             is_holiday
49             is_by_holiday
50             holidays
51             is_business_day
52             is_short_business_day
53             );
54              
55             =head2 $Date::Holidays::BY::HOLIDAYS_VALID_SINCE, $Date::Holidays::BY::INACCURATE_TIMES_SINCE
56              
57             HOLIDAYS_VALID_SINCE before this year package doesn't matter
58             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
59              
60             =cut
61              
62 5     5   46 use List::Util;
  5         10  
  5         6687  
63              
64             our $HOLIDAYS_VALID_SINCE = 2016; # TODO add all old
65             our $INACCURATE_TIMES_SINCE = 2023;
66              
67             =head2 $Date::Holidays::BY::strict
68              
69             Allows you to return an error if the requested date is outside the determined times.
70             Default is 0.
71              
72             =cut
73              
74             our $strict = 0;
75              
76             # internal date formatting alike ISO 8601: MMDD
77             my @REGULAR_HOLIDAYS = (
78             {
79             name => 'Новый год',
80             days => {
81             1992 => '0101',
82             2020 => [ qw( 0101 0102 ) ],
83             },
84             },
85             {
86             name => 'Международный женский день',
87             days => '0308',
88             },
89             {
90             name => 'Праздник труда',
91             days => '0501',
92             },
93             {
94             name => 'День Победы',
95             days => '0509',
96             },
97             {
98             name => 'День Независимости Республики Беларусь',
99             days => '0703',
100             },
101             {
102             name => 'День Октябрьской революции',
103             days => '1107',
104             },
105             {
106             name => 'Рождество Христово (православное Рождество)',
107             days => '0107',
108             },
109             {
110             name => 'Рождество Христово (католическое Рождество)',
111             days => '1225',
112             },
113             {
114             name => 'Радоница',
115             days => \&_radonitsa_mmdd,
116             },
117             );
118              
119             my %HOLIDAYS_SPECIAL = (
120             2016 => [ qw( 0108 0307 ) ],
121             2017 => [ qw( 0102 0424 0425 0508 1106 ) ],
122             2018 => [ qw( 0102 0309 0416 0417 0430 0702 1224 1231 ) ],
123             2019 => [ qw( 0506 0507 0508 1108 ) ],
124             2020 => [ qw( 0106 0427 0428 ) ],
125             2021 => [ qw( 0108 0510 0511 ) ],
126             2022 => [ qw( 0307 0502 ) ],
127             2023 => [ qw( 0424 0508 1106 ) ],
128             );
129              
130             my %BUSINESS_DAYS_ON_WEEKENDS = (
131             2016 => [ qw( 0116 0305 ) ],
132             2017 => [ qw( 0121 0429 0506 1104 ) ],
133             2018 => [ qw( 0120 0303 0414 0428 0707 1222 1229 ) ],
134             2019 => [ qw( 0504 0511 1116 ) ],
135             2020 => [ qw( 0104 0404 ) ],
136             2021 => [ qw( 0116 0515 ) ],
137             2022 => [ qw( 0312 0514 ) ],
138             2023 => [ qw( 0429 0513 1111 ) ],
139             );
140              
141             my %SHORT_BUSINESS_DAYS = (
142             2016 => [ qw( 0106 ) ],
143             2017 => [ qw( 0106 0307 0429 0506 1104 ) ],
144             2018 => [ qw( 0307 0508 1106 ) ],
145             2019 => [ qw( 0307 0430 0506 0702 1106 1224 ) ],
146             2020 => [ qw( ) ],
147             2021 => [ qw( ) ],
148             2022 => [ qw( ) ],
149             2023 => [ qw( ) ],
150             );
151              
152              
153              
154             sub _radonitsa_mmdd {
155 12     12   111 my $year=$_[0];
156 12 100 66     64 if ($year < 1583 || $year > 7666) {croak "Module has limitation in counting Easter outside the period 1583-7666";}
  1         19  
157 11         2015 require Date::Easter;
158 11         14653 my ($easter_month, $easter_day) = Date::Easter::orthodox_easter($year);
159 11         1115 my $radonitsa_month = $easter_month;
160 11         21 my $radonitsa_day = $easter_day + 9;
161 11 100       33 if ( $radonitsa_day > 30 ) {
162 2         5 $radonitsa_month++;
163 2         4 $radonitsa_day -= 30;
164             }
165 11         41 return _get_date_key($radonitsa_month, $radonitsa_day);
166             }
167              
168             =head2 is_holiday( $year, $month, $day )
169              
170             Determine whether this date is a BY holiday. Returns holiday name or undef.
171              
172             =cut
173              
174             sub is_holiday {
175 15     15 1 4592 my ( $year, $month, $day ) = @_;
176 15 100 100     121 croak 'Bad params' unless $year && $month && $day;
      66        
177              
178 13         44 return holidays( $year )->{ _get_date_key($month, $day) };
179             }
180              
181             =head2 is_by_holiday( $year, $month, $day )
182              
183             Alias for is_holiday().
184              
185             =cut
186              
187             sub is_by_holiday {
188 1     1 1 116 goto &is_holiday;
189             }
190              
191             =head2 holidays( $year )
192              
193             Returns hash ref of all BY holidays in the year.
194              
195             =cut
196              
197             my %cache;
198             sub holidays {
199 17 50   17 1 1932 my $year = shift or croak 'Bad year';
200              
201 17 100       57 return $cache{ $year } if $cache{ $year };
202              
203 11         43 my $holidays = _get_regular_holidays_by_year($year);
204              
205 8 100       28 if ( my $spec = $HOLIDAYS_SPECIAL{ $year } ) {
206 6         40 $holidays->{ $_ } = 'Перенос праздничного дня' for @$spec;
207             }
208              
209 8         32 return $cache{ $year } = $holidays;
210             }
211              
212             sub _get_regular_holidays_by_year {
213 11     11   25 my ($year) = @_;
214 11 100       63 croak "BY holidays is not valid before $HOLIDAYS_VALID_SINCE" if $year < $HOLIDAYS_VALID_SINCE;
215 10 100       39 if ($strict) {
216 1 50       5 croak "BY holidays is not valid after @{[ $INACCURATE_TIMES_SINCE - 1 ]}" if $year >= $INACCURATE_TIMES_SINCE;
  1         14  
217             }
218              
219 9         14 my %day;
220 9         24 for my $holiday (@REGULAR_HOLIDAYS) {
221 81         154 my $days = _resolve_yhash_value($holiday->{days}, $year);
222 80 50       155 next if !$days;
223 80 100       527 $days = [$days] if !ref $days;
224 80 50       151 next if !@$days;
225              
226 80         136 my $name = _resolve_yhash_value($holiday->{name}, $year);
227 80 50       138 croak "Name is not defined" if !$name; # assertion
228              
229 80         300 $day{$_} = $name for @$days;
230             }
231              
232 8         21 return \%day;
233             }
234              
235             sub _resolve_yhash_value {
236 161     161   286 my ($value, $year) = @_;
237 161 100       305 return $value->($year) if ref $value eq 'CODE';
238 152 100       303 return $value if ref $value ne 'HASH';
239              
240 9     13   89 my $ykey = List::Util::first {$year >= $_} reverse sort keys %$value;
  13         45  
241 9 50       38 return if !$ykey;
242 9 50       27 return $value->{$ykey}->($year) if ref $value->{$ykey} eq 'CODE';
243 9         23 return $value->{$ykey};
244             }
245              
246              
247             =head2 is_business_day( $year, $month, $day )
248              
249             Returns true if date is a business day in BY taking holidays and weekends into account.
250              
251             =cut
252              
253             sub is_business_day {
254 5     5 1 114 my ( $year, $month, $day ) = @_;
255              
256 5 50 33     34 croak 'Bad params' unless $year && $month && $day;
      33        
257              
258 5 50       12 return 0 if is_holiday( $year, $month, $day );
259              
260             # check if date is a weekend
261 5         534 require Time::Piece;
262 5         7728 my $t = Time::Piece->strptime( "$year-$month-$day", '%Y-%m-%d' );
263 5         298 my $wday = $t->day;
264 5 100 100     90 return 1 unless $wday eq 'Sat' || $wday eq 'Sun';
265              
266             # check if date is a business day on weekend
267 4 50       14 my $ref = $BUSINESS_DAYS_ON_WEEKENDS{ $year } or return 0;
268              
269 4         8 my $md = _get_date_key($month, $day);
270 4         11 for ( @$ref ) {
271 13 100       62 return 1 if $_ eq $md;
272             }
273              
274 3         16 return 0;
275             }
276              
277             =head2 is_short_business_day( $year, $month, $day )
278              
279             Returns true if date is a shortened business day in BY.
280              
281             =cut
282              
283             sub is_short_business_day {
284 3     3 1 86 my ( $year, $month, $day ) = @_;
285              
286 3 100       15 my $short_days_ref = $SHORT_BUSINESS_DAYS{ $year } or return 0;
287              
288 2         6 my $date_key = _get_date_key($month, $day);
289 2         6 return !!grep { $_ eq $date_key } @$short_days_ref;
  10         29  
290             }
291              
292              
293             sub _get_date_key {
294 27     27   63 my ($month, $day) = @_;
295 27         152 return sprintf '%02d%02d', $month, $day;
296             }
297              
298             =head1 LICENSE
299              
300             This software is copyright (c) 2022 by Vladimir Varlamov.
301              
302             This is free software; you can redistribute it and/or modify it under
303             the same terms as the Perl 5 programming language system itself.
304              
305             Terms of the Perl programming language system itself
306              
307             a) the GNU General Public License as published by the Free
308             Software Foundation; either version 1, or (at your option) any
309             later version, or
310             b) the "Artistic License"
311              
312             =cut
313              
314              
315             =head1 AUTHOR
316              
317             Vladimir Varlamov, C<< >>
318              
319             =cut
320              
321              
322              
323             1;