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
Changes from all 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
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
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
184 changes: 106 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
@@ -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"
```
JoeSouthan marked this conversation as resolved.
Show resolved Hide resolved

### 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")
@@ -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")
@@ -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
58 changes: 0 additions & 58 deletions lib/business/data/achus.yml

This file was deleted.

Loading