Skip to content

Commit

Permalink
Merge pull request #54 from gocardless/joesouthan/remove-calendars
Browse files Browse the repository at this point in the history
Remove included calendars from gem
  • Loading branch information
JoeSouthan authored May 26, 2020
2 parents b12c186 + 5e2aaef commit b7bcef4
Show file tree
Hide file tree
Showing 15 changed files with 148 additions and 928 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 2.0.0 - May 4, 2020

🚨 **BREAKING CHANGES** 🚨

For more on the breaking changes that have been introduced in v2.0.0 please [see the readme](README.md#v200-breaking-changes).

- Remove bundled calendars see [this pr](https://github.com/gocardless/business/pull/54) for more context. If you need to use any of the previously bundled calendars, [see here](https://github.com/gocardless/business/tree/b12c186ca6fd4ffdac85175742ff7e4d0a705ef4/lib/business/data)
- `Business::Calendar.load_paths=` is now required

## 1.18.0 - April 30, 2020

### Note we have dropped support for Ruby < 2.4.x
Expand Down
184 changes: 106 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,135 @@

Date calculations based on business calendars.

## Documentation
- [v2.0.0 breaking changes](#v200-breaking-changes)
- [Getting Started](#getting-started)
- [Creating a calendar](#creating-a-calendar)
- [Using a calendar file](#use-a-calendar-file)
- [Checking for business days](#checking-for-business-days)
- [Business day arithmetic](#business-day-arithmetic)
- [But other libraries already do this](#but-other-libraries-already-do-this)
- [License & Contributing](#license--contributing)

To get business, simply:
## 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](https://github.com/gocardless/business/tree/b12c186ca6fd4ffdac85175742ff7e4d0a705ef4/lib/business/data)
- 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

```ruby
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`

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

## Getting started

To install business, simply:

```bash
$ gem install business
gem install business
```

If you are using a Gemfile:

```ruby
gem "business", "~> 2.0"
```

### Getting started
### Creating a calendar

Get started 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.
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.

```ruby
calendar = Business::Calendar.new(
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.
)
```

`extra_working_dates` key makes the calendar to consider a weekend day as a working day.
### Use a calendar file

A few calendar configs are bundled with the gem (see [lib/business/data]((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.
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](#example-calendar) to build your first calendar. All keys are optional and will default to the following:

```ruby
calendar = Business::Calendar.load("weekdays")
calendar = Business::Calendar.load_cached("weekdays")
```
Note: Elements of `holidays` and `extra_working_dates` may be either strings that `Date.parse()` [can understand](https://ruby-doc.org/stdlib-2.7.1/libdoc/date/rdoc/Date.html#method-c-parse), 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].

If `working_days` is missing, then common default is used (mon-fri).
If `holidays` is missing, "no holidays" assumed.
If `extra_working_dates` is missing, then no changes in `working_days` will happen.
#### YAML file Structure

```yml
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
-
```
Elements of `holidays` and `extra_working_dates` may be
eiter strings that `Date.parse()` can understand,
or YYYY-MM-DD (which is considered as a Date by Ruby YAML itself).
#### Example calendar
```yaml
# lib/calendars/my_calendar.yml
working_days:
- Monday
- Wednesday
- Friday
holidays:
- 2017-01-08 # Same as January 8th, 2017
- 1st April 2020
- 2021-04-01
extra_working_dates:
- 9th March 2020 # A Saturday
```
### Checking for business days
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.

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 `Calendar`.
`load_paths` also accepts an array of plain Ruby hashes with the format:

```ruby
calendar.business_day?(Date.parse("Monday, 9 June 2014"))
# => true
calendar.business_day?(Date.parse("Sunday, 8 June 2014"))
# => false
{ "calendar_name" => { "working_days" => [] }
```

### Custom calendars
#### Example loading both a path and ruby hashes

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

To use a calendar you've written yourself, you need to add the directory it's
stored in as an additional calendar load path:
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.

```ruby
Business::Calendar.additional_load_paths = ['path/to/your/calendar/directory']
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")
```

You can then load the calendar as normal.
## Checking for business days

### Business day arithmetic
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`.

The `add_business_days` and `subtract_business_days` are used to perform
business day arithmetic on dates.
```ruby
calendar.business_day?(Date.parse("Monday, 9 June 2014"))
# => true
calendar.business_day?(Date.parse("Sunday, 8 June 2014"))
# => false
```

## Business day arithmetic

The `add_business_days` and `subtract_business_days` are used to perform business day arithmetic on dates.

```ruby
date = Date.parse("Thursday, 12 June 2014")
Expand All @@ -88,10 +143,7 @@ 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.
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.

```ruby
date = Date.parse("Saturday, 14 June 2014")
Expand All @@ -101,51 +153,27 @@ 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.
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.

```ruby
date = Date.parse("Saturday, 14 June 2014")
calendar.business_days_between(date, date + 7)
# => 5
```

### Included Calendars

We include some calendar data with this Gem but give no guarantees of its
accuracy.
The calendars that we include are:

* Bacs
* Bankgirot
* BECS (Australia)
* BECSNZ (New Zealand)
* PAD (Canada)
* Betalingsservice
* Target (SEPA)
* TargetFrance (SEPA + French bank holidays)
* US Banking (ACH)

## But other libraries already do this

Another gem, [business_time](https://github.com/bokmann/business_time), also
exists for this purpose. We previously used business_time, but encountered
several issues that prompted us to start business.
Another gem, [business_time](https://github.com/bokmann/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.

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.

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.
<p align="center"><img src="http://3.bp.blogspot.com/-aq4iOz2OZzs/Ty8xaQwMhtI/AAAAAAAABrM/-vn4tcRA9-4/s1600/daily-morning-awesomeness-243.jpeg" alt="I'm late for business" width="250"/></p>

## License & Contributing
- business is available as open source under the terms of the [MIT License](LICENSE).
- Bug reports and pull requests are welcome on GitHub at https://github.com/gocardless/business.

![I'm late for business](http://3.bp.blogspot.com/-aq4iOz2OZzs/Ty8xaQwMhtI/AAAAAAAABrM/-vn4tcRA9-4/s1600/daily-morning-awesomeness-243.jpeg)
GoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/about/jobs).
31 changes: 20 additions & 11 deletions lib/business/calendar.rb
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
require 'yaml'
require 'date'

module Business
class Calendar
class << self
attr_accessor :additional_load_paths
attr_accessor :load_paths
end

def self.calendar_directories
directories = @additional_load_paths || []
directories + [File.join(File.dirname(__FILE__), 'data')]
@load_paths
end
private_class_method :calendar_directories

def self.load(calendar)
directory = calendar_directories.find do |dir|
File.exists?(File.join(dir, "#{calendar}.yml"))
def self.load(calendar_name)
data = calendar_directories.detect do |path|
if path.is_a?(Hash)
break path[calendar_name] if path[calendar_name]
else
next unless File.exists?(File.join(path, "#{calendar_name}.yml"))

break YAML.load_file(File.join(path, "#{calendar_name}.yml"))
end
end
raise "No such calendar '#{calendar}'" unless directory

yaml = YAML.load_file(File.join(directory, "#{calendar}.yml"))
raise "No such calendar '#{calendar_name}'" unless data

valid_keys = %w(holidays working_days extra_working_dates)

unless (yaml.keys - valid_keys).empty?
unless (data.keys - valid_keys).empty?
raise "Only valid keys are: #{valid_keys.join(', ')}"
end

self.new(holidays: yaml['holidays'], working_days: yaml['working_days'],
extra_working_dates: yaml['extra_working_dates'])
self.new(
holidays: data['holidays'],
working_days: data['working_days'],
extra_working_dates: data['extra_working_dates'],
)
end

@lock = Mutex.new
Expand Down
58 changes: 0 additions & 58 deletions lib/business/data/achus.yml

This file was deleted.

Loading

0 comments on commit b7bcef4

Please sign in to comment.