Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove included calendars from gem #54

Merged
merged 7 commits into from
May 26, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 2.0.0 - May 4, 2020

**BREAKING CHANGES** 🚨
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved

- 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
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved

## 1.18.0 - April 30, 2020

### Note we have dropped support for Ruby < 2.4.x
Expand Down
143 changes: 71 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,72 +13,101 @@ To get business, simply:
$ gem install business
```

## Important: 2.0.0 breaking changes
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved

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 [before version 2](https://github.com/gocardless/business/tree/b12c186ca6fd4ffdac85175742ff7e4d0a705ef4/lib/business/data) and place them in a suitable place in your project.
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved

Then, add the directory to where you placed the yml files before you load the calendar:

```ruby
Calendar::Business.load_paths("lib/calendars") # your_project/lib/calendars/ contains bacs.yml
Calendar::Business.load("bacs")
```
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved

### Getting started

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, passing in a hash that specifies with days of the week are considered working days, and which days are holidays.
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved

```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.
### Load a calendar from a 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.
#### Calendar file definition

```ruby
calendar = Business::Calendar.load("weekdays")
calendar = Business::Calendar.load_cached("weekdays")
```
Defining a calendar as a Ruby object may not be convient, to load it from a YAML file follow and customise the example below. All keys are optional and will default to the following:
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved

- If `working_days` is missing, then common default is used (mon-fri).
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved
- If `holidays` is missing, "no holidays" assumed.
- If `extra_working_dates` is missing, then no changes in `working_days` will happen.

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.
> Note: 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).

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

```yaml
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
#### Using the 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`.
Ensure the calendar file is saved to a directory that will hold all your calendars, eg; `path/to/your/calendar/directory` then add this directory to your code before you call your calendar:

```ruby
calendar.business_day?(Date.parse("Monday, 9 June 2014"))
# => true
calendar.business_day?(Date.parse("Sunday, 8 June 2014"))
# => false
Business::Calendar.load_paths = ["path/to/your/calendar/directory"]
```

### Custom calendars
Now you can load the calendar by calling the `load` class method on `Business::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.

To use a calendar you've written yourself, you need to add the directory it's
stored in as an additional calendar load path:
```ruby
calendar = Business::Calendar.load("my_calendars")
# or
calendar = Business::Calendar.load_cached("my_calendars")
```

#### New in version 2.0.0:

Add a hash to the `load_path` array to use already loaded data. This can be useful if loading calendar data from an external source.

```ruby
Business::Calendar.additional_load_paths = ['path/to/your/calendar/directory']
Business::Calendar.load_paths = [
"path/to/your/calendar/directory",
{ "foo_calendar" => { "working_days" => ["monday"] } }
]

Business::Calendar.load("foo_calendar")
```

You can then load the calendar as normal.
### 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`.

```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.
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 +117,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 +127,24 @@ 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
## But other libraries already do this

We include some calendar data with this Gem but give no guarantees of its
accuracy.
The calendars that we include are:
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.

* Bacs
* Bankgirot
* BECS (Australia)
* BECSNZ (New Zealand)
* PAD (Canada)
* Betalingsservice
* Target (SEPA)
* TargetFrance (SEPA + French bank holidays)
* US Banking (ACH)
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.

## But other libraries already do this
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.

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.

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](http://3.bp.blogspot.com/-aq4iOz2OZzs/Ty8xaQwMhtI/AAAAAAAABrM/-vn4tcRA9-4/s1600/daily-morning-awesomeness-243.jpeg)
30 changes: 19 additions & 11 deletions lib/business/calendar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,38 @@
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