Project

business

0.23
There's a lot of open issues
No release in over a year
Date calculations based on business calendars
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 3.1
~> 2.31.0
~> 1.25.0
 Project Readme

Business

Gem version CircleCI

Date calculations based on business calendars.

  • v2.0.0 breaking changes
  • Getting Started
    • Creating a calendar
    • Using a calendar file
  • Checking for business days
  • Business day arithmetic
  • But other libraries already do this
  • License & Contributing

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.