Date calculations based on business calendars.
Get stareted with business by creating an instance of the calendar class, passing in a hash that specifies with days of the week are considered working days, and which days are holidays.
calendar = Business::Calendar.new(
working_days: %w( mon tue wed thu fri ),
holidays: ["01/01/2014", "03/01/2014"]
)
A few calendar configs are bundled with the gem (see lib/business/data for
details). Load them by calling the load
class method on Calendar
. The
load_cached
variant of this method caches the calendars by name after loading
them, to avoid reading and parsing the config file multiple times.
calendar = Business::Calendar.load("weekdays")
calendar = Business::Calendar.load_cached("weekdays")
To check whether a given date is a business day (falls on one of the specified
working days, and is not a holiday), use the business_day?
method on
Calendar
.
calendar.business_day?(Date.parse("Monday, 9 June 2014"))
# => true
calendar.business_day?(Date.parse("Sunday, 8 June 2014"))
# => false
The add_business_days
and subtract_business_days
are used to perform
business day arithemtic on dates.
date = Date.parse("Thursday, 12 June 2014")
calendar.add_business_days(date, 4).strftime("%A, %d %B %Y")
# => "Wednesday, 18 June 2014"
calendar.subtract_business_days(date, 4).strftime("%A, %d %B %Y")
# => "Friday, 06 June 2014"
The roll_forward
and roll_backward
methods snap a date to a nearby business
day. If provided with a business day, they will return that date. Otherwise,
they will advance (forward for roll_forward
and backward for roll_backward
)
until a business day is found.
date = Date.parse("Saturday, 14 June 2014")
calendar.roll_forward(date).strftime("%A, %d %B %Y")
# => "Monday, 16 June 2014"
calendar.roll_backward(date).strftime("%A, %d %B %Y")
# => "Friday, 13 June 2014"
To count the number of business days between two dates, pass the dates to
business_days_between
. This method counts from start of the first date to
start of the second date. So, assuming no holidays, there would be two business
days between a Monday and a Wednesday.
date = Date.parse("Saturday, 14 June 2014")
calendar.business_days_between(date, date + 7)
# => 5
Another gem, business_time, also exists for this purpose. We previously used business_time, but encountered several issues that prompted us to start business.
Firstly, business_time works by monkey-patching Date
, Time
, and FixNum
.
While this enables syntax like Time.now + 1.business_day
, it means that all
configuration has to be global. GoCardless handles payments across several
geographies, so being able to work with multiple working-day calendars is
essential for us. Business provides a simple Calendar
class, that is
initialized with a configuration that specifies which days of the week are
considered to be working days, and which dates are holidays.
Secondly, business_time supports calculations on times as well as dates. For our purposes, date-based calculations are sufficient. Supporting time-based calculations as well makes the code significantly more complex. We chose to avoid this extra complexity by sticking solely to date-based mathematics.