File Coverage

blib/lib/Date/Gregorian/Business.pm
Criterion Covered Total %
statement 252 252 100.0
branch 84 86 97.6
condition 54 58 93.1
subroutine 38 38 100.0
pod 13 14 92.8
total 441 448 98.4


line stmt bran cond sub pod time code
1             # Copyright (c) 2005-2007 Martin Becker. All rights reserved.
2             # This package is free software; you can redistribute it and/or modify it
3             # under the same terms as Perl itself.
4             #
5             # $Id: Business.pm,v 1.5 2007/06/18 06:11:56 martin Stab $
6              
7             package Date::Gregorian::Business;
8              
9 2     2   4347 use strict;
  2         6  
  2         88  
10 2     2   1048 use integer;
  2         12  
  2         12  
11 2     2   781 use Date::Gregorian;
  2         6  
  2         146  
12 2     2   13 use base qw(Date::Gregorian);
  2         4  
  2         375  
13 2     2   13 use vars qw($VERSION);
  2         4  
  2         155  
14              
15             # ----- object definition -----
16              
17             # ............. index .............. # .......... value ..........
18 2     2   10 use constant F_OFFSET => Date::Gregorian::NFIELDS;
  2         4  
  2         176  
19 2     2   12 use constant F_ALIGNMENT => F_OFFSET+0; # 0 = morning, 1 = evening
  2         4  
  2         157  
20 2     2   11 use constant F_MAKE_CAL => F_OFFSET+1; # sub (date, year) => [calendar]
  2         4  
  2         121  
21 2     2   11 use constant F_YEAR => F_OFFSET+2; # currently initialized year
  2         4  
  2         161  
22 2     2   11 use constant F_CALENDAR => F_OFFSET+3; # list of: 1 = biz, 0 = holiday
  2         4  
  2         115  
23 2     2   11 use constant NFIELDS => F_OFFSET+4;
  2         3  
  2         3894  
24              
25             # ----- predefined variables -----
26              
27             $VERSION = 0.04;
28              
29             # elements of default biz calendars
30             my $skip_weekend = [ 0, 0, 0, 0, 0, 2, 1]; # Sat, Sun -> Mon
31             my $avoid_weekend = [ 0, 0, 0, 0, 0, -1, 1]; # Sat -> Fri, Sun -> Mon
32             my $next_monday = [ 0, 6, 5, 4, 3, 2, 1]; # set_weekday(Mon, ">=")
33             my $prev_monday = [-7, -1, -2, -3, -4, -5, -6]; # set_weekday(Mon, "<")
34             my $next_wednesday = [ 2, 1, 0, 6, 5, 4, 3]; # set_weekday(Wed, ">=")
35             my $next_thursday = [ 3, 2, 1, 0, 6, 5, 4]; # set_weekday(Thu, ">=")
36             my $saturday_sunday = [ 5, 6];
37              
38             # some biz calendars known by default
39             my %samples = (
40             'us' => [
41             $saturday_sunday,
42             [
43             [ 1, 1, $skip_weekend], # New Year's day
44             [ 1, 15, $next_monday], # Martin Luther King
45             [ 2, 15, $next_monday], # President's day
46             [ 6, 1, $prev_monday], # Memorial day
47             [ 7, 4, $avoid_weekend], # Independence day
48             [ 9, 1, $next_monday], # Labor day
49             [10, 8, $next_monday], # Columbus day
50             [11, 11, $avoid_weekend], # Veteran's day
51             [11, 22, $next_thursday], # Thanksgiving day
52             [12, 25, $skip_weekend], # Christmas day
53             ],
54             ],
55             'de' => [
56             $saturday_sunday,
57             [
58             [ 1, 1], # New Year's day
59             [ 0, -2], # Good Friday
60             [ 0, 1], # Easter Monday
61             [ 5, 1], # Labour day
62             [ 0, 50], # Pentecost Monday
63             [ 6, 17, undef, [1954, 1989]], # German Unity
64             [10, 3, undef, [1990, undef]], # German Unity
65             [11, 16, $next_wednesday, [undef, 1994]], # Penitence day
66             [12, 25], # Christmas day
67             [12, 26], # 2nd day of Christmas
68             ],
69             ],
70             );
71             $samples{'de_BW'} = [
72             $saturday_sunday,
73             [
74             @{$samples{'de'}->[1]},
75             [ 1, 6], # Epiphany
76             [ 0, 39], # Ascension day
77             [ 0, 60], # Corpus Christi
78             [11, 1], # All Saints day
79             ]
80             ];
81             $samples{'de_BY'} = [
82             $saturday_sunday,
83             [
84             @{$samples{'de_BW'}->[1]},
85             [ 8, 15], # Assumption day
86             ]
87             ];
88             $samples{'de_BW2'} = _more_xmas(@{$samples{'de_BW'}});
89             $samples{'de_BY2'} = _more_xmas(@{$samples{'de_BY'}});
90              
91             my $default_configuration = 'us';
92              
93             # ----- private functions and methods -----
94              
95             # check whether a given year is in a range or general selection
96             sub _select_year {
97 222     222   260 my ($self, $day, $year) = @_;
98 222         268 my $selection = $day->[3];
99 222 100       365 if (!ref $selection) {
100 39         172 return $year == $selection;
101             }
102 183 100       377 if ('CODE' eq ref $selection) {
103 39         45 return $selection->($self, $year, @{$day}[0, 1]);
  39         531  
104             }
105             return
106 144   66     1163 (!defined($selection->[0]) || $selection->[0] <= $year) &&
107             (!defined($selection->[1]) || $year <= $selection->[1]);
108             }
109              
110             # make_cal factory, generating a calendar generator enclosing a configuration
111             sub _make_make_cal {
112 26     26   74 my ($weekly, $yearly) = @_;
113              
114             return sub {
115 167     167   181 my ($date, $year) = @_;
116 167         411 my $firstday = $date->new->set_yd($year, 1, 1);
117 167         457 my $first_wd = $firstday->get_weekday;
118 167   66     509 my $someday = @$yearly && $firstday->new;
119 167         179 my $easter = undef;
120 167         163 my $index;
121 167         352 my $calendar = $firstday->get_empty_calendar($year, $weekly);
122 167         277 foreach my $day (@$yearly) {
123 1508 100 100     3631 if (!defined($day->[3]) || _select_year($someday, $day, $year)) {
124 1381 100       2509 if ($day->[0]) {
125 1308         7040 $index =
126 1308         1409 $someday->set_ymd($year, @{$day}[0, 1])
127             ->get_days_since($firstday);
128 1308 100       4606 $index += $day->[2]->[$someday->get_weekday] if $day->[2];
129             }
130             else {
131 73 100       154 if (!defined $easter) {
132 17         57 $easter =
133             $someday->set_easter($year)
134             ->get_days_since($firstday);
135             }
136 73         83 $index = $easter + $day->[1];
137 73 100       141 $index += $day->[2]->[(496 + $day->[1]) % 7] if $day->[2];
138             }
139 1381 100 100     5916 $calendar->[$index] = 0 if 0 <= $index && $index < @$calendar;
140             }
141             }
142 167         580 return $calendar;
143 26         222 };
144             }
145              
146             # experimental feature: half business days on Dec 24 and 31, if not weekend
147             sub _more_xmas {
148 4     4   84 my $make_cal = _make_make_cal(@_);
149             return sub {
150 4     4   14 my $calendar = $make_cal->(@_);
151 4 100 66     20 if (8 <= @$calendar && $calendar->[-1]) {
152 2         3 @{$calendar}[-8, -1] = (0.5, 0.5);
  2         5  
153             }
154 4         27 return $calendar;
155             }
156 4         13 }
157              
158             # fetch biz calendar for given year, initializing it if necessary
159             sub _calendar {
160 5561     5561   6116 my ($self, $year) = @_;
161              
162 5561 100 100     21830 if (!defined($self->[F_YEAR]) || $year != $self->[F_YEAR]) {
163 183         248 $self->[F_YEAR] = $year;
164 183         382 $self->[F_CALENDAR] = $self->[F_MAKE_CAL]->($self, $year);
165             }
166 5561         14364 return $self->[F_CALENDAR];
167             }
168              
169             # ----- public methods -----
170              
171             sub get_empty_calendar {
172 185     185 1 1969 my ($date, $year, $weekly_nonbiz) = @_;
173              
174 185         321 my $firstday = $date->new->set_yd($year, 1);
175 185         498 my $days = $firstday->get_days_in_year($year);
176 185         422 my $first_wd = $firstday->get_weekday;
177              
178 185         478 my @week = (1) x 7;
179 185         421 foreach my $day (@$weekly_nonbiz) {
180 287         521 $week[$day] = 0;
181             }
182 185 100       1137 @week = @week[$first_wd .. 6, 0 .. $first_wd-1] if $first_wd;
183              
184 185         4876 my @calendar = ((@week) x ($days / 7), @week[0 .. ($days % 7)-1]);
185 185         720 return \@calendar;
186             }
187              
188             sub define_configuration {
189 8     8 1 2133 my ($class, $name, $configuration) = @_;
190 8 50       24 my $type = defined($configuration)? ref($configuration): '!';
191              
192 8 100 100     33 if (!$type) {
    100          
193 5 100       23 return undef if !exists $samples{$configuration};
194 3         8 $configuration = $samples{$configuration};
195             }
196             elsif ('ARRAY' ne $type && 'CODE' ne $type) {
197 1         3 return undef;
198             }
199 5         16 $samples{$name} = $configuration;
200 5         16 return $class;
201             }
202              
203             sub configure_business {
204 34     34 1 1268 my ($self, $configuration) = @_;
205 34 50       113 my $type = defined($configuration)? ref($configuration): '!';
206              
207 34 100       73 if (!$type) {
208 21 100       76 return undef if !exists $samples{$configuration};
209 20         38 $configuration = $samples{$configuration};
210 20         39 $type = ref $configuration;
211             }
212 33 100       73 if (ref $self) {
213             # instance method: configure this object
214 30 100       85 if ('CODE' eq $type) {
    100          
215 7         16 $self->[F_MAKE_CAL] = $configuration;
216             }
217             elsif ('ARRAY' eq $type) {
218 22         63 $self->[F_MAKE_CAL] = _make_make_cal(@$configuration);
219             }
220             else {
221 1         5 return undef;
222             }
223 29         165 $self->[F_YEAR] = $self->[F_CALENDAR] = undef;
224             }
225             else {
226             # class method: configure default
227 3 100 100     22 if ('ARRAY' ne $type && 'CODE' ne $type) {
228 1         3 return undef;
229             }
230 2         6 $default_configuration = $configuration;
231             }
232              
233 31         197 return $self;
234             }
235              
236             sub new {
237 708     708 1 8914 my ($class_or_object, $configuration) = @_;
238 708         1890 my $self = $class_or_object->SUPER::new;
239              
240 708 100       1395 if (!ref $class_or_object) {
241 15         53 $self->[F_ALIGNMENT] = 0;
242             }
243 708 100       1945 if (defined $configuration) {
    100          
244 9         33 return $self->configure_business($configuration);
245             }
246             elsif (!ref $class_or_object) {
247 6         17 return $self->configure_business($default_configuration);
248             }
249 693         2051 return $self;
250             }
251              
252             sub align {
253 96     96 1 165 my ($self, $alignment) = @_;
254 96 100       225 $self->[F_ALIGNMENT] = $alignment? 1: 0;
255 96         295 return $self;
256             }
257              
258             sub get_alignment {
259 195     195 1 787 my $self = $_[0];
260 195         420 return $self->[F_ALIGNMENT];
261             }
262              
263             # tweak super class to provide default alignment
264             sub Date::Gregorian::get_alignment {
265 32     32 0 60 return 0;
266             }
267              
268             sub is_businessday {
269 5263     5263 1 16556 my ($self) = @_;
270 5263         11028 my ($year, $day) = $self->get_yd;
271              
272 5263         9777 return $self->_calendar($year)->[$day-1];
273             }
274              
275             # count business days, proceeding into the future
276             # $days gives the interval measured in real days (positive)
277             # alignment tells where to start: 0 = at current day, 1 = the day after
278             # 0 <= result <= $days
279             sub _count_businessdays_up {
280 63     63   95 my ($self, $days) = @_;
281 63         174 my ($year, $day) = $self->get_yd;
282 63         379 my $calendar = $self->_calendar($year);
283 63         88 my $result = 0;
284              
285 63 100       194 --$day if !$self->[F_ALIGNMENT];
286 63         221 while (0 < $days) {
287 3199         5466 while (@$calendar <= $day) {
288 6         12 $calendar = $self->_calendar(++$year);
289 6         40 $day = 0;
290             }
291 3199         2675 do {
292 2     2   13 no integer;
  2         3  
  2         12  
293 3199         3133 $result += $calendar->[$day];
294             };
295 3199         2474 ++$day;
296 3199         8066 --$days;
297             }
298 63         585 return $result;
299             }
300              
301             # count business days, proceeding into the past
302             # $days gives the interval measured in real days (positive)
303             # alignment tells where to start: 1 = at current day, 0 = the day before
304             # 0 <= result <= $days
305             sub _count_businessdays_down {
306 56     56   95 my ($self, $days) = @_;
307 56         162 my ($year, $day) = $self->get_yd;
308 56         136 my $calendar = $self->_calendar($year);
309 56         79 my $result = 0;
310              
311 56 100       117 --$day if !$self->[F_ALIGNMENT];
312 56         120 while (0 < $days) {
313 1081         1004 --$day;
314 1081         959 --$days;
315 1081         2162 while ($day < 0) {
316 8         16 $calendar = $self->_calendar(--$year);
317 8         64 $day = $#$calendar;
318             }
319 1081         1087 do {
320 2     2   335 no integer;
  2         3  
  2         9  
321 1081         2155 $result += $calendar->[$day];
322             };
323             }
324 56         451 return $result;
325             }
326              
327             # Alignments and results Now:0 Now:1 Now:0 Now:1
328             # b--(H)--b---b---b--(H)--b---b Then:0 Then:1 Then:1 Then:0
329             # Then Now 3 3 3 3
330             # Then Now 3 2 2 3
331             # Then Now 3 4 3 4
332             # Then Now 3 3 2 4
333             # b--(H)--b---b---b--(H)--b---b
334             # Now Then -3 -3 -3 -3
335             # Now Then -3 -4 -4 -3
336             # Now Then -3 -2 -3 -2
337             # Now Then -3 -3 -4 -2
338             # b--(H)--b---b---b--(H)--b---b
339              
340             sub get_businessdays_since {
341 58     58 1 243 my ($self, $then) = @_;
342 58         169 my $delta =
343             $self->get_days_since($then) +
344             $self->[F_ALIGNMENT] - $then->get_alignment;
345 58 100       165 if ($delta > 0) {
346 29         65 return $self->_count_businessdays_down($delta);
347             }
348 29 100       66 if ($delta < 0) {
349 27         76 return -$self->_count_businessdays_up(-$delta);
350             }
351 2         10 return 0;
352             }
353              
354             sub get_businessdays_until {
355 65     65 1 444 my ($self, $then) = @_;
356 65         311 my $delta =
357             $self->get_days_since($then) +
358             $self->[F_ALIGNMENT] - $then->get_alignment;
359 65 100       951 if ($delta > 0) {
360 27         69 return -$self->_count_businessdays_down($delta);
361             }
362 38 100       108 if ($delta < 0) {
363 36         99 return $self->_count_businessdays_up(-$delta);
364             }
365 2         8 return 0;
366             }
367              
368             sub set_next_businessday {
369 40     40 1 57 my ($self, $relation) = @_;
370 40         99 my ($year, $day) = $self->get_yd;
371 40         81 my $calendar = $self->_calendar($year);
372              
373 40         47 --$day;
374 40 100 100     186 return $self if '<' ne $relation && '>' ne $relation && $calendar->[$day];
      100        
375 32 100 100     95 if ('<' eq $relation || '<=' eq $relation) {
376 16         18 do {
377 34         26 --$day;
378 34         72 while ($day < 0) {
379 14         27 $calendar = $self->_calendar(--$year);
380 14         123 $day = $#$calendar;
381             }
382             }
383             while (!$calendar->[$day]);
384             }
385             else {
386 16         15 do {
387 34         33 ++$day;
388 34         100 while (@$calendar <= $day) {
389 2         6 $calendar = $self->_calendar(++$year);
390 2         19 $day = 0;
391             }
392             }
393             while (!$calendar->[$day]);
394             }
395 32         89 return $self->set_yd($year, $day+1);
396             }
397              
398             sub iterate_businessdays_upto {
399 22     22 1 77 my ($self, $limit, $rel) = @_;
400 22         67 my $days = ($rel eq '<=') - $self->get_days_since($limit);
401 22         37 my ($year, $day, $calendar);
402 22 100       47 if (0 < $days) {
403 18         44 ($year, $day) = $self->get_yd;
404 18         22 --$day;
405 18         34 $calendar = $self->_calendar($year);
406             }
407             return sub {
408 70     70   422 while (0 < $days) {
409 89         174 while (@$calendar <= $day) {
410 2         6 $calendar = $self->_calendar(++$year);
411 2         21 $day = 0;
412             }
413 89         86 --$days;
414 89 100       197 if ($calendar->[$day++]) {
415 48         125 return $self->set_yd($year, $day);
416             }
417             }
418 22         39 return undef;
419 22         120 };
420             }
421              
422             sub iterate_businessdays_downto {
423 10     10 1 47 my ($self, $limit, $rel) = @_;
424 10         30 my $days = $self->get_days_since($limit) + ($rel ne '>');
425 10         12 my ($year, $day, $calendar);
426 10 100       23 if (0 < $days) {
427 6         16 ($year, $day) = $self->get_yd;
428 6         206 --$day;
429 6         17 $calendar = $self->_calendar($year);
430             }
431             return sub {
432 34     34   257 while (0 < $days) {
433 51         97 while ($day < 0) {
434 1         5 $calendar = $self->_calendar(--$year);
435 1         4 $day = $#$calendar;
436             }
437 51         49 --$days;
438 51 100       136 if ($calendar->[$day--]) {
439 24         70 return $self->set_yd($year, $day+2);
440             }
441             }
442 10         20 return undef;
443 10         66 };
444             }
445              
446             # -b----H----b----b----H----b-
447             # ^ ^ ^ ^
448             # 0 0 1 1 2 2
449              
450             sub add_businessdays {
451 2     2   1470 no integer;
  2         11  
  2         9  
452 72     72 1 102 my ($self, $days, $new_alignment) = @_;
453 72         189 my ($year, $day) = $self->get_yd;
454 72         83 -- $day;
455 72         165 my $calendar = $self->_calendar($year);
456 72         105 my $alignment = $self->[F_ALIGNMENT];
457              
458             # handle alignment change
459 72 100 100     297 if (defined($new_alignment) && ($alignment xor $new_alignment)) {
      100        
460 20 100       40 if ($new_alignment) {
461 10         17 $alignment = $self->[F_ALIGNMENT] = 1;
462 10         19 $days -= $calendar->[$day];
463             }
464             else {
465 10         14 $alignment = $self->[F_ALIGNMENT] = 0;
466 10         17 $days += $calendar->[$day];
467             }
468             }
469              
470 72 100 100     313 if (0 < $days || !$days && !$alignment) {
      100        
471             # move forward in time
472 42 100       86 $days -= $calendar->[$day] if !$alignment;
473 42   100     145 while (0 < $days || !$days && !$alignment) {
      100        
474 131         132 ++$day;
475 131         239 while (@$calendar <= $day) {
476 3         7 $calendar = $self->_calendar(++$year);
477 3         27 $day = 0;
478             }
479 131         499 $days -= $calendar->[$day];
480             }
481             }
482             else {
483             # move backwards in time
484 30 100       64 $days += $calendar->[$day] if $alignment;
485 30   100     106 while ($days < 0 || !$days && $alignment) {
      66        
486 107         100 --$day;
487 107         212 while ($day < 0) {
488 7         16 $calendar = $self->_calendar(--$year);
489 7         56 $day = $#$calendar;
490             }
491 107         320 $days += $calendar->[$day];
492             }
493             }
494              
495 72         229 return $self->set_yd($year, $day+1);
496             }
497              
498             1;
499              
500             __END__