NAME

Date::Gregorian::Business - business days extension for Date::Gregorian

SYNOPSIS

use Date::Gregorian::Business;
use Date::Gregorian qw(:weekdays);

$date = Date::Gregorian::Business->new('us');

if ($date->set_today->is_businessday) {
  print "Busy today.\n";
}

$date2 = $date->new->set_ymd(2005, 3, 14);

$date2->align(0);                          # morning
$date->align(1);                           # evening

$delta = $date->get_businessdays_since($date2);
$delta = -$date->get_businessdays_until($date2);

$date->set_next_businessday('>=');
$date->add_businessdays(25);
$date->add_businessdays(-10, 0);
$date->add_businessdays(-10, 1);

$iterator = $date->iterate_businessdays_upto($date2, '<');
$iterator = $date->iterate_businessdays_upto($date2, '<=');
$iterator = $date->iterate_businessdays_downto($date2, '>');
$iterator = $date->iterate_businessdays_downto($date2, '>=');
while ($iterator->()) {
  printf "%d-%02d-%02d\n", $date->get_ymd;
}

$alignment = $date->get_alignment;

# ----- configuration -----

@my_holidays = (
    [6],                                   # Sundays
    [
      [11, 22, [3, 2, 1, 0, 6, 5, 4]],     # Thanksgiving
      [12, 25],                            # December 25
      [12, 26, undef, [2005, 2010]],       # December 26 in 2005-2010
      [12, 27, undef, sub { $_[1] & 1 }],  # December 27 in odd years
    ]
);

sub my_make_calendar {
  my ($date, $year) = @_;
  my $calendar = $date->get_empty_calendar($year, [SATURDAY, SUNDAY]);
  my $firstday = $date->new->set_yd($year, 1);

  # ... calculate holidays of given year, for example ...
  my $holiday = $date->new->set_ymd($year, 7, 4);
  my $index = $holiday->get_days_since($firstday);
  # Sunday -> next Monday, Saturday -> previous Friday
  if (!$calendar->[$index] && !$calendar->[++$index]) {
      $index -= 2;
  }
  $calendar->[$index] = 0;
  # ... and so on for all holidays of year $year.

  return $calendar;
}

Date::Gregorian::Business->define_configuration(
  'Acme Ltd.' => \@my_holidays
);

Date::Gregorian::Business->define_configuration(
  'Acme Ltd.' => \&my_make_calendar
);

# set default configuration and create object with defaults
Date::Gregorian::Business->configure_business('Acme Ltd.') or die;
$date = Date::Gregorian::Business->new;

# create object with explicitly specified configuration
$date = Date::Gregorian::Business->new('Acme Ltd.') or die;

# create object and change configuration later
$date = Date::Gregorian::Business->new;
$date->configure_business('Acme Ltd.') or die;
$date->configure_business(\@my_holidays) or die;
$date->configure_business(\&my_make_calendar) or die;

# some pre-defined configurations
$date->configure_business('us');           # US banking
$date->configure_business('de');           # German nation-wide

DESCRIPTION

Date::Gregorian::Business is an extension of Date::Gregorian supporting date calculations involving business days.

Objects of this class have a notion of whether or not a day is a business day and provide methods to count business days between two dates or find the other end of a date interval, given a start or end date and a number of business days in between. Other methods allow to define business calendars for use with this module.

By default, a date interval includes the earlier date and does not include the later date of its two end points, no matter in what order they are given. We call this "morning alignment". However, individual date objects can be either "morning" or "evening" aligned, meaning they represent the situation at the beginning or end of the day in question. Where a date object is the result of a calculation, its alignment can be chosen through an optional method argument.

User methods

new

new, called as a class method, creates and returns a new date object. The optional parameter can be a configuration or (more typically) the name of a configuration. If omitted, the current default configuration is used. Business calendar configurations are described in detail in an extra section below. In case of bad configurations undef is returned.

new, called as an object method, returns a clone of the object. A different configuration for the new object can be specified. Again, in case of bad configurations undef is returned.

is_businessday

is_businessday returns a nonzero number (typically 1) if the date currently represented by the object is a business day, or zero if it falls on a weekend or holiday. Special business calendars may have business days counting less than a whole day in calculations. Objects configured that way may return 0.5 or even another numeric value between 0 and 1 for some dates. In any case is_businessday can be used in boolean context.

align

align sets the alignment of a date. An alignment of 0 means morning alignment, 1 means evening alignment. With morning alignment, the current day is counted in durations extending into the future, and not counted in durations extending from that date into the past. Mnemonic is, in the morning, a day's business lies ahead, whereas in the evening, it lies behind. Night workers please pardon the simplification.

get_businessdays_since get_businessdays_until

There are two methods to count the number of business days between two dates. Their only difference is the sign of the result: get_businessdays_since is positive if the parameter refers to an earlier date than the object and business days lie between them, zero if no business days are counted, and negative otherwise. Note the role of alignments described in the previous paragraph. get_businessdays_until is positive when get_businessdays_since is negative and vice versa. The parameter may be an arbitrary Date::Gregorian object. If it is not a Date::Gregorian::Business object its alignment is taken to be the default (morning).

set_next_businessday

set_next_businessday moves an arbitrary date up or down to the next business day. Its parameter must be one of the four relation operators ">=", ">", "<=" or "<" as a string. ">=" means, the date should not be changed if it is a business day, or changed to the closest business day in the future otherwise. ">" means the date should be changed to the closest business day truly later than the current date. "<=" and "<" likewise work in the other direction. Alignment does not matter and is not changed.

add_businessdays

add_businessdays moves an arbitrary date forward or backwards in time up to a given number of business days. A positive number of days means moving towards the future. The result is always a business day. The alignment will not be changed if the second parameter is omitted, or else set to the second parameter. The result will be rounded to the beginning or end of a business day if necessary, as determined by its alignment.

Rounding: If you work with simple calendars and integer numbers, all results will be precise. However, with calendars containing fractions of business days or with non-integer values of day differences, a calculated date may end up somewhere in the middle of a business day rather than at its beginning or end. The final result will stay at that date but move up or down to the desired alignment. In other words, fractional days will be rounded down to morning alignment or up to evening alignment, whichever applies.

No ambiguities: Even if a calculated date lies next to a number of non-business days in a way that more than one date would satisfy a desired span of business days, results are always well-defined by the fact that they must be business days. Thus, morning alignment will pull a result to the first business day after weekends and holidays, while evening alignment will pull a result to the last business day before any non-business days. If you add zero business days to some arbitrary date you get the unique date of the properly aligned business day next to it.

iterate_businessdays_upto iterate_businessdays_downto

iterate_businessdays_upto and iterate_businessdays_downto provide iterators over a range of business days. They return a reference to a subroutine that can be called without argument in a while condition to set the given date iteratively to each one of a sequence of dates, while skipping non-business days. The business day closest to the current date is always the first one to be visited (unless the sequence is all empty). The limit parameter determines the end of the sequence, together with the relation parameter: '<' excludes the upper limit from the sequence, '<=' includes the upper limit, '>=' includes the lower limit and '>' excludes the lower limit.

Each iterator maintains its own state; therefore it is legal to run more than one iterator in parallel or even create new iterators within iterations. Undefining an iterator after use might help to save memory.

get_alignment

get_alignment retrieves the alignment (either 0 for morning or 1 for evening).

Configuration

Version compatibility note: The configuration specifications described here are expected to evolve with further development of this module. In fact, they should ultimately be replaced by easier-to-use configuration objects. We will try to stay downward compatible for some time, however.

The business calendar to use can be customized both on an object-by-object basis and by way of general defaults. Business calendars can be stored under a name and later referenced by that name.

A business calendar can be defined through a list of holiday definitions or more generally through a code reference, as explained below. A number of such definitions of common interest will be accessible in later editions of this module or some related component.

define_configuration

define_configuration names and defines a configuration. It can later be referenced by its name. By convention, user-defined names should start with an uppercase letter, while configuration names provided as a part of the distribution will always start with a lowercase letter.

configure_business

configure_business, used as an object method, re-configures that object. It returns the object on success, undef in case of a bad configuration.

configure_business, used as a class method, defines the default configuration for new objects created with neither a configuration parameter nor a reference object. It returns the class name on success, undef in case of a bad configuration.

The configuration parameter for define_configuration, new and configure_business can be the name of a known configuration, an array reference or a code reference. A configuration name must be known at the time it is used, for it is always immediately replaced by the named configuration.

An array reference used as a configuration has to refer to a two-element array like this:

$configuration = [\@weekend_days, \@holidays];

Here, @weekend_days is a list of the non-business days of every week, given as numerical values as defined in Date::Gregorian. For example:

use Date::Gregorian qw(:weekdays);
@weekend_days = (SATURDAY, SUNDAY);

The list of weekend days may be empty, but must not contain all seven days of the week, which would imply that the whole week has no business days and thus be the reason for endless loops.

The second element of a configuration is a list of holiday definitions. Each one of these defines a yearly recurring event like this:

$holiday = [$month, $day, $weekday_shift, $valid_years];

Here, $month and $day with month ranging from 1 to 12 define an anniversary by date. Alternatively, month may be zero and day a signed integer value defining a date relative to Easter Sunday. For example, [0, -2] would refer to Good Friday (two days before Easter Sunday) while [0, 1] would refer to Easter Monday. The distance from Easter Sunday must be in the range of (roughly) -80..250 to make sure the actual date is a day of the same year. Easter-related holidays ending up in different years are silently ignored.

If $weekday_shift is omitted or undefined, a holiday occurs on a fixed month and day (or distance from easter), no matter what day of the week it falls on. In order to shift it dependent on the weekday, $weekday_shift must be a reference of a seven-element array of days to add, ordered from Monday to Sunday. Examples:

[0, 0, 0, 0, 0, 2, 1] # Saturday and Sunday -> next Monday

[0, 6, 5, 4, 3, 2, 1] # any day other than Monday -> next Monday

[3, 2, 1, 0, 6, 5, 4] # any non-Thursday -> next Thursday

The last two examples above show how holidays can be defined that always fall on the same day of the week. To continue the example, Thanksgiving Day could be defined like this:

$thanksgiving = [11, 22, [3, 2, 1, 0, 6, 5, 4]];

The fourth element of a holiday definition is also optional and limits the years the definition is valid for. It may be either:

  • a plain number, defining the single year the definition is valid,

  • a reference of a two-element array, defining the first and the last year of a range of years, where undef means no limit,

  • a reference of a subroutine taking a date object and a year, month and day, returning a boolean for whether the holiday is valid in that year. Month and day are taken directly from the holiday definition (even where the month value is zero for dates relative to easter). The date object is a clone of the original object (though not initialized to a particular date), just for safety. It may be changed while the original object should not be.

A more general way to specify a complete configuration is a code reference. It must refer to a subroutine that takes a date object and a year (which you can also view as a method with a year parameter) and returns an array reference. The array must have exactly that many elements as there are days in the given year. Each element must be defined and have a numerical value greater or equal to zero. These values will be returned by is_businessday and added together in calculations. The idea is that one call to the subroutine figures out the calendar of a whole year in one go.

get_empty_calendar

get_empty_calendar is a helper method mainly intended for use in such a subroutine. It takes two mandatory parameters, a year and a reference to an array like @weekend_days above, and returns a reference of an array of zeroes and ones representing the weekends and weekly business days of that year suitable to be further modified and finally returned by said subroutine.

EXPORTS

None.

SEE ALSO

Date::Gregorian.

AUTHOR

Martin Becker <becker-cpan-mp (at) cozap.com>

LICENSE AND COPYRIGHT

Copyright (c) 1999-2019 by Martin Becker, Blaubeuren.

This library is free software; you can distribute it and/or modify it under the terms of the Artistic License 2.0 (see the LICENSE file).

DISCLAIMER OF WARRANTY

This library is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.