Skip to content

gocardless/business

Repository files navigation

Business

Gem version CircleCI

Date calculations based on business calendars.

v2.0.0 breaking changes

We have removed the bundled calendars as of version 2.0.0, if you need the calendars that were included:

  • Download the calendars you wish to use from v1.18.0
  • Place them in a suitable directory in your project, typically lib/calendars
  • Add this directory path to your instance of Business::Calendar using the load_paths method.dd the directory to where you placed the yml files before you load the calendar
Business::Calendar.load_paths = ["lib/calendars"] # your_project/lib/calendars/ contains bacs.yml
Business::Calendar.load("bacs")

If you wish to stay on the last version that contained bundled calendars, pin business to v1.18.0

# Gemfile
gem "business", "v1.18.0"

Getting started

To install business, simply:

gem install business

If you are using a Gemfile:

gem "business", "~> 2.0"

Creating a calendar

Get started with business by creating an instance of the calendar class, that accepts a hash that specifies which days of the week are considered working days, which days are holidays and which are extra working dates.

Additionally each calendar instance can be given a name. This can come in handy if you use multiple calendars.

calendar = Business::Calendar.new(
  name: 'my calendar',
  working_days: %w( mon tue wed thu fri ),
  holidays: ["01/01/2014", "03/01/2014"],    # array items are either parseable date strings, or real Date objects
  extra_working_dates: [nil], # Makes the calendar to consider a weekend day as a working day.
)

Use a calendar file

Defining a calendar as a Ruby object may not be convenient, so we provide a way of defining these calendars as YAML. Below we will walk through the necessary steps to build your first calendar. All keys are optional and will default to the following:

Note: Elements of holidays and extra_working_dates may be either strings that Date.parse() can understand, or YYYY-MM-DD (which is considered as a Date by Ruby YAML itself)[https://github.com/ruby/psych/blob/6ec6e475e8afcf7868b0407fc08014aed886ecf1/lib/psych/scalar_scanner.rb#L60].

YAML file Structure

working_days: # Optional, default [Monday-Friday]
  -
holidays: # Optional, default: []  ie: "no holidays" assumed
  -
extra_working_dates: # Optional, default: [], ie: no changes in `working_days` will happen
  -

Example calendar

# lib/calendars/my_calendar.yml
working_days:
  - Monday
  - Wednesday
  - Friday
holidays:
  - 1st April 2020
  - 2021-04-01
extra_working_dates:
  - 9th March 2020 # A Saturday

Ensure the calendar file is saved to a directory that will hold all your calendars, typically lib/calendars, then add this directory to your instance of Business::Calendar using the load_paths method before you call your calendar.

load_paths also accepts an array of plain Ruby hashes with the format:

  { "calendar_name" => { "working_days" => [] }

Example loading both a path and ruby hashes

Business::Calendar.load_paths = [
  "lib/calendars",
  { "foo_calendar" => { "working_days" => ["monday"] } },
  { "bar_calendar" => { "working_days" => ["sunday"] } },
]

Now you can load the calendar by calling the Business::Calendar.load(calendar_name). In order to avoid parsing the calendar file multiple times, there is a Business::Calendar.load_cached(calendar_name) method that caches the calendars by name after loading them.

calendar = Business::Calendar.load("my_calendar") # lib/calendars/my_calendar.yml
calendar = Business::Calendar.load("foo_calendar")
# or
calendar = Business::Calendar.load_cached("my_calendar")
calendar = Business::Calendar.load_cached("foo_calendar")

Checking for business days

To check whether a given date is a business day (falls on one of the specified working days or working dates, and is not a holiday), use the business_day? method on Business::Calendar.

calendar.business_day?(Date.parse("Monday, 9 June 2014"))
# => true
calendar.business_day?(Date.parse("Sunday, 8 June 2014"))
# => false

More specifically you can check if a given business_day? is either a working_day? or a holiday? using methods on Business::Calendar.

# Assuming "Monday, 9 June 2014" is a holiday
calendar.working_day?(Date.parse("Monday, 9 June 2014"))
# => true
calendar.holiday?(Date.parse("Monday, 9 June 2014"))
# => true
# Monday is a working day, but we have a holiday so it's not
# a business day
calendar.business_day?(Date.parse("Monday, 9 June 2014"))
# => false

Business day arithmetic

The add_business_days and subtract_business_days are used to perform business day arithmetic 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

But other libraries already do this

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.

I'm late for business

License & Contributing

GoCardless ♥ open source. If you do too, come join us.