File Coverage

blib/lib/Date/Gregorian/Business.pm
Criterion Covered Total %
statement 262 262 100.0
branch 84 84 100.0
condition 53 55 96.3
subroutine 42 42 100.0
pod 13 14 92.8
total 454 457 99.3


line stmt bran cond sub pod time code
1             # Copyright (c) 2005-2019 by Martin Becker, Blaubeuren.
2             # This package is free software; you can distribute it and/or modify it
3             # under the terms of the Artistic License 2.0 (see LICENSE file).
4              
5             package Date::Gregorian::Business;
6              
7 2     2   1013 use 5.006;
  2         10  
8 2     2   8 use strict;
  2         4  
  2         32  
9 2     2   7 use warnings;
  2         3  
  2         37  
10 2     2   410 use integer;
  2         12  
  2         7  
11 2     2   436 use Date::Gregorian qw(:weekdays);
  2         4  
  2         282  
12              
13             our @ISA = qw(Date::Gregorian);
14             our $VERSION = '0.13';
15              
16             # ----- object definition -----
17              
18             # ............. index .............. # .......... value ..........
19 2     2   11 use constant F_OFFSET => Date::Gregorian::NFIELDS;
  2         2  
  2         92  
20 2     2   10 use constant F_ALIGNMENT => F_OFFSET+0; # 0 = morning, 1 = evening
  2         2  
  2         87  
21 2     2   9 use constant F_MAKE_CAL => F_OFFSET+1; # sub (date, year) => [calendar]
  2         2  
  2         153  
22 2     2   12 use constant F_YEAR => F_OFFSET+2; # currently initialized year
  2         4  
  2         91  
23 2     2   9 use constant F_CALENDAR => F_OFFSET+3; # list of: 1 = biz, 0 = holiday
  2         9  
  2         82  
24 2     2   17 use constant NFIELDS => F_OFFSET+4;
  2         4  
  2         83  
25              
26             # ----- other constants -----
27              
28             # index into calendar definition
29 2     2   9 use constant _WEEKLY => 0; # array of non-biz weekdays
  2         3  
  2         80  
30 2     2   10 use constant _YEARLY => 1; # array of holidays per year
  2         1478  
  2         83  
31              
32             # index into single holiday per year definition
33 2     2   10 use constant _D_MONTH => 0; # month number or 0 for easter
  2         3  
  2         108  
34 2     2   10 use constant _D_DAY => 1; # day number or easter difference
  2         3  
  2         76  
35 2     2   9 use constant _D_DELTA => 2; # array of deltas per weekday
  2         3  
  2         64  
36 2     2   8 use constant _D_YEARS => 3; # array of first and last year
  2         4  
  2         2763  
37              
38             # ----- predefined variables -----
39              
40             # elements of default biz calendars
41             my $skip_weekend = [ 0, 0, 0, 0, 0, 2, 1]; # Sat, Sun -> Mon
42             my $avoid_weekend = [ 0, 0, 0, 0, 0, -1, 1]; # Sat -> Fri, Sun -> Mon
43             my $next_monday = [ 0, 6, 5, 4, 3, 2, 1]; # set_weekday(Mon, ">=")
44             my $prev_monday = [-7, -1, -2, -3, -4, -5, -6]; # set_weekday(Mon, "<")
45             my $next_wednesday = [ 2, 1, 0, 6, 5, 4, 3]; # set_weekday(Wed, ">=")
46             my $next_thursday = [ 3, 2, 1, 0, 6, 5, 4]; # set_weekday(Thu, ">=")
47             my $saturday_sunday = [SATURDAY, SUNDAY];
48              
49             # some biz calendars known by default
50             my %samples = (
51             'us' => [
52             $saturday_sunday,
53             [
54             [ 1, 1, $skip_weekend], # New Year's day
55             [ 1, 15, $next_monday], # Martin Luther King
56             [ 2, 15, $next_monday], # President's day
57             [ 6, 1, $prev_monday], # Memorial day
58             [ 7, 4, $avoid_weekend], # Independence day
59             [ 9, 1, $next_monday], # Labor day
60             [10, 8, $next_monday], # Columbus day
61             [11, 11, $avoid_weekend], # Veteran's day
62             [11, 22, $next_thursday], # Thanksgiving day
63             [12, 25, $skip_weekend], # Christmas day
64             ],
65             ],
66             'de' => [ # Germany
67             $saturday_sunday,
68             [
69             [ 1, 1], # New Year's day
70             [ 0, -2], # Good Friday
71             [ 0, 1], # Easter Monday
72             [ 5, 1], # Labour day
73             [ 0, 39], # Ascension day
74             [ 0, 50], # Pentecost Monday
75             [ 6, 17, undef, [1954, 1990]], # German Unity
76             [10, 3, undef, [1990, undef]], # German Unity
77             [10, 31, undef, [2017, 2017]], # Reformation day
78             [11, 16, $next_wednesday, [undef, 1994]], # Penitence day
79             [12, 25], # Christmas day
80             [12, 26], # 2nd day of Christmas
81             ],
82             ],
83             'dd' => [ # Germany, new states
84             $saturday_sunday,
85             [
86             [ 1, 1, ], # New Year's day
87             [ 0, -2, ], # Good Friday
88             [ 0, 1, undef, [undef, 1967]], # Easter Monday
89             [ 0, 1, undef, [1990, undef]], # Easter Monday
90             [ 5, 1, ], # Labour day
91             [ 5, 8, undef, [undef, 1967]], # Liberation day
92             [ 5, 8, undef, [1985, 1985]], # Liberation day
93             [ 5, 9, undef, [1975, 1975]], # Victory day
94             [ 0, 39, undef, [undef, 1967]], # Ascension day
95             [ 0, 39, undef, [1990, undef]], # Ascension day
96             [ 0, 50, ], # Pentecost Monday
97             [10, 3, undef, [1990, undef]], # German Unity
98             [10, 7, undef, [undef, 1989]], # Republic day
99             [10, 31, undef, [undef, 1966]], # Reformation day
100             [10, 31, undef, [1990, undef]], # Reformation day
101             [11, 16, $next_wednesday, [undef, 1966]], # Penitence day
102             [11, 16, $next_wednesday, [1990, 1994]], # Penitence day
103             [12, 25, ], # Christmas day
104             [12, 26, ], # 2nd day of Christmas
105             ],
106             ],
107             );
108             $samples{'de_BW'} = [ # Baden-Wuerttemberg
109             $saturday_sunday,
110             [
111             @{$samples{'de'}->[_YEARLY]},
112             [ 1, 6], # Epiphany
113             [ 0, 60], # Corpus Christi
114             [11, 1], # All Saints day
115             ]
116             ];
117             $samples{'de_BY'} = [ # Bayern
118             $saturday_sunday,
119             [
120             @{$samples{'de_BW'}->[_YEARLY]},
121             [ 8, 15], # Assumption day
122             ]
123             ];
124             $samples{'de_Augsburg'} = [ # Stadt Augsburg
125             $saturday_sunday,
126             [
127             @{$samples{'de_BY'}->[_YEARLY]},
128             [ 8, 1], # Peace day
129             ]
130             ];
131             $samples{'de_BE'} = [ # Berlin
132             $saturday_sunday,
133             [
134             @{$samples{'de'}->[_YEARLY]},
135             [ 1, 8, undef, [2019, undef]], # Women's day
136             ]
137             ];
138             $samples{'de_BB'} = [ # Brandenburg
139             $saturday_sunday,
140             [
141             @{$samples{'dd'}->[_YEARLY]},
142             ]
143             ];
144             $samples{'de_HB'} = [ # Bremen
145             $saturday_sunday,
146             [
147             @{$samples{'de'}->[_YEARLY]},
148             [10, 31], # Reformation day
149             ]
150             ];
151             $samples{'de_HH'} = [ # Hamburg
152             $saturday_sunday,
153             [
154             @{$samples{'de_HB'}->[_YEARLY]},
155             ]
156             ];
157             $samples{'de_HE'} = [ # Hessen
158             $saturday_sunday,
159             [
160             @{$samples{'de'}->[_YEARLY]},
161             [ 0, 60], # Corpus Christi
162             ]
163             ];
164             $samples{'de_MV'} = [ # Mecklenburg-Vorpommern
165             $saturday_sunday,
166             [
167             @{$samples{'dd'}->[_YEARLY]},
168             ]
169             ];
170             $samples{'de_NI'} = [ # Niedersachsen
171             $saturday_sunday,
172             [
173             @{$samples{'de_HB'}->[_YEARLY]},
174             ]
175             ];
176             $samples{'de_NW'} = [ # Nordrhein-Westfalen
177             $saturday_sunday,
178             [
179             @{$samples{'de_HE'}->[_YEARLY]},
180             [11, 1], # All Saints day
181             ]
182             ];
183             $samples{'de_RP'} = [ # Rheinland-Pfalz
184             $saturday_sunday,
185             [
186             @{$samples{'de_NW'}->[_YEARLY]},
187             ]
188             ];
189             $samples{'de_SL'} = [ # Saarland
190             $saturday_sunday,
191             [
192             @{$samples{'de_NW'}->[_YEARLY]},
193             [ 8, 15], # Assumption day
194             ]
195             ];
196             $samples{'de_SN'} = [ # Sachsen
197             $saturday_sunday,
198             [
199             @{$samples{'dd'}->[_YEARLY]},
200             [11, 16, $next_wednesday, [1995, undef]], # Penitence day
201             ]
202             ];
203             $samples{'de_ST'} = [ # Sachsen-Anhalt
204             $saturday_sunday,
205             [
206             @{$samples{'dd'}->[_YEARLY]},
207             [ 1, 6, undef, [1990, undef]], # Epiphany
208             ]
209             ];
210             $samples{'de_SH'} = [ # Schleswig-Holstein
211             $saturday_sunday,
212             [
213             @{$samples{'de_HB'}->[_YEARLY]},
214             ]
215             ];
216             $samples{'de_TH'} = [ # Thueringen
217             $saturday_sunday,
218             [
219             @{$samples{'dd'}->[_YEARLY]},
220             [ 9, 20, undef, [2019, undef]], # Children's day
221             ]
222             ];
223              
224             my $default_configuration = 'us';
225              
226             # ----- private functions and methods -----
227              
228             # check whether a given year is in a range or general selection
229             sub _select_year {
230 223     223   363 my ($self, $day, $year) = @_;
231 223         267 my $selection = $day->[3];
232 223 100       335 if (!ref $selection) {
233 39         104 return $year == $selection;
234             }
235 184 100       273 if ('CODE' eq ref $selection) {
236 39         43 return $selection->($self, $year, @{$day}[0, 1]);
  39         71  
237             }
238             return
239 145   66     640 (!defined($selection->[0]) || $selection->[0] <= $year) &&
240             (!defined($selection->[1]) || $year <= $selection->[1]);
241             }
242              
243             # make_cal factory, generating a calendar generator enclosing a configuration
244             sub _make_make_cal {
245 23     23   44 my ($weekly, $yearly) = @_;
246              
247             return sub {
248 165     165   222 my ($date, $year) = @_;
249 165         261 my $firstday = $date->new->set_yd($year, 1, 1);
250 165         297 my $first_wd = $firstday->get_weekday;
251 165   66     352 my $someday = @$yearly && $firstday->new;
252 165         189 my $easter = undef;
253 165         178 my $index;
254 165         288 my $calendar = $firstday->get_empty_calendar($year, $weekly);
255 165         266 foreach my $day (@$yearly) {
256 1484 100 100     2559 if (!defined($day->[3]) || _select_year($someday, $day, $year)) {
257 1355 100       1816 if ($day->[0]) {
258             $index =
259 1291         1462 $someday->set_ymd($year, @{$day}[0, 1])
  1291         2217  
260             ->get_days_since($firstday);
261 1291 100       2647 $index += $day->[2]->[$someday->get_weekday] if $day->[2];
262             }
263             else {
264 64 100       92 if (!defined $easter) {
265 15         46 $easter =
266             $someday->set_easter($year)
267             ->get_days_since($firstday);
268             }
269 64         77 $index = $easter + $day->[1];
270 64 100       95 $index += $day->[2]->[(496 + $day->[1]) % 7] if $day->[2];
271             }
272 1355 100 100     3816 $calendar->[$index] = 0 if 0 <= $index && $index < @$calendar;
273             }
274             }
275 165         620 return $calendar;
276 23         172 };
277             }
278              
279             # fetch biz calendar for given year, initializing it if necessary
280             sub _calendar {
281 5562     5562   7547 my ($self, $year) = @_;
282              
283 5562 100 100     13262 if (!defined($self->[F_YEAR]) || $year != $self->[F_YEAR]) {
284 181         227 $self->[F_YEAR] = $year;
285 181         293 $self->[F_CALENDAR] = $self->[F_MAKE_CAL]->($self, $year);
286             }
287 5562         11038 return $self->[F_CALENDAR];
288             }
289              
290             # ----- public methods -----
291              
292             sub get_empty_calendar {
293 183     183 1 1106 my ($date, $year, $weekly_nonbiz) = @_;
294              
295 183         261 my $firstday = $date->new->set_yd($year, 1);
296 183         367 my $days = $firstday->get_days_in_year($year);
297 183         328 my $first_wd = $firstday->get_weekday;
298              
299 183         369 my @week = (1) x 7;
300 183         303 foreach my $day (@$weekly_nonbiz) {
301 283         369 $week[$day] = 0;
302             }
303 183 100       521 @week = @week[$first_wd .. 6, 0 .. $first_wd-1] if $first_wd;
304              
305 183         2348 my @calendar = ((@week) x ($days / 7), @week[0 .. ($days % 7)-1]);
306 183         578 return \@calendar;
307             }
308              
309             sub define_configuration {
310 9     9 1 873 my ($class, $name, $configuration) = @_;
311 9 100       43 my $type = defined($configuration)? ref($configuration): '!';
312              
313 9 100 100     32 if (!$type) {
    100          
314 5 100       16 return undef if !exists $samples{$configuration};
315 3         5 $configuration = $samples{$configuration};
316             }
317             elsif ('ARRAY' ne $type && 'CODE' ne $type) {
318 2         7 return undef;
319             }
320 5         11 $samples{$name} = $configuration;
321 5         11 return $class;
322             }
323              
324             sub configure_business {
325 36     36 1 241 my ($self, $configuration) = @_;
326 36 100       73 my $type = defined($configuration)? ref($configuration): '!';
327              
328 36 100       66 if (!$type) {
329 22 100       65 return undef if !exists $samples{$configuration};
330 18         30 $configuration = $samples{$configuration};
331 18         30 $type = ref $configuration;
332             }
333 32 100       60 if (ref $self) {
334             # instance method: configure this object
335 28 100       72 if ('CODE' eq $type) {
    100          
336 4         8 $self->[F_MAKE_CAL] = $configuration;
337             }
338             elsif ('ARRAY' eq $type) {
339 23         53 $self->[F_MAKE_CAL] = _make_make_cal(@$configuration);
340             }
341             else {
342 1         4 return undef;
343             }
344 27         78 $self->[F_YEAR] = $self->[F_CALENDAR] = undef;
345             }
346             else {
347             # class method: configure default
348 4 100 100     14 if ('ARRAY' ne $type && 'CODE' ne $type) {
349 2         4 return undef;
350             }
351 2         5 $default_configuration = $configuration;
352             }
353              
354 29         99 return $self;
355             }
356              
357             sub new {
358 703     703 1 1817 my ($class_or_object, $configuration) = @_;
359 703         1237 my $self = $class_or_object->SUPER::new;
360              
361 703 100       1122 if (!ref $class_or_object) {
362 16         45 $self->[F_ALIGNMENT] = 0;
363             }
364 703 100       1374 if (defined $configuration) {
    100          
365 10         23 return $self->configure_business($configuration);
366             }
367             elsif (!ref $class_or_object) {
368 6         15 return $self->configure_business($default_configuration);
369             }
370 687         1307 return $self;
371             }
372              
373             sub align {
374 96     96 1 146 my ($self, $alignment) = @_;
375 96 100       149 $self->[F_ALIGNMENT] = $alignment? 1: 0;
376 96         282 return $self;
377             }
378              
379             sub get_alignment {
380 195     195 1 357 my $self = $_[0];
381 195         326 return $self->[F_ALIGNMENT];
382             }
383              
384             # tweak super class to provide default alignment
385             sub Date::Gregorian::get_alignment {
386 32     32 0 43 return 0;
387             }
388              
389             sub is_businessday {
390 5264     5264 1 14302 my ($self) = @_;
391 5264         7973 my ($year, $day) = $self->get_yd;
392              
393 5264         8199 return $self->_calendar($year)->[$day-1];
394             }
395              
396             # count business days, proceeding into the future
397             # $days gives the interval measured in real days (positive)
398             # alignment tells where to start: 0 = at current day, 1 = the day after
399             # 0 <= result <= $days
400             sub _count_businessdays_up {
401 63     63   86 my ($self, $days) = @_;
402 63         114 my ($year, $day) = $self->get_yd;
403 63         114 my $calendar = $self->_calendar($year);
404 63         74 my $result = 0;
405              
406 63 100       107 --$day if !$self->[F_ALIGNMENT];
407 63         106 while (0 < $days) {
408 3199         4149 while (@$calendar <= $day) {
409 6         11 $calendar = $self->_calendar(++$year);
410 6         10 $day = 0;
411             }
412 3199         3118 do {
413 2     2   13 no integer;
  2         4  
  2         5  
414 3199         3337 $result += $calendar->[$day];
415             };
416 3199         3106 ++$day;
417 3199         4172 --$days;
418             }
419 63         221 return $result;
420             }
421              
422             # count business days, proceeding into the past
423             # $days gives the interval measured in real days (positive)
424             # alignment tells where to start: 1 = at current day, 0 = the day before
425             # 0 <= result <= $days
426             sub _count_businessdays_down {
427 56     56   75 my ($self, $days) = @_;
428 56         106 my ($year, $day) = $self->get_yd;
429 56         89 my $calendar = $self->_calendar($year);
430 56         80 my $result = 0;
431              
432 56 100       90 --$day if !$self->[F_ALIGNMENT];
433 56         95 while (0 < $days) {
434 1081         1101 --$day;
435 1081         1067 --$days;
436 1081         1406 while ($day < 0) {
437 8         14 $calendar = $self->_calendar(--$year);
438 8         14 $day = $#$calendar;
439             }
440 1081         1089 do {
441 2     2   254 no integer;
  2         3  
  2         5  
442 1081         1476 $result += $calendar->[$day];
443             };
444             }
445 56         189 return $result;
446             }
447              
448             # Alignments and results Now:0 Now:1 Now:0 Now:1
449             # b--(H)--b---b---b--(H)--b---b Then:0 Then:1 Then:1 Then:0
450             # Then Now 3 3 3 3
451             # Then Now 3 2 2 3
452             # Then Now 3 4 3 4
453             # Then Now 3 3 2 4
454             # b--(H)--b---b---b--(H)--b---b
455             # Now Then -3 -3 -3 -3
456             # Now Then -3 -4 -4 -3
457             # Now Then -3 -2 -3 -2
458             # Now Then -3 -3 -4 -2
459             # b--(H)--b---b---b--(H)--b---b
460              
461             sub get_businessdays_since {
462 58     58 1 116 my ($self, $then) = @_;
463 58         152 my $delta =
464             $self->get_days_since($then) +
465             $self->[F_ALIGNMENT] - $then->get_alignment;
466 58 100       112 if ($delta > 0) {
467 29         54 return $self->_count_businessdays_down($delta);
468             }
469 29 100       55 if ($delta < 0) {
470 27         41 return -$self->_count_businessdays_up(-$delta);
471             }
472 2         6 return 0;
473             }
474              
475             sub get_businessdays_until {
476 65     65 1 158 my ($self, $then) = @_;
477 65         134 my $delta =
478             $self->get_days_since($then) +
479             $self->[F_ALIGNMENT] - $then->get_alignment;
480 65 100       151 if ($delta > 0) {
481 27         49 return -$self->_count_businessdays_down($delta);
482             }
483 38 100       73 if ($delta < 0) {
484 36         67 return $self->_count_businessdays_up(-$delta);
485             }
486 2         7 return 0;
487             }
488              
489             sub set_next_businessday {
490 40     40 1 64 my ($self, $relation) = @_;
491 40         74 my ($year, $day) = $self->get_yd;
492 40         73 my $calendar = $self->_calendar($year);
493              
494 40         48 --$day;
495 40 100 100     128 return $self if '<' ne $relation && '>' ne $relation && $calendar->[$day];
      100        
496 32 100 100     77 if ('<' eq $relation || '<=' eq $relation) {
497 16         18 do {
498 34         35 --$day;
499 34         61 while ($day < 0) {
500 14         21 $calendar = $self->_calendar(--$year);
501 14         39 $day = $#$calendar;
502             }
503             }
504             while (!$calendar->[$day]);
505             }
506             else {
507 16         18 do {
508 34         35 ++$day;
509 34         72 while (@$calendar <= $day) {
510 2         4 $calendar = $self->_calendar(++$year);
511 2         7 $day = 0;
512             }
513             }
514             while (!$calendar->[$day]);
515             }
516 32         68 return $self->set_yd($year, $day+1);
517             }
518              
519             sub iterate_businessdays_upto {
520 22     22 1 68 my ($self, $limit, $rel) = @_;
521 22         45 my $days = ($rel eq '<=') - $self->get_days_since($limit);
522 22         25 my ($year, $day, $calendar);
523 22 100       39 if (0 < $days) {
524 18         31 ($year, $day) = $self->get_yd;
525 18         21 --$day;
526 18         24 $calendar = $self->_calendar($year);
527             }
528             return sub {
529 70     70   280 while (0 < $days) {
530 89         134 while (@$calendar <= $day) {
531 2         5 $calendar = $self->_calendar(++$year);
532 2         5 $day = 0;
533             }
534 89         92 --$days;
535 89 100       157 if ($calendar->[$day++]) {
536 48         81 return $self->set_yd($year, $day);
537             }
538             }
539 22         33 return undef;
540 22         129 };
541             }
542              
543             sub iterate_businessdays_downto {
544 10     10 1 37 my ($self, $limit, $rel) = @_;
545 10         17 my $days = $self->get_days_since($limit) + ($rel ne '>');
546 10         14 my ($year, $day, $calendar);
547 10 100       18 if (0 < $days) {
548 6         13 ($year, $day) = $self->get_yd;
549 6         10 --$day;
550 6         18 $calendar = $self->_calendar($year);
551             }
552             return sub {
553 34     34   140 while (0 < $days) {
554 51         70 while ($day < 0) {
555 1         2 $calendar = $self->_calendar(--$year);
556 1         3 $day = $#$calendar;
557             }
558 51         54 --$days;
559 51 100       84 if ($calendar->[$day--]) {
560 24         42 return $self->set_yd($year, $day+2);
561             }
562             }
563 10         18 return undef;
564 10         67 };
565             }
566              
567             # -b----H----b----b----H----b-
568             # ^ ^ ^ ^
569             # 0 0 1 1 2 2
570              
571             sub add_businessdays {
572 2     2   1223 no integer;
  2         4  
  2         4  
573 72     72 1 118 my ($self, $days, $new_alignment) = @_;
574 72         270 my ($year, $day) = $self->get_yd;
575 72         91 -- $day;
576 72         129 my $calendar = $self->_calendar($year);
577 72         89 my $alignment = $self->[F_ALIGNMENT];
578              
579             # handle alignment change
580 72 100 100     193 if (defined($new_alignment) && ($alignment xor $new_alignment)) {
      100        
581 20 100       35 if ($new_alignment) {
582 10         15 $alignment = $self->[F_ALIGNMENT] = 1;
583 10         12 $days -= $calendar->[$day];
584             }
585             else {
586 10         12 $alignment = $self->[F_ALIGNMENT] = 0;
587 10         14 $days += $calendar->[$day];
588             }
589             }
590              
591 72 100 100     187 if (0 < $days || !$days && !$alignment) {
      100        
592             # move forward in time
593 42 100       68 $days -= $calendar->[$day] if !$alignment;
594 42   100     102 while (0 < $days || !$days && !$alignment) {
      100        
595 131         139 ++$day;
596 131         190 while (@$calendar <= $day) {
597 3         6 $calendar = $self->_calendar(++$year);
598 3         7 $day = 0;
599             }
600 131         298 $days -= $calendar->[$day];
601             }
602             }
603             else {
604             # move backwards in time
605 30 100       56 $days += $calendar->[$day] if $alignment;
606 30   100     77 while ($days < 0 || !$days && $alignment) {
      100        
607 107         117 --$day;
608 107         153 while ($day < 0) {
609 7         12 $calendar = $self->_calendar(--$year);
610 7         16 $day = $#$calendar;
611             }
612 107         217 $days += $calendar->[$day];
613             }
614             }
615              
616 72         192 return $self->set_yd($year, $day+1);
617             }
618              
619             1;
620              
621             __END__
622              
623             =head1 NAME
624              
625             Date::Gregorian::Business - business days extension for Date::Gregorian
626              
627             =head1 SYNOPSIS
628              
629             use Date::Gregorian::Business;
630             use Date::Gregorian qw(:weekdays);
631              
632             $date = Date::Gregorian::Business->new('us');
633              
634             if ($date->set_today->is_businessday) {
635             print "Busy today.\n";
636             }
637            
638             $date2 = $date->new->set_ymd(2005, 3, 14);
639              
640             $date2->align(0); # morning
641             $date->align(1); # evening
642              
643             $delta = $date->get_businessdays_since($date2);
644             $delta = -$date->get_businessdays_until($date2);
645              
646             $date->set_next_businessday('>=');
647             $date->add_businessdays(25);
648             $date->add_businessdays(-10, 0);
649             $date->add_businessdays(-10, 1);
650              
651             $iterator = $date->iterate_businessdays_upto($date2, '<');
652             $iterator = $date->iterate_businessdays_upto($date2, '<=');
653             $iterator = $date->iterate_businessdays_downto($date2, '>');
654             $iterator = $date->iterate_businessdays_downto($date2, '>=');
655             while ($iterator->()) {
656             printf "%d-%02d-%02d\n", $date->get_ymd;
657             }
658              
659             $alignment = $date->get_alignment;
660              
661             # ----- configuration -----
662              
663             @my_holidays = (
664             [6], # Sundays
665             [
666             [11, 22, [3, 2, 1, 0, 6, 5, 4]], # Thanksgiving
667             [12, 25], # December 25
668             [12, 26, undef, [2005, 2010]], # December 26 in 2005-2010
669             [12, 27, undef, sub { $_[1] & 1 }], # December 27 in odd years
670             ]
671             );
672              
673             sub my_make_calendar {
674             my ($date, $year) = @_;
675             my $calendar = $date->get_empty_calendar($year, [SATURDAY, SUNDAY]);
676             my $firstday = $date->new->set_yd($year, 1);
677              
678             # ... calculate holidays of given year, for example ...
679             my $holiday = $date->new->set_ymd($year, 7, 4);
680             my $index = $holiday->get_days_since($firstday);
681             # Sunday -> next Monday, Saturday -> previous Friday
682             if (!$calendar->[$index] && !$calendar->[++$index]) {
683             $index -= 2;
684             }
685             $calendar->[$index] = 0;
686             # ... and so on for all holidays of year $year.
687              
688             return $calendar;
689             }
690              
691             Date::Gregorian::Business->define_configuration(
692             'Acme Ltd.' => \@my_holidays
693             );
694              
695             Date::Gregorian::Business->define_configuration(
696             'Acme Ltd.' => \&my_make_calendar
697             );
698              
699             # set default configuration and create object with defaults
700             Date::Gregorian::Business->configure_business('Acme Ltd.') or die;
701             $date = Date::Gregorian::Business->new;
702              
703             # create object with explicitly specified configuration
704             $date = Date::Gregorian::Business->new('Acme Ltd.') or die;
705              
706             # create object and change configuration later
707             $date = Date::Gregorian::Business->new;
708             $date->configure_business('Acme Ltd.') or die;
709             $date->configure_business(\@my_holidays) or die;
710             $date->configure_business(\&my_make_calendar) or die;
711              
712             # some pre-defined configurations
713             $date->configure_business('us'); # US banking
714             $date->configure_business('de'); # German nation-wide
715              
716             =head1 DESCRIPTION
717              
718             I<Date::Gregorian::Business> is an extension of Date::Gregorian supporting
719             date calculations involving business days.
720              
721             Objects of this class have a notion of whether or not a day is a
722             business day and provide methods to count business days between two
723             dates or find the other end of a date interval, given a start or
724             end date and a number of business days in between. Other methods
725             allow to define business calendars for use with this module.
726              
727             By default, a date interval includes the earlier date and does not
728             include the later date of its two end points, no matter in what order
729             they are given. We call this "morning alignment". However, individual
730             date objects can be either "morning" or "evening" aligned, meaning they
731             represent the situation at the beginning or end of the day in question.
732             Where a date object is the result of a calculation, its alignment can
733             be chosen through an optional method argument.
734              
735             =head2 User methods
736              
737             =over 4
738              
739             =item new
740              
741             I<new>, called as a class method, creates and returns a new date
742             object. The optional parameter can be a configuration or (more
743             typically) the name of a configuration. If omitted, the current
744             default configuration is used. Business calendar configurations
745             are described in detail in an extra section below. In case of bad
746             configurations B<undef> is returned.
747              
748             I<new>, called as an object method, returns a clone of the object.
749             A different configuration for the new object can be specified.
750             Again, in case of bad configurations B<undef> is returned.
751              
752             =item is_businessday
753              
754             I<is_businessday> returns a nonzero number (typically 1) if the
755             date currently represented by the object is a business day, or zero
756             if it falls on a weekend or holiday. Special business calendars
757             may have business days counting less than a whole day in calculations.
758             Objects configured that way may return 0.5 or even another numeric
759             value between 0 and 1 for some dates. In any case I<is_businessday>
760             can be used in boolean context.
761              
762             =item align
763              
764             I<align> sets the alignment of a date. An alignment of 0 means
765             morning alignment, 1 means evening alignment. With morning alignment,
766             the current day is counted in durations extending into the future,
767             and not counted in durations extending from that date into the past.
768             Mnemonic is, in the morning, a day's business lies ahead, whereas
769             in the evening, it lies behind. Night workers please pardon the
770             simplification.
771              
772             =item get_businessdays_since get_businessdays_until
773              
774             There are two methods to count the number of business days between
775             two dates. Their only difference is the sign of the result:
776             I<get_businessdays_since> is positive if the parameter refers to
777             an earlier date than the object and business days lie between them,
778             zero if no business days are counted, and negative otherwise. Note
779             the role of alignments described in the previous paragraph.
780             I<get_businessdays_until> is positive when I<get_businessdays_since>
781             is negative and vice versa. The parameter may be an arbitrary
782             Date::Gregorian object. If it is not a Date::Gregorian::Business
783             object its alignment is taken to be the default (morning).
784              
785             =item set_next_businessday
786              
787             I<set_next_businessday> moves an arbitrary date up or down to the
788             next business day. Its parameter must be one of the four relation
789             operators ">=", ">", "<=" or "<" as a string. ">=" means, the date
790             should not be changed if it is a business day, or changed to the
791             closest business day in the future otherwise. ">" means the date
792             should be changed to the closest business day truly later than the
793             current date. "<=" and "<" likewise work in the other direction.
794             Alignment does not matter and is not changed.
795              
796             =item add_businessdays
797              
798             I<add_businessdays> moves an arbitrary date forward or backwards
799             in time up to a given number of business days. A positive number
800             of days means moving towards the future. The result is always a
801             business day. The alignment will not be changed if the second
802             parameter is omitted, or else set to the second parameter. The
803             result will be rounded to the beginning or end of a business day
804             if necessary, as determined by its alignment.
805              
806             Rounding: If you work with simple calendars and integer numbers,
807             all results will be precise. However, with calendars containing
808             fractions of business days or with non-integer values of day
809             differences, a calculated date may end up somewhere in the middle
810             of a business day rather than at its beginning or end. The final
811             result will stay at that date but move up or down to the desired
812             alignment. In other words, fractional days will be rounded down
813             to morning alignment or up to evening alignment, whichever applies.
814              
815             No ambiguities: Even if a calculated date lies next to a number of
816             non-business days in a way that more than one date would satisfy a
817             desired span of business days, results are always well-defined by
818             the fact that they must be business days. Thus, morning alignment
819             will pull a result to the first business day after weekends and
820             holidays, while evening alignment will pull a result to the last
821             business day before any non-business days. If you add zero business
822             days to some arbitrary date you get the unique date of the properly
823             aligned business day next to it.
824              
825             =item iterate_businessdays_upto iterate_businessdays_downto
826              
827             I<iterate_businessdays_upto> and I<iterate_businessdays_downto>
828             provide iterators over a range of business days. They return a
829             reference to a subroutine that can be called without argument in a
830             while condition to set the given date iteratively to each one of a
831             sequence of dates, while skipping non-business days. The business
832             day closest to the current date is always the first one to be
833             visited (unless the sequence is all empty). The limit parameter
834             determines the end of the sequence, together with the relation
835             parameter: '<' excludes the upper limit from the sequence, '<='
836             includes the upper limit, '>=' includes the lower limit and '>'
837             excludes the lower limit.
838              
839             Each iterator maintains its own state; therefore it is legal to run
840             more than one iterator in parallel or even create new iterators
841             within iterations. Undefining an iterator after use might help to
842             save memory.
843              
844             =item get_alignment
845              
846             I<get_alignment> retrieves the alignment (either 0 for morning or
847             1 for evening).
848              
849             =back
850              
851             =head2 Configuration
852              
853             Version compatibility note: The configuration specifications described
854             here are expected to evolve with further development of this module.
855             In fact, they should ultimately be replaced by easier-to-use
856             configuration objects. We will try to stay downward compatible for
857             some time, however.
858              
859             The business calendar to use can be customized both on an
860             object-by-object basis and by way of general defaults. Business
861             calendars can be stored under a name and later referenced by that
862             name.
863              
864             A business calendar can be defined through a list of holiday
865             definitions or more generally through a code reference, as explained
866             below. A number of such definitions of common interest will be
867             accessible in later editions of this module or some related component.
868              
869             =over 4
870              
871             =item define_configuration
872              
873             I<define_configuration> names and defines a configuration. It can
874             later be referenced by its name. By convention, user-defined names
875             should start with an uppercase letter, while configuration names
876             provided as a part of the distribution will always start with a
877             lowercase letter.
878              
879             =item configure_business
880              
881             I<configure_business>, used as an object method, re-configures that
882             object. It returns the object on success, B<undef> in case of a
883             bad configuration.
884              
885             I<configure_business>, used as a class method, defines the default
886             configuration for new objects created with neither a configuration
887             parameter nor a reference object. It returns the class name on
888             success, B<undef> in case of a bad configuration.
889              
890             The configuration parameter for I<define_configuration>, I<new> and
891             I<configure_business> can be the name of a known configuration, an
892             array reference or a code reference. A configuration name must be
893             known at the time it is used, for it is always immediately replaced
894             by the named configuration.
895              
896             An array reference used as a configuration has to refer to a
897             two-element array like this:
898              
899             $configuration = [\@weekend_days, \@holidays];
900              
901             Here, C<@weekend_days> is a list of the non-business days of every
902             week, given as numerical values as defined in I<Date::Gregorian>.
903             For example:
904              
905             use Date::Gregorian qw(:weekdays);
906             @weekend_days = (SATURDAY, SUNDAY);
907              
908             The list of weekend days may be empty, but must not contain all
909             seven days of the week, which would imply that the whole week has
910             no business days and thus be the reason for endless loops.
911              
912             The second element of a configuration is a list of holiday definitions.
913             Each one of these defines a yearly recurring event like this:
914              
915             $holiday = [$month, $day, $weekday_shift, $valid_years];
916              
917             Here, C<$month> and C<$day> with month ranging from 1 to 12 define
918             an anniversary by date. Alternatively, month may be zero and day
919             a signed integer value defining a date relative to Easter Sunday.
920             For example, C<[0, -2]> would refer to Good Friday (two days before
921             Easter Sunday) while C<[0, 1]> would refer to Easter Monday. The
922             distance from Easter Sunday must be in the range of (roughly)
923             C<-80..250> to make sure the actual date is a day of the same year.
924             Easter-related holidays ending up in different years are silently
925             ignored.
926              
927             If C<$weekday_shift> is omitted or undefined, a holiday occurs on
928             a fixed month and day (or distance from easter), no matter what day
929             of the week it falls on. In order to shift it dependent on the
930             weekday, C<$weekday_shift> must be a reference of a seven-element
931             array of days to add, ordered from Monday to Sunday. Examples:
932              
933             [0, 0, 0, 0, 0, 2, 1] # Saturday and Sunday -> next Monday
934              
935             [0, 6, 5, 4, 3, 2, 1] # any day other than Monday -> next Monday
936              
937             [3, 2, 1, 0, 6, 5, 4] # any non-Thursday -> next Thursday
938              
939             The last two examples above show how holidays can be defined that
940             always fall on the same day of the week. To continue the example,
941             Thanksgiving Day could be defined like this:
942              
943             $thanksgiving = [11, 22, [3, 2, 1, 0, 6, 5, 4]];
944              
945             The fourth element of a holiday definition is also optional and
946             limits the years the definition is valid for. It may be either:
947              
948             =over 4
949              
950             =item *
951              
952             a plain number, defining the single year the definition is valid,
953              
954             =item *
955              
956             a reference of a two-element array, defining the first and
957             the last year of a range of years, where B<undef> means
958             no limit,
959              
960             =item *
961              
962             a reference of a subroutine taking a date object and a year, month
963             and day, returning a boolean for whether the holiday is valid in
964             that year. Month and day are taken directly from the holiday
965             definition (even where the month value is zero for dates relative
966             to easter). The date object is a clone of the original object
967             (though not initialized to a particular date), just for safety.
968             It may be changed while the original object should not be.
969              
970             =back
971              
972             A more general way to specify a complete configuration is a code
973             reference. It must refer to a subroutine that takes a date object
974             and a year (which you can also view as a method with a year parameter)
975             and returns an array reference. The array must have exactly that
976             many elements as there are days in the given year. Each element
977             must be defined and have a numerical value greater or equal to zero.
978             These values will be returned by I<is_businessday> and added together
979             in calculations. The idea is that one call to the subroutine figures
980             out the calendar of a whole year in one go.
981              
982             =item get_empty_calendar
983              
984             I<get_empty_calendar> is a helper method mainly intended for use
985             in such a subroutine. It takes two mandatory parameters, a year
986             and a reference to an array like C<@weekend_days> above, and returns
987             a reference of an array of zeroes and ones representing the weekends
988             and weekly business days of that year suitable to be further modified
989             and finally returned by said subroutine.
990              
991             =back
992              
993             =head1 EXPORTS
994              
995             None.
996              
997             =head1 SEE ALSO
998              
999             L<Date::Gregorian>.
1000              
1001             =head1 AUTHOR
1002              
1003             Martin Becker C<< <becker-cpan-mp (at) cozap.com> >>
1004              
1005             =head1 LICENSE AND COPYRIGHT
1006              
1007             Copyright (c) 1999-2019 by Martin Becker, Blaubeuren.
1008              
1009             This library is free software; you can distribute it and/or modify it
1010             under the terms of the Artistic License 2.0 (see the LICENSE file).
1011              
1012             =head1 DISCLAIMER OF WARRANTY
1013              
1014             This library is distributed in the hope that it will be useful,
1015             but without any warranty; without even the implied warranty of
1016             merchantability or fitness for a particular purpose.
1017              
1018             =cut