File Coverage

blib/lib/Date/Business.pm
Criterion Covered Total %
statement 33 170 19.4
branch 0 102 0.0
condition 0 46 0.0
subroutine 11 30 36.6
pod 0 19 0.0
total 44 367 11.9


line stmt bran cond sub pod time code
1             # $Id: Business.pm,v 1.1 1999/12/28 22:05:38 desimr Exp desimr $
2             #
3             # $Log: Business.pm,v $
4             #
5             # Revision 1.2 1999/11/25 01:15:31 desimr
6             # added support for Holidays
7             #
8             # Revision 1.1 1999/11/23 18:11:55 desimr
9             # Business date package
10             #
11             # (c) 1999 Morgan Stanley Dean Witter and Co.
12             # See LICENSE for terms of distribution.
13             #
14             # Author: Richard DeSimine
15             #
16             package Date::Business;
17              
18 1     1   700 use strict;
  1         2  
  1         36  
19 1     1   1090 use POSIX;
  1         8191  
  1         8  
20 1     1   5570 use Time::Local;
  1         2924  
  1         119  
21 1     1   9 use vars qw($VERSION @ISA @EXPORT);
  1         2  
  1         148  
22            
23             require Exporter;
24             require DynaLoader;
25            
26             @ISA = qw(Exporter DynaLoader);
27              
28             $VERSION = '1.2';
29            
30             #RCS/CVS Version
31             my($RCSVERSION) = do {
32             my @r = (q$Revision: 1.1 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r
33             };
34              
35 1     1   7 use constant DAY => 86_400;
  1         1  
  1         78  
36 1     1   6 use constant WEEK => DAY * 7;
  1         2  
  1         229  
37 1     1   116 use constant E_SUNDAY => DAY * 3; # offset from Epoch Day Of Week
  1         1  
  1         61  
38 1     1   6 use constant THURSDAY => 4; # day of week
  1         2  
  1         39  
39 1     1   5 use constant FRIDAY => 5; # day of week
  1         2  
  1         195  
40 1     1   6 use constant SATURDAY => 6; # day of week
  1         1  
  1         156  
41 1     1   158 use constant SUNDAY => 0; # day of week
  1         2  
  1         4287  
42              
43             # create a new object with the specified date
44             # an offset in business days may be provided
45             sub new($;$$$) {
46 0     0 0   my($class) = shift;
47 0           my(%params) = @_;
48            
49 0           my($date) = $params{DATE}; # string or Date object
50 0           my($offset) = $params{OFFSET}; # business days
51            
52 0           bless my $self = {'val' => 0}, $class;
53 0 0         $self->{FORCE} = $params{FORCE} if (defined($params{FORCE}));
54 0 0         $self->{HOLIDAY} = $params{HOLIDAY} if (ref($params{HOLIDAY}) eq 'CODE');
55            
56             # is the date parameter another Date::Business object?
57 0 0         if (ref($date) eq __PACKAGE__) {
58 0           $self->{val} = $date->{val};
59 0 0 0       $self->{FORCE} ||= $date->{FORCE} if (defined($date->{FORCE}));
60 0 0 0       $self->{HOLIDAY} ||= $date->{HOLIDAY} if (ref($date->{HOLIDAY}) eq 'CODE');
61             } else {
62             # if not a Date::Business object is it a date string?
63 0 0 0       if (defined($date) && length($date) != 0) {
64 0           $self->{'val'} = image2value($date);
65             } else {
66             # else use current localtime
67 0           my($lt) = timegm(localtime());
68 0           $self->{'val'} = $lt - ($lt % DAY);
69             }
70             }
71            
72             # compute offset if specified
73 0 0         if (defined($offset)) {
74 0 0         $self->addb($offset) if ($offset > 0);
75 0 0         $self->subb(-$offset) if ($offset < 0);
76             } else {
77             # if the date was initialized with a weekend or holiday
78             # and the FORCE option is set, force it to the 'next'
79             # or 'prev' business day
80 0 0         if (defined($params{FORCE})) {
81 0 0 0       if ($self->day_of_week == SATURDAY || $self->day_of_week == SUNDAY ||
      0        
      0        
82             (ref($self->{HOLIDAY}) eq 'CODE' &&
83             $self->{HOLIDAY}->($self->image, $self->image))) {
84 0 0         $self->prevb if ($self->{FORCE} eq 'prev');
85 0 0         $self->nextb if ($self->{FORCE} eq 'next');
86             }
87             }
88             }
89 0           return $self;
90             }
91              
92             sub image2value($;$) {
93 0     0 0   my($image) = @_;
94              
95 0           $image =~ m/(....)(..)(..)/;
96 0           return timegm(0, 0, 0, $3, ($2-1), $1 - 1900);
97             }
98              
99             sub value($) {
100 0     0 0   my($self) = @_;
101 0           return $self->{'val'};
102             }
103              
104             sub image($) {
105 0     0 0   my($self) = @_;
106 0           return POSIX::strftime("%Y%m%d", gmtime($self->{'val'}));
107             }
108              
109             sub next(;$) {
110 0     0 0   my($self, $n) = @_;
111 0 0         $n = 1 if (!defined($n));
112 0           $self->{'val'} += DAY * $n;
113             }
114              
115             sub prev(;$) {
116 0     0 0   my($self, $n) = @_;
117 0 0         $n = 1 if (!defined($n));
118 0           $self->{'val'} -= (DAY * $n);
119             }
120              
121             sub datecmp($$) {
122 0     0 0   my($self, $other) = @_;
123              
124 0           return $self->{'val'} <=> $other->{'val'};
125             }
126              
127             sub eq($$) {
128 0     0 0   my($self, $other) = @_;
129              
130 0           return $self->{'val'} <=> $other->{'val'};
131             }
132              
133             sub gt($$) {
134 0     0 0   my($self, $other) = @_;
135 0           return $self->{'val'} > $other->{'val'};
136             }
137              
138             sub lt($$) {
139 0     0 0   my($self, $other) = @_;
140 0           return $self->{'val'} < $other->{'val'};
141             }
142              
143             sub add($$) {
144 0     0 0   my($self, $inc) = @_;
145 0           $self->{'val'} += $inc * DAY;
146             }
147              
148             sub sub($$) {
149 0     0 0   my($self, $inc) = @_;
150 0           $self->{'val'} -= $inc * DAY;
151             }
152              
153             sub diff($$) {
154 0     0 0   my($self, $other) = @_;
155              
156 0           return int(($self->{'val'} - $other->{'val'}) / DAY);
157             }
158              
159             sub day_of_week($$) {
160 0     0 0   my($self) = @_;
161 0           return (gmtime($self->{'val'}))[6];
162             }
163              
164              
165             # business date functions
166             sub nextb() {
167 0     0 0   my($self) = @_;
168 0           $self->addb(1);
169             }
170              
171             sub prevb() {
172 0     0 0   my($self) = @_;
173 0           $self->subb(1);
174             }
175              
176             # takes a reference to $self and a reference
177             # to an object of type Date::Business and returns
178             # the difference in business days
179             sub diffb($$;$$) {
180 0     0 0   my($self, $other, $force_self, $force_other) = @_;
181 0 0         return -1 if (!defined($other));
182 0           my($days, $o_val, $sval, $tmp, $dow);
183 0           my($sign) = 1;
184            
185 0   0       $force_self ||= 'prev';
186 0   0       $force_other ||= 'prev';
187              
188 0           $sval = $self->{val};
189 0           while ($force_self eq 'prev') {
190 0           $tmp = $sval;
191 0           $dow = (gmtime($sval))[6];
192 0 0         $sval -= 2 * DAY if ($dow == SUNDAY);
193 0 0         $sval -= 1 * DAY if ($dow == SATURDAY);
194 0 0 0       $sval -= 1 * DAY if (ref($self->{HOLIDAY}) eq 'CODE' &&
195             $self->{HOLIDAY}->(POSIX::strftime("%Y%m%d", gmtime($sval)),
196             POSIX::strftime("%Y%m%d", gmtime($sval))));
197 0 0         last if ($sval == $tmp);
198             }
199 0           while ($force_self eq 'next') {
200 0           $tmp = $sval;
201 0           $dow = (gmtime($sval))[6];
202 0 0         $sval += 1 * DAY if ($dow == SUNDAY);
203 0 0         $sval += 2 * DAY if ($dow == SATURDAY);
204 0 0 0       $sval += 1 * DAY if (ref($self->{HOLIDAY}) eq 'CODE' &&
205             $self->{HOLIDAY}->(POSIX::strftime("%Y%m%d", gmtime($sval)),
206             POSIX::strftime("%Y%m%d", gmtime($sval))));
207 0 0         last if ($sval == $tmp);
208             }
209            
210 0           $o_val = $other->{val};
211 0           while ($force_other eq 'prev') {
212 0           $tmp = $o_val;
213 0           $dow = (gmtime($o_val))[6];
214 0 0         $o_val -= 2 * DAY if ($dow == SUNDAY);
215 0 0         $o_val -= 1 * DAY if ($dow == SATURDAY);
216 0 0 0       $o_val -= 1 * DAY if (ref($other->{HOLIDAY}) eq 'CODE' &&
217             $other->{HOLIDAY}->(POSIX::strftime("%Y%m%d", gmtime($o_val)),
218             POSIX::strftime("%Y%m%d", gmtime($o_val))));
219 0 0         last if ($o_val == $tmp);
220             }
221 0           while ($force_other eq 'next') {
222 0           $tmp = $o_val;
223 0           $dow = (gmtime($o_val))[6];
224 0 0         $o_val += 1 * DAY if ($dow == SUNDAY);
225 0 0         $o_val += 2 * DAY if ($dow == SATURDAY);
226 0 0 0       $o_val += 1 * DAY if (ref($other->{HOLIDAY}) eq 'CODE' &&
227             $other->{HOLIDAY}->(POSIX::strftime("%Y%m%d", gmtime($o_val)),
228             POSIX::strftime("%Y%m%d", gmtime($o_val))));
229 0 0         last if ($o_val == $tmp);
230             }
231            
232 0 0         if ($sval < $o_val){
233 0           $sign = -1;
234             } else {
235 0           $tmp = $sval;
236 0           $sval = $o_val;
237 0           $o_val = $tmp;
238             }
239            
240 0           my($weeks) = int((($o_val - $sval)/WEEK)) * 5;
241 0           $days = ((($o_val + E_SUNDAY) / DAY) % 7) - ((($sval + E_SUNDAY)/ DAY) % 7);
242 0 0         $days += 5 if ($days < 0);
243              
244 0 0         if (ref($other->{HOLIDAY}) eq 'CODE') {
245 0           $days -= $self->{HOLIDAY}->(POSIX::strftime("%Y%m%d", gmtime($sval)),
246             POSIX::strftime("%Y%m%d", gmtime($o_val)));
247             }
248 0           return $sign * ($weeks + $days);
249             }
250              
251             # adds n business days
252             sub addb($$) {
253 0     0 0   my($self, $inc) = @_;
254              
255 0 0 0       return if ($inc == 0 || $inc < 0 && $self->subb(-$inc));
      0        
256              
257 0           my($start) = $self->{'val'};
258 0           my($weeks) = int($inc/5) * 7;
259 0           my($dow) = (($self->{'val'} + E_SUNDAY) / DAY) % 7;
260 0           my($days) = $inc % 5;
261 0 0         if ($dow > THURSDAY) {
262 0 0         $self->{'val'} -= 1 * DAY if ($dow == FRIDAY);
263 0 0         $self->{'val'} -= 2 * DAY if ($dow == SATURDAY);
264 0 0         $dow-- if ($days == 0);
265             }
266 0 0         $days += 2 if ($days + $dow > THURSDAY);
267 0           $self->{'val'} += ($weeks + $days) * DAY;
268              
269 0 0         if (ref($self->{HOLIDAY}) eq 'CODE') {
270 0           my($start_txt) = POSIX::strftime("%Y%m%d", gmtime($start + DAY));
271 0           my($numHolidays) = $self->{HOLIDAY}->($start_txt, $self->image);
272 0 0         $self->addb($numHolidays) if ($numHolidays);
273             }
274 0           return 1;
275             }
276              
277             # subs n business days
278             sub subb($$) {
279 0     0 0   my($self, $dec) = @_;
280              
281 0 0 0       return if ($dec == 0 || $dec < 0 && $self->addb(-$dec));
      0        
282              
283 0           my($start) = $self->{'val'};
284 0           my($weeks) = int($dec/5) * 7;
285 0           my($dow) = (($self->{'val'} + E_SUNDAY) / DAY) % 7;
286 0           my($days) = $dec % 5;
287 0 0         if ($dow > 4) {
288 0 0         $self->{'val'} += 2 * DAY if ($dow == FRIDAY);
289 0 0         $self->{'val'} += 1 * DAY if ($dow == SATURDAY);
290 0 0         $days += 2 if ($days);
291             } else {
292 0 0         $days += 2 if ($days > $dow);
293             }
294 0           $self->{'val'} -= ($weeks + $days) * DAY;
295              
296 0 0         if (ref($self->{HOLIDAY}) eq 'CODE') {
297 0           my($end_txt) = POSIX::strftime("%Y%m%d", gmtime($start - DAY));
298 0           my($numHolidays) = $self->{HOLIDAY}->($self->image, $end_txt);
299 0 0         $self->subb($numHolidays) if ($numHolidays);
300             }
301 0           return 1;
302             }
303             1;
304             __END__
305              
306             =head1 NAME
307              
308             Date::Business - fast calendar and business date calculations
309              
310             =head1 SYNOPSIS
311              
312             All arguments to the Date::Business constructor are optional.
313              
314             # simplest case, default is today's date (localtime)
315             $d = new Date::Business();
316              
317             # initialize with date string,
318             # offset in business days is optional
319             $d = new Date::Business(DATE => '19991124' [, OFFSET => <integer>]);
320              
321             # initialize with another Date::Business object
322             # offset in business days is optional
323             $x = new Date::Business(DATE => $d [, OFFSET => <integer>]);
324              
325             # initialize with holiday function (see Holidays, below)
326             $d = new Date::Business(HOLIDAY => \&holiday);
327              
328             # force weekends/holidays to the previous or next business day
329             $d = new Date::Business(FORCE => 'prev'); # Friday (usually)
330             $d = new Date::Business(FORCE => 'next'); # Monday (usually)
331              
332             $d->image(); # returns YYYYMMDD string
333             $d->value(); # returns Unix time as integer
334              
335             $d->day_of_week(); # 0 = Sunday
336              
337             $d->datecmp($x); # are two dates equal?
338             $d->eq($x); # synonym for datecmp
339             $d->lt($x); # less than
340             $d->gt($x); # greater than
341              
342             Calendar date functions
343             $d->next(); # next calendar day
344             $d->prev(); # previous calendar day
345             $d->add(<offset>); # adds n calendar days
346             $d->sub(<offset>); # subtracts n calendar days
347             $d->diff($x); # difference between two dates
348            
349             Business date functions
350             $d->nextb(); # next business day
351             $d->prevb(); # previous business day
352             $d->addb(<offset>); # adds n business days
353             $d->subb(<offset>); # subtracts n business days
354             $d->diffb($x); # difference between two business dates
355             $d->diffb($x, 'next'); # treats $d weekend/holiday as next business date
356             $d->diffb($x, 'next', 'next'); # treats $x weekend/holiday as above
357              
358              
359             =head1 DESCRIPTION
360              
361             Date::Business provides the functionality to perform simple date
362             manipulations quickly. Support for calendar date and
363             business date math is provided.
364              
365             Business dates are weekdays only. Adding 1 to a weekend returns
366             Monday, subtracting 1 returns Friday.
367              
368             The difference in business days between Friday and the following
369             Monday (using the diffb function) is one business day. The number
370             of business days between Friday and the following Monday (using the
371             betweenb function) is zero.
372              
373             =head1 EXAMPLE
374              
375             Date::Business works very well for iterating over dates,
376             and determining start and end dates of arbitray n business day
377             periods (e.g. consider how to perform a computation for
378             a series of business days starting from an arbitrary day).
379            
380             $end = new Date::Business(); # today
381             # 10 business days ago
382             $start = new Date::Business(DATE => $end, OFFSET => -10);
383              
384             while (!$start->gt($end)) {
385             compute_something($start);
386             $start->nextb();
387             }
388              
389             =head1 HOLIDAYS
390              
391             Optionally, a reference to a function that counts the number of
392             holidays in a given date range can be passed. Business date addition,
393             subtraction, and difference functions will consider holidays.
394              
395             Sample holiday function:
396              
397             # MUST BE NON-WEEKEND HOLIDAYS !!!
398             sub holiday($$) {
399             my($start, $end) = @_;
400            
401             my($numHolidays) = 0;
402             my($holiday, @holidays);
403            
404             push @holidays, '19981225'; # Christmas
405             push @holidays, '19990101'; $ New Year's
406            
407             foreach $holiday (@holidays) {
408             $numHolidays++ if ($start le $holiday && $end ge $holiday);
409             }
410             return $numHolidays;
411             }
412              
413             Example using the holiday function:
414              
415             # 10 business days after 21 DEC 1998, where
416             # 25 DEC 1998 and 01 JAN 1999 are holidays
417             #
418             $d = new Date::Business(DATE => '19981221',
419             OFFSET => 10,
420             HOLIDAY => \&holiday);
421              
422             print $d->image."\n"; # prints 19990106
423              
424             =head1 The diffb() function explained
425              
426             The difference between two business days is relatively straightforward
427             when the operands are business days. The difference (in business days)
428             between two days when one or both of those days is a weekend or
429             holiday is ambiguous. The 'next' and 'prev' parameters are used to
430             resolve the ambiguity.
431              
432             The first parameter to the diffb function is the other date. The
433             second parameter indicates that 'self' is to be treated as the
434             previous or next business date if it is not a business date. The third
435             parameter is similar to the second parameter but applies to the
436             'other' date. The default behavior is treat both dates as if the
437             'prev' option was set.
438              
439             For example:
440              
441             $d = new Date::Business(DATE => '19991225'); # saturday
442             $x = new Date::Business(DATE => '19991225'); # saturday
443             print $d->image; # prints 19991225
444             print $d->diffb($x); # prints 0
445             print $d->diffb($x, 'prev', 'next'); # prints -1
446             print $d->diffb($x, 'next', 'prev'); # prints 1
447             print $d->diffb($x, 'next', 'next'); # prints 0
448              
449             =head1 CAVEATS
450              
451             Business dates may be initialized with values in the range of
452             '19700101' through '20380119'. The range of valid results are
453             '19011213' through '20380119'.
454              
455             Computations on dates that exceed the maximum value will wrap
456             around. (i.e. the day after '20380119' is '19011214'). Computations
457             that exceed the minimum value will result in the minimum
458             value. (i.e. the day before '19011213' is '19011213')
459              
460             =cut