File Coverage

blib/lib/Date/Holidays/BY.pm
Criterion Covered Total %
statement 74 78 94.8
branch 26 34 76.4
condition 17 30 56.6
subroutine 15 15 100.0
pod 5 5 100.0
total 137 162 84.5


line stmt bran cond sub pod time code
1             package Date::Holidays::BY;
2             our $VERSION = '2.2026.0'; # VERSION
3              
4             =encoding utf8
5              
6             =head1 NAME
7              
8             Date::Holidays::BY - Determine public holidays and business days in Belarus.
9              
10             =head1 SYNOPSIS
11              
12             use Date::Holidays::BY;
13              
14             my $holidays = Date::Holidays::BY::holidays( 2024 );
15             # {
16             # "0101" => "New Year",
17             # ...
18             # "1225" => "Christmas (Catholic Christmas)"
19             # }
20              
21             if ( my $holidayname = Date::Holidays::BY::is_holiday( 2007, 1, 1 ) ) {
22             print "Is a holiday: $holidayname\n";
23             }
24              
25             if ( Date::Holidays::BY::is_business_day( 2012, 3, 11 ) ) {
26             print "2012-03-11 is business day on weekend\n";
27             }
28              
29             if ( Date::Holidays::BY::is_short_business_day( 2015, 04, 30 ) ) {
30             print "2015-04-30 is short business day\n";
31             }
32              
33             =cut
34              
35             =head1 DESCRIPTION
36              
37             Date::Holidays::BY provides functions to check if a given date is a public holiday in Belarus. This module follows the standard holiday calendar observed in Belarus, including both national holidays and specific religious observances recognized in the country.
38              
39             Imports nothing by default.
40              
41             =cut
42              
43 7     7   798399 use warnings;
  7         13  
  7         403  
44 7     7   38 use strict;
  7         43  
  7         147  
45 7     7   32 use utf8;
  7         11  
  7         64  
46 7     7   226 use base 'Exporter';
  7         20  
  7         927  
47 7     7   40 use Carp;
  7         12  
  7         535  
48 7     7   38 use List::Util;
  7         10  
  7         14828  
49             #require Date::Easter;
50             #require Time::Piece;
51              
52             our @EXPORT_OK = qw(
53             is_holiday
54             is_by_holiday
55             holidays
56             is_business_day
57             is_short_business_day
58             );
59              
60              
61             =head1 CONFIGURATION VARIABLES
62              
63             =head2 $Date::Holidays::BY::ref
64              
65             Hash reference containing all static holiday data, including holiday names for i18n support.
66             Dates are formatted as MMDD (ISO 8601).
67              
68             =cut
69              
70             our $ref = {
71              
72             HOLIDAYS_VALID_SINCE => 1992,
73              
74             HOLIDAYS => {
75             '0101' => {
76             name => {
77             be => 'Новы год',
78             en => 'New Year',
79             ru => 'Новый год',
80             },
81             days => {
82             1992 => [ qw( 0101 ) ],
83             2020 => [ qw( 0101 0102 ) ],
84             },
85             },
86             '0308' => {
87             name => {
88             be => 'Дзень жанчын',
89             en => 'Women\'s Day',
90             ru => 'День женщин',
91             },
92             days => [ qw( 0308 ) ],
93             },
94             '0501' => {
95             name => {
96             be => 'Свята працы',
97             en => 'Labor Day',
98             ru => 'Праздник труда',
99             },
100             days => [ qw( 0501 ) ],
101             },
102             '0509' => {
103             name => {
104             be => 'Дзень Перамогі',
105             en => 'Victory Day',
106             ru => 'День Победы',
107             },
108             days => [ qw( 0509 ) ],
109             },
110             '0703' => {
111             name => {
112             be => 'Дзень незалежнасці Рэспублікі Беларусь',
113             en => 'Independence Day of the Republic of Belarus',
114             ru => 'День Независимости Республики Беларусь',
115             },
116             days => {
117             1991 => [ qw( 0727 ) ],
118             1997 => [ qw( 0703 ) ],
119             }
120             },
121             '1107' => {
122             name => {
123             be => 'Дзень Кастрычніцкай рэвалюцыі',
124             en => 'October Revolution Day',
125             ru => 'День Октябрьской революции',
126             },
127             days => [ qw( 1107 ) ],
128             },
129             '0107' => {
130             name => {
131             be => 'Раство Хрыстова (праваслаўнае Раство)',
132             en => 'Christmas (Orthodox Christmas)',
133             ru => 'Рождество Христово (православное Рождество)',
134             },
135             days => [ qw( 0107 ) ],
136             },
137             '1225' => {
138             name => {
139             be => 'Раство Хрыстова (каталіцкае Раство)',
140             en => 'Christmas (Catholic Christmas)',
141             ru => 'Рождество Христово (католическое Рождество)',
142             },
143             days => [ qw( 1225 ) ],
144             },
145             'rado' => {
146             name => {
147             be => 'Радаўніца',
148             en => 'Radunica',
149             ru => 'Радуница',
150             },
151             days => \&_radonitsa_mmdd,
152             },
153             'spec' => {
154             name => {
155             be => 'Перанос працоўнага дня',
156             en => 'Postponed working day',
157             ru => 'Перенос рабочего дня',
158             },
159             days => {
160             # ... TODO
161             2013 => [ qw( 0102 0510 ) ],
162             2014 => [ qw( 0102 0106 0430 0704 1226 ) ],
163             2015 => [ qw( 0102 0420 ) ],
164             2016 => [ qw( 0108 0307 ) ],
165             2017 => [ qw( 0102 0424 0425 0508 1106 ) ],
166             2018 => [ qw( 0102 0309 0416 0417 0430 0702 1224 1231 ) ],
167             2019 => [ qw( 0506 0507 0508 1108 ) ],
168             2020 => [ qw( 0106 0427 0428 ) ],
169             2021 => [ qw( 0108 0510 0511 ) ],
170             2022 => [ qw( 0307 0502 ) ],
171             2023 => [ qw( 0424 0508 1106 ) ],
172             2024 => [ qw( 0513 1108 ) ],
173             2025 => [ qw( 0106 0428 0704 1226 ) ],
174             2026 => [ qw( 0420 ) ],
175             },
176             },
177             },
178              
179             BUSINESS_DAYS_ON_WEEKENDS => {
180             'all' => {
181             name => {
182             be => 'Працоўны дзень у выхадныя дні',
183             en => 'Working day on weekends',
184             ru => 'Рабочий день в выходные дни',
185             },
186             days => {
187             # ... TODO
188             2013 => [ qw( 0105 0518 ) ],
189             2014 => [ qw( 0104 0111 0503 0712 1220 ) ],
190             2015 => [ qw( 0110 0425 ) ],
191             2016 => [ qw( 0116 0305 ) ],
192             2017 => [ qw( 0121 0429 0506 1104 ) ],
193             2018 => [ qw( 0120 0303 0414 0428 0707 1222 1229 ) ],
194             2019 => [ qw( 0504 0511 1116 ) ],
195             2020 => [ qw( 0104 0404 ) ],
196             2021 => [ qw( 0116 0515 ) ],
197             2022 => [ qw( 0312 0514 ) ],
198             2023 => [ qw( 0429 0513 1111 ) ],
199             2024 => [ qw( 0518 1116 ) ],
200             2025 => [ qw( 0111 0426 0712 1220 ) ],
201             2026 => [ qw( 0425 ) ],
202             },
203             },
204             },
205              
206              
207             SHORT_BUSINESS_DAYS => {
208             'all' => {
209             name => {
210             be => 'Перадсвяточны працоўны дзень',
211             en => 'Pre-holiday working day',
212             ru => 'Предпраздничный рабочий день',
213             },
214             days => {
215             # ... TODO
216             2014 => [ qw( 0428 0508 0702 1106 1224 1231 ) ],
217             2015 => [ qw( 0106 0430 0508 0702 1106 1224 ) ],
218             2016 => [ qw( 0106 ) ],
219             2017 => [ qw( 0106 0307 0429 0506 1104 ) ],
220             2018 => [ qw( 0307 0508 1106 ) ],
221             2019 => [ qw( 0307 0430 0506 0702 1106 1224 ) ],
222             # ... TODO
223             },
224             },
225             },
226              
227             };
228              
229             sub _radonitsa_mmdd {
230 16     16   219090 my $year=$_[0];
231 16 50       65 if ($year < 1583) {croak "Module has limitation in counting Easter outside the period 1583-7666";}
  0         0  
232 16 50 66     104 if ($year >= 2038 && "$]" < 5.012 && (eval{require Config; $Config::Config{ivsize}} < 8)) {croak "Require perl>=5.12.0 because 2038 problem";}
  0   33     0  
  0         0  
  0         0  
233 16         3607 require Date::Easter;
234 16         19045 my ($easter_month, $easter_day) = Date::Easter::orthodox_easter($year);
235 16         5184 require Time::Piece;
236 16         59367 return [ (Time::Piece->strptime("$year-$easter_month-$easter_day", '%Y-%m-%d') + 9*3600*24)->strftime('%m%d') ];
237             }
238              
239              
240             =head3 HOLIDAYS_VALID_SINCE
241              
242             C<< $Date::Holidays::BY::ref->{'HOLIDAYS_VALID_SINCE'} >>
243              
244             The module is only relevant from this year onward. Throws an exception (croak) for dates before this year.
245              
246             =head3 INACCURATE_TIMES_BEFORE - INACCURATE_TIMES_SINCE
247              
248             C<< $Date::Holidays::BY::ref->{'INACCURATE_TIMES_BEFORE'} >>
249              
250             C<< $Date::Holidays::BY::ref->{'INACCURATE_TIMES_SINCE'} >>
251              
252             Outside this period, postponement of a holidays are not specified and in the strict mode C<$Date::Holidays::BY::strict=1> throws an exception (croak). But you can be sure of the periodic holidays.
253              
254             =cut
255              
256             $ref->{'INACCURATE_TIMES_BEFORE'} = (sort keys %{$ref->{'HOLIDAYS'}->{'spec'}->{'days'}})[0];
257             $ref->{'INACCURATE_TIMES_SINCE'} = (reverse sort keys %{$ref->{'HOLIDAYS'}->{'spec'}->{'days'}})[0] + 1;
258              
259              
260             =head2 $Date::Holidays::BY::strict
261              
262             Allows you to throws an exception (croak) if the requested date is outside the determined times.
263             Default is 0.
264              
265             =cut
266              
267             our $strict = 0;
268              
269              
270             =head2 $Date::Holidays::BY::lang
271              
272             The language is determined by the locale from C<$ENV{LANG}>. Allows you to override this language after loading the module.
273              
274             =cut
275              
276             my $envlang = lc substr(( $ENV{LANG} || $ENV{LC_ALL} || $ENV{LC_MESSAGES} || '' ), 0, 2);
277             $envlang = (List::Util::first {/^\Q$envlang\E$/} qw(be en ru)) || 'en';
278             our $lang = $lang || $envlang;
279              
280              
281             =head2 $Date::Holidays::BY::HOLIDAYS_VALID_SINCE
282              
283             Deprecated. See C<< $Date::Holidays::BY::ref->{'HOLIDAYS_VALID_SINCE'} >>
284              
285             =cut
286              
287             our $HOLIDAYS_VALID_SINCE = $ref->{'HOLIDAYS_VALID_SINCE'};
288              
289              
290             =head2 $Date::Holidays::BY::INACCURATE_TIMES_SINCE
291              
292             Deprecated. See C<< $Date::Holidays::BY::ref->{'INACCURATE_TIMES_BEFORE'} >> and C<< $Date::Holidays::BY::ref->{'INACCURATE_TIMES_SINCE'} >>
293              
294             =cut
295              
296             our $INACCURATE_TIMES_SINCE = $ref->{'INACCURATE_TIMES_SINCE'};
297              
298              
299              
300             =head1 FUNCTIONS
301              
302             =head2 holidays( $year )
303              
304             Returns hash ref of all holidays in the year.
305              
306             {
307             MMDD => 'name',
308             ...
309             }
310              
311             Сaches the result for the selected language for the specified year in a variable C<$Date::Holidays::ref-E{'cache'}>
312              
313             =cut
314              
315             sub holidays {
316 23 50   23 1 2040 my $year = shift or croak 'Bad year';
317              
318 23 100       124 return $ref->{'cache'}->{'HOLIDAYS'}->{$lang}->{$year} if $ref->{'cache'}->{'HOLIDAYS'}->{$lang}->{$year};
319              
320 17 100       105 croak "BY holidays is not valid before $ref->{'HOLIDAYS_VALID_SINCE'}" if $year < $ref->{'HOLIDAYS_VALID_SINCE'};
321 14 50 33     49 if ($strict && ($year < $ref->{'INACCURATE_TIMES_BEFORE'} || $year >= $ref->{'INACCURATE_TIMES_SINCE'} )) {
      66        
322 1         4 croak "BY holidays are not valid outside the period @{[ $ref->{'INACCURATE_TIMES_BEFORE'} ]}-@{[ $ref->{'INACCURATE_TIMES_SINCE'} - 1 ]}";
  1         6  
  1         20  
323             }
324              
325 13         44 for my $key (keys %{$ref->{'HOLIDAYS'}}) {
  13         78  
326              
327 130   33     312 my $name = _resolve_yhash_value( $ref->{'HOLIDAYS'}->{$key}->{'name'}, $year )->{$lang} || croak "Name is not defined";
328              
329 130         189 for my $md (@{_resolve_yhash_value( $ref->{'HOLIDAYS'}->{$key}->{'days'}, $year )}) {
  130         241  
330 165         3133 $ref->{'cache'}->{'HOLIDAYS'}->{$lang}->{$year}->{$md} = $name;
331             }
332              
333             }
334              
335 13         66 return $ref->{'cache'}->{'HOLIDAYS'}->{$lang}->{$year};
336             }
337              
338             sub _resolve_yhash_value {
339 267     267   462 my ($value, $year) = @_;
340 267 100       577 return $value->($year) if ref $value eq 'CODE';
341 254 100       542 return $value if ref $value ne 'HASH';
342 176         236 my @keys = keys %{$value};
  176         492  
343 176 100       677 return $value if $keys[0] !~ /^\d\d\d\d$/;
344 46     159   386 my $ykey = List::Util::first { $year >= $_ } reverse sort @keys;
  159         299  
345 46 50       191 return [] if !$ykey;
346 46 50       142 return $value->{$ykey}->($year) if ref $value->{$ykey} eq 'CODE';
347 46         166 return $value->{$ykey};
348             }
349              
350              
351             =head2 is_holiday( $year, $month, $day )
352              
353             Determine whether this date is a BY holiday. Returns holiday name or undef.
354              
355             =cut
356              
357             sub is_holiday {
358 21     21 1 610492 my ( $year, $month, $day ) = @_;
359 21 100 100     185 croak 'Bad params' unless $year && $month && $day;
      66        
360              
361 19         66 return holidays( $year )->{ _get_date_key($month, $day) };
362             }
363              
364              
365             =head2 is_by_holiday( $year, $month, $day )
366              
367             Alias for is_holiday().
368              
369             =cut
370              
371             sub is_by_holiday {
372 1     1 1 162354 goto &is_holiday;
373             }
374              
375              
376             =head2 is_business_day( $year, $month, $day )
377              
378             Returns true if date is a business day in BY taking holidays and weekends into account.
379              
380             =cut
381              
382             sub is_business_day {
383 5     5 1 154021 my ( $year, $month, $day ) = @_;
384 5 50 33     32 croak 'Bad params' unless $year && $month && $day;
      33        
385              
386 5 50       12 return 0 if is_holiday( $year, $month, $day );
387              
388             # check if date is a weekend
389 5         23 require Time::Piece;
390 5         21 my $t = Time::Piece->strptime( "$year-$month-$day", '%Y-%m-%d' );
391 5         114 my $wday = $t->day;
392 5 100 100     42 return 1 unless $wday eq 'Sat' || $wday eq 'Sun';
393              
394             # check if date is a business day on weekend
395 4         4 for my $md (@{_resolve_yhash_value($ref->{'BUSINESS_DAYS_ON_WEEKENDS'}->{'all'}->{'days'}, $year)}) {
  4         8  
396 13 100       15 if ($md eq _get_date_key($month, $day)) {return 1;}
  1         16  
397             }
398              
399 3         14 return 0;
400             }
401              
402              
403             =head2 is_short_business_day( $year, $month, $day )
404              
405             Returns true if date is a shortened business day.
406              
407             =cut
408              
409             sub is_short_business_day {
410 3     3 1 187892 my ( $year, $month, $day ) = @_;
411              
412 3         7 for my $md (@{_resolve_yhash_value($ref->{'SHORT_BUSINESS_DAYS'}->{'all'}->{'days'}, $year)}) {
  3         15  
413 13 100       28 if ($md eq _get_date_key($month, $day)) {return 1;}
  1         10  
414             }
415              
416 2         10 return 0;
417             }
418              
419              
420             sub _get_date_key {
421 41     41   69 my ($month, $day) = @_;
422 41         207 return sprintf '%02d%02d', $month, $day;
423             }
424              
425              
426             =head1 I18N
427              
428             Translations are available in Belarusian (be), English (en), and Russian (ru), with the language selected based on the locale from C<$ENV{LANG}>. If not mapped, "en" is used by default. See C<$Date::Holidays::BY::lang>.
429              
430             The module supports localization of holiday names, which can be redefined if needed in C<$Date::Holidays::BY::ref>:
431              
432             use Date::Holidays::BY;
433              
434             $Date::Holidays::BY::ref->{'HOLIDAYS'}->{'0308'}->{'name'}->{'en'} = 'name1';
435              
436             say Date::Holidays::BY::holidays(2024)->{'0308'}; # name1
437              
438             $Date::Holidays::BY::ref->{'HOLIDAYS'}->{'0308'}->{'name'}->{'en'} = 'name2';
439             $Date::Holidays::BY::ref->{'cache'} = undef;
440              
441             say Date::Holidays::BY::holidays(2024)->{'0308'}; # name2
442              
443             =cut
444              
445             =head1 LICENSE
446              
447             This software is copyright (c) 2025 by Vladimir Varlamov.
448              
449             This is free software; you can redistribute it and/or modify it under
450             the same terms as the Perl 5 programming language system itself.
451              
452             Terms of the Perl programming language system itself
453              
454             a) the GNU General Public License as published by the Free
455             Software Foundation; either version 1, or (at your option) any
456             later version, or
457             b) the "Artistic License"
458              
459             =cut
460              
461              
462             =head1 AUTHOR
463              
464             Vladimir Varlamov, C<< >>
465              
466             =cut
467              
468              
469              
470             1;