business 1.17.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +37 -0
- data/.rubocop.yml +13 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +29 -1
- data/Gemfile +2 -0
- data/README.md +119 -76
- data/business.gemspec +10 -5
- data/lib/business.rb +3 -1
- data/lib/business/calendar.rb +81 -46
- data/lib/business/version.rb +3 -1
- data/spec/{calendar_spec.rb → business/calendar_spec.rb} +273 -106
- metadata +52 -20
- data/.travis.yml +0 -17
- data/lib/business/data/achus.yml +0 -58
- data/lib/business/data/bacs.yml +0 -129
- data/lib/business/data/bankgirot.yml +0 -208
- data/lib/business/data/becs.yml +0 -36
- data/lib/business/data/becsnz.yml +0 -50
- data/lib/business/data/betalingsservice.yml +0 -54
- data/lib/business/data/padca.yml +0 -27
- data/lib/business/data/target.yml +0 -101
- data/lib/business/data/targetfrance.yml +0 -130
- data/lib/business/data/weekdays.yml +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a053f4e9b8b8fa5ed407b58d1e81155dac75f1f0c305410403011c608c69c60
|
4
|
+
data.tar.gz: 90cd733d6991516ae8c438824e669850d554147fbb47172f1c4f0dd0bdd44e37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f09af74f16a84326d8b193f219b1d4de9362c618d197977e4c887df49a34e73d9ad3db154cfe7606fa43b3017b4448175ac37e598ea497e8f508888cf3a2bdb2
|
7
|
+
data.tar.gz: 9dc9a98ae02f7cb056af0bd710ac754594931c796d934b18695fafec5caecf1d7e4f107966863bc46d11e71e576b214cb774fd1a19dbe1d8c024483e55e27503
|
@@ -0,0 +1,37 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
docker:
|
6
|
+
- image: circleci/ruby:<< parameters.ruby-version >>
|
7
|
+
parameters:
|
8
|
+
ruby-version:
|
9
|
+
type: string
|
10
|
+
steps:
|
11
|
+
- checkout
|
12
|
+
- restore_cache:
|
13
|
+
keys:
|
14
|
+
- bundle-v1-<< parameters.ruby-version >>-{{ checksum "business.gemspec" }}
|
15
|
+
- bundle-v1-<< parameters.ruby-version >>-
|
16
|
+
- run:
|
17
|
+
name: Install dependencies
|
18
|
+
command: bundle install --clean --no-cache --path vendor/bundle --jobs=4 --retry=3
|
19
|
+
- save_cache:
|
20
|
+
key: bundle-v1-<< parameters.ruby-version >>-{{ checksum "business.gemspec" }}
|
21
|
+
paths:
|
22
|
+
- vendor/bundle
|
23
|
+
- run:
|
24
|
+
name: Run tests
|
25
|
+
command: bundle exec rspec
|
26
|
+
- run:
|
27
|
+
name: Run rubocop
|
28
|
+
command: bundle exec rubocop --parallel --extra-details --display-style-guide
|
29
|
+
|
30
|
+
workflows:
|
31
|
+
default:
|
32
|
+
jobs:
|
33
|
+
- test:
|
34
|
+
name: Ruby << matrix.ruby-version >>
|
35
|
+
matrix:
|
36
|
+
parameters:
|
37
|
+
ruby-version: ["2.4.9", "2.5.8", "2.6.6", "2.7.1"]
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
inherit_gem:
|
2
|
+
gc_ruboconfig: rubocop.yml
|
3
|
+
|
4
|
+
RSpec/NestedGroups:
|
5
|
+
Exclude:
|
6
|
+
- spec/business/calendar_spec.rb
|
7
|
+
|
8
|
+
# Disabled as API uses set_holidays etc
|
9
|
+
Naming/AccessorMethodName:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Gemspec/RequiredRubyVersion:
|
13
|
+
Enabled: false
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.7.1
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,32 @@
|
|
1
|
-
##
|
1
|
+
## 2.2.0 - March 4, 2021
|
2
|
+
|
3
|
+
- Add `Business::Calendar#name` - thanks @mattmcf!
|
4
|
+
|
5
|
+
## 2.1.0 - June 8, 2020
|
6
|
+
|
7
|
+
- Add seperate `working_day?` and `holiday?` methods to the calendar
|
8
|
+
|
9
|
+
## 2.0.0 - May 4, 2020
|
10
|
+
|
11
|
+
🚨 **BREAKING CHANGES** 🚨
|
12
|
+
|
13
|
+
For more on the breaking changes that have been introduced in v2.0.0 please [see the readme](README.md#v200-breaking-changes).
|
14
|
+
|
15
|
+
- 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)
|
16
|
+
- `Business::Calendar.load_paths=` is now required
|
17
|
+
|
18
|
+
## 1.18.0 - April 30, 2020
|
19
|
+
|
20
|
+
### Note we have dropped support for Ruby < 2.4.x
|
21
|
+
|
22
|
+
- Correct Danish public holidays
|
23
|
+
|
24
|
+
## 1.17.1 - November 19, 2019
|
25
|
+
|
26
|
+
- Change May Bank Holiday 2020 for UK (Bacs) - this was moved to the 8th.
|
27
|
+
- Add 2020 holidays for PAD.
|
28
|
+
|
29
|
+
## 1.17.0 - October 30, 2019
|
2
30
|
|
3
31
|
- Add holiday calendar for France (Target(SEPA) + French bank holidays)
|
4
32
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,61 +1,128 @@
|
|
1
1
|
# Business
|
2
2
|
|
3
3
|
[![Gem version](https://badge.fury.io/rb/business.svg)](http://badge.fury.io/rb/business)
|
4
|
-
[![
|
4
|
+
[![CircleCI](https://circleci.com/gh/gocardless/business.svg?style=svg)](https://circleci.com/gh/gocardless/business)
|
5
5
|
|
6
6
|
Date calculations based on business calendars.
|
7
7
|
|
8
|
-
|
8
|
+
- [v2.0.0 breaking changes](#v200-breaking-changes)
|
9
|
+
- [Getting Started](#getting-started)
|
10
|
+
- [Creating a calendar](#creating-a-calendar)
|
11
|
+
- [Using a calendar file](#use-a-calendar-file)
|
12
|
+
- [Checking for business days](#checking-for-business-days)
|
13
|
+
- [Business day arithmetic](#business-day-arithmetic)
|
14
|
+
- [But other libraries already do this](#but-other-libraries-already-do-this)
|
15
|
+
- [License & Contributing](#license--contributing)
|
9
16
|
|
10
|
-
|
17
|
+
## v2.0.0 breaking changes
|
18
|
+
|
19
|
+
We have removed the bundled calendars as of version 2.0.0, if you need the calendars that were included:
|
20
|
+
|
21
|
+
- Download the calendars you wish to use from [v1.18.0](https://github.com/gocardless/business/tree/b12c186ca6fd4ffdac85175742ff7e4d0a705ef4/lib/business/data)
|
22
|
+
- Place them in a suitable directory in your project, typically `lib/calendars`
|
23
|
+
- 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
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
Business::Calendar.load_paths = ["lib/calendars"] # your_project/lib/calendars/ contains bacs.yml
|
27
|
+
Business::Calendar.load("bacs")
|
28
|
+
```
|
29
|
+
|
30
|
+
If you wish to stay on the last version that contained bundled calendars, pin `business` to `v1.18.0`
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# Gemfile
|
34
|
+
gem "business", "v1.18.0"
|
35
|
+
```
|
36
|
+
|
37
|
+
## Getting started
|
38
|
+
|
39
|
+
To install business, simply:
|
11
40
|
|
12
41
|
```bash
|
13
|
-
|
42
|
+
gem install business
|
14
43
|
```
|
15
44
|
|
16
|
-
|
45
|
+
If you are using a Gemfile:
|
17
46
|
|
18
|
-
|
19
|
-
|
20
|
-
|
47
|
+
```ruby
|
48
|
+
gem "business", "~> 2.0"
|
49
|
+
```
|
50
|
+
|
51
|
+
### Creating a calendar
|
52
|
+
|
53
|
+
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.
|
21
54
|
|
22
55
|
```ruby
|
23
56
|
calendar = Business::Calendar.new(
|
24
57
|
working_days: %w( mon tue wed thu fri ),
|
25
58
|
holidays: ["01/01/2014", "03/01/2014"] # array items are either parseable date strings, or real Date objects
|
59
|
+
extra_working_dates: [nil], # Makes the calendar to consider a weekend day as a working day.
|
26
60
|
)
|
27
61
|
```
|
28
62
|
|
29
|
-
|
63
|
+
### Use a calendar file
|
30
64
|
|
31
|
-
|
32
|
-
details). Load them by calling the `load` class method on `Calendar`. The
|
33
|
-
`load_cached` variant of this method caches the calendars by name after loading
|
34
|
-
them, to avoid reading and parsing the config file multiple times.
|
65
|
+
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:
|
35
66
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
```
|
67
|
+
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].
|
68
|
+
|
69
|
+
#### YAML file Structure
|
40
70
|
|
41
|
-
|
42
|
-
|
43
|
-
|
71
|
+
```yml
|
72
|
+
working_days: # Optional, default [Monday-Friday]
|
73
|
+
-
|
74
|
+
holidays: # Optional, default: [] ie: "no holidays" assumed
|
75
|
+
-
|
76
|
+
extra_working_dates: # Optional, default: [], ie: no changes in `working_days` will happen
|
77
|
+
-
|
78
|
+
```
|
44
79
|
|
45
|
-
|
46
|
-
eiter strings that `Date.parse()` can understand,
|
47
|
-
or YYYY-MM-DD (which is considered as a Date by Ruby YAML itself).
|
80
|
+
#### Example calendar
|
48
81
|
|
49
82
|
```yaml
|
83
|
+
# lib/calendars/my_calendar.yml
|
84
|
+
working_days:
|
85
|
+
- Monday
|
86
|
+
- Wednesday
|
87
|
+
- Friday
|
50
88
|
holidays:
|
51
|
-
-
|
89
|
+
- 1st April 2020
|
90
|
+
- 2021-04-01
|
91
|
+
extra_working_dates:
|
92
|
+
- 9th March 2020 # A Saturday
|
93
|
+
```
|
94
|
+
|
95
|
+
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.
|
96
|
+
|
97
|
+
`load_paths` also accepts an array of plain Ruby hashes with the format:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
{ "calendar_name" => { "working_days" => [] }
|
101
|
+
```
|
102
|
+
|
103
|
+
#### Example loading both a path and ruby hashes
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
Business::Calendar.load_paths = [
|
107
|
+
"lib/calendars",
|
108
|
+
{ "foo_calendar" => { "working_days" => ["monday"] } },
|
109
|
+
{ "bar_calendar" => { "working_days" => ["sunday"] } },
|
110
|
+
]
|
111
|
+
```
|
112
|
+
|
113
|
+
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.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
calendar = Business::Calendar.load("my_calendar") # lib/calendars/my_calendar.yml
|
117
|
+
calendar = Business::Calendar.load("foo_calendar")
|
118
|
+
# or
|
119
|
+
calendar = Business::Calendar.load_cached("my_calendar")
|
120
|
+
calendar = Business::Calendar.load_cached("foo_calendar")
|
52
121
|
```
|
53
122
|
|
54
|
-
|
123
|
+
## Checking for business days
|
55
124
|
|
56
|
-
To check whether a given date is a business day (falls on one of the specified
|
57
|
-
working days or working dates, and is not a holiday), use the `business_day?`
|
58
|
-
method on `Calendar`.
|
125
|
+
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`.
|
59
126
|
|
60
127
|
```ruby
|
61
128
|
calendar.business_day?(Date.parse("Monday, 9 June 2014"))
|
@@ -64,21 +131,23 @@ calendar.business_day?(Date.parse("Sunday, 8 June 2014"))
|
|
64
131
|
# => false
|
65
132
|
```
|
66
133
|
|
67
|
-
|
68
|
-
|
69
|
-
To use a calendar you've written yourself, you need to add the directory it's
|
70
|
-
stored in as an additional calendar load path:
|
134
|
+
More specifically you can check if a given `business_day?` is either a `working_day?` or a `holiday?` using methods on `Business::Calendar`.
|
71
135
|
|
72
136
|
```ruby
|
73
|
-
|
137
|
+
# Assuming "Monday, 9 June 2014" is a holiday
|
138
|
+
calendar.working_day?(Date.parse("Monday, 9 June 2014"))
|
139
|
+
# => true
|
140
|
+
calendar.holiday?(Date.parse("Monday, 9 June 2014"))
|
141
|
+
# => true
|
142
|
+
# Monday is a working day, but we have a holiday so it's not
|
143
|
+
# a business day
|
144
|
+
calendar.business_day?(Date.parse("Monday, 9 June 2014"))
|
145
|
+
# => false
|
74
146
|
```
|
75
147
|
|
76
|
-
|
148
|
+
## Business day arithmetic
|
77
149
|
|
78
|
-
|
79
|
-
|
80
|
-
The `add_business_days` and `subtract_business_days` are used to perform
|
81
|
-
business day arithmetic on dates.
|
150
|
+
The `add_business_days` and `subtract_business_days` are used to perform business day arithmetic on dates.
|
82
151
|
|
83
152
|
```ruby
|
84
153
|
date = Date.parse("Thursday, 12 June 2014")
|
@@ -88,10 +157,7 @@ calendar.subtract_business_days(date, 4).strftime("%A, %d %B %Y")
|
|
88
157
|
# => "Friday, 06 June 2014"
|
89
158
|
```
|
90
159
|
|
91
|
-
The `roll_forward` and `roll_backward` methods snap a date to a nearby business
|
92
|
-
day. If provided with a business day, they will return that date. Otherwise,
|
93
|
-
they will advance (forward for `roll_forward` and backward for `roll_backward`)
|
94
|
-
until a business day is found.
|
160
|
+
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.
|
95
161
|
|
96
162
|
```ruby
|
97
163
|
date = Date.parse("Saturday, 14 June 2014")
|
@@ -101,10 +167,7 @@ calendar.roll_backward(date).strftime("%A, %d %B %Y")
|
|
101
167
|
# => "Friday, 13 June 2014"
|
102
168
|
```
|
103
169
|
|
104
|
-
To count the number of business days between two dates, pass the dates to
|
105
|
-
`business_days_between`. This method counts from start of the first date to
|
106
|
-
start of the second date. So, assuming no holidays, there would be two business
|
107
|
-
days between a Monday and a Wednesday.
|
170
|
+
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.
|
108
171
|
|
109
172
|
```ruby
|
110
173
|
date = Date.parse("Saturday, 14 June 2014")
|
@@ -112,39 +175,19 @@ calendar.business_days_between(date, date + 7)
|
|
112
175
|
# => 5
|
113
176
|
```
|
114
177
|
|
115
|
-
### Included Calendars
|
116
|
-
|
117
|
-
We include some calendar data with this Gem but give no guarantees of its
|
118
|
-
accuracy.
|
119
|
-
The calendars that we include are:
|
120
|
-
|
121
|
-
* Bacs
|
122
|
-
* Bankgirot
|
123
|
-
* BECS (Australia)
|
124
|
-
* BECSNZ (New Zealand)
|
125
|
-
* PAD (Canada)
|
126
|
-
* Betalingsservice
|
127
|
-
* Target (SEPA)
|
128
|
-
* TargetFrance (SEPA + French bank holidays)
|
129
|
-
|
130
178
|
## But other libraries already do this
|
131
179
|
|
132
|
-
Another gem, [business_time](https://github.com/bokmann/business_time), also
|
133
|
-
|
134
|
-
|
180
|
+
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.
|
181
|
+
|
182
|
+
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
|
183
|
+
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.
|
135
184
|
|
136
|
-
|
137
|
-
While this enables syntax like `Time.now + 1.business_day`, it means that all
|
138
|
-
configuration has to be global. GoCardless handles payments across several
|
139
|
-
geographies, so being able to work with multiple working-day calendars is
|
140
|
-
essential for us. Business provides a simple `Calendar` class, that is
|
141
|
-
initialized with a configuration that specifies which days of the week are
|
142
|
-
considered to be working days, and which dates are holidays.
|
185
|
+
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.
|
143
186
|
|
144
|
-
|
145
|
-
our purposes, date-based calculations are sufficient. Supporting time-based
|
146
|
-
calculations as well makes the code significantly more complex. We chose to
|
147
|
-
avoid this extra complexity by sticking solely to date-based mathematics.
|
187
|
+
<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>
|
148
188
|
|
189
|
+
## License & Contributing
|
190
|
+
- business is available as open source under the terms of the [MIT License](LICENSE).
|
191
|
+
- Bug reports and pull requests are welcome on GitHub at https://github.com/gocardless/business.
|
149
192
|
|
150
|
-
|
193
|
+
GoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/about/jobs).
|
data/business.gemspec
CHANGED
@@ -1,22 +1,27 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path("lib", __dir__)
|
3
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
6
|
+
require "business/version"
|
5
7
|
|
6
8
|
Gem::Specification.new do |spec|
|
7
9
|
spec.name = "business"
|
8
10
|
spec.version = Business::VERSION
|
9
11
|
spec.authors = ["Harry Marr"]
|
10
12
|
spec.email = ["developers@gocardless.com"]
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
+
spec.summary = "Date calculations based on business calendars"
|
14
|
+
spec.description = "Date calculations based on business calendars"
|
13
15
|
spec.homepage = "https://github.com/gocardless/business"
|
14
16
|
spec.licenses = ["MIT"]
|
15
17
|
|
16
|
-
spec.files = `git ls-files`.split(
|
18
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
19
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
21
|
spec.require_paths = ["lib"]
|
20
22
|
|
23
|
+
spec.add_development_dependency "gc_ruboconfig", "~> 2.24.0"
|
21
24
|
spec.add_development_dependency "rspec", "~> 3.1"
|
25
|
+
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.1"
|
26
|
+
spec.add_development_dependency "rubocop", "~> 1.10.0"
|
22
27
|
end
|
data/lib/business.rb
CHANGED
data/lib/business/calendar.rb
CHANGED
@@ -1,63 +1,90 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "date"
|
2
5
|
|
3
6
|
module Business
|
4
7
|
class Calendar
|
8
|
+
VALID_KEYS = %w[holidays working_days extra_working_dates].freeze
|
9
|
+
|
5
10
|
class << self
|
6
|
-
attr_accessor :
|
11
|
+
attr_accessor :load_paths
|
7
12
|
end
|
8
13
|
|
9
14
|
def self.calendar_directories
|
10
|
-
|
11
|
-
directories + [File.join(File.dirname(__FILE__), 'data')]
|
15
|
+
@load_paths
|
12
16
|
end
|
13
17
|
private_class_method :calendar_directories
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
# rubocop:disable Metrics/MethodLength
|
20
|
+
def self.load(calendar_name)
|
21
|
+
data = find_calendar_data(calendar_name)
|
22
|
+
raise "No such calendar '#{calendar_name}'" unless data
|
23
|
+
|
24
|
+
unless (data.keys - VALID_KEYS).empty?
|
25
|
+
raise "Only valid keys are: #{VALID_KEYS.join(', ')}"
|
18
26
|
end
|
19
|
-
raise "No such calendar '#{calendar}'" unless directory
|
20
27
|
|
21
|
-
|
22
|
-
|
28
|
+
new(
|
29
|
+
name: calendar_name,
|
30
|
+
holidays: data["holidays"],
|
31
|
+
working_days: data["working_days"],
|
32
|
+
extra_working_dates: data["extra_working_dates"],
|
33
|
+
)
|
34
|
+
end
|
35
|
+
# rubocop:enable Metrics/MethodLength
|
23
36
|
|
24
|
-
|
25
|
-
|
26
|
-
|
37
|
+
def self.find_calendar_data(calendar_name)
|
38
|
+
calendar_directories.detect do |path|
|
39
|
+
if path.is_a?(Hash)
|
40
|
+
break path[calendar_name] if path[calendar_name]
|
41
|
+
else
|
42
|
+
next unless File.exist?(File.join(path, "#{calendar_name}.yml"))
|
27
43
|
|
28
|
-
|
29
|
-
|
44
|
+
break YAML.load_file(File.join(path, "#{calendar_name}.yml"))
|
45
|
+
end
|
46
|
+
end
|
30
47
|
end
|
31
48
|
|
32
49
|
@lock = Mutex.new
|
33
50
|
def self.load_cached(calendar)
|
34
51
|
@lock.synchronize do
|
35
|
-
@cache ||= {
|
36
|
-
unless @cache.include?(calendar)
|
37
|
-
@cache[calendar] = self.load(calendar)
|
38
|
-
end
|
52
|
+
@cache ||= {}
|
53
|
+
@cache[calendar] = self.load(calendar) unless @cache.include?(calendar)
|
39
54
|
@cache[calendar]
|
40
55
|
end
|
41
56
|
end
|
42
57
|
|
43
58
|
DAY_NAMES = %( mon tue wed thu fri sat sun )
|
44
59
|
|
45
|
-
attr_reader :holidays, :working_days, :extra_working_dates
|
60
|
+
attr_reader :name, :holidays, :working_days, :extra_working_dates
|
61
|
+
|
62
|
+
def initialize(name:, extra_working_dates: nil, working_days: nil, holidays: nil)
|
63
|
+
@name = name
|
64
|
+
set_extra_working_dates(extra_working_dates)
|
65
|
+
set_working_days(working_days)
|
66
|
+
set_holidays(holidays)
|
46
67
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
set_holidays(config[:holidays])
|
68
|
+
unless (@holidays & @extra_working_dates).none?
|
69
|
+
raise ArgumentError, "Holidays cannot be extra working dates"
|
70
|
+
end
|
51
71
|
end
|
52
72
|
|
53
73
|
# Return true if the date given is a business day (typically that means a
|
54
74
|
# non-weekend day) and not a holiday.
|
55
75
|
def business_day?(date)
|
56
76
|
date = date.to_date
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
77
|
+
working_day?(date) && !holiday?(date)
|
78
|
+
end
|
79
|
+
|
80
|
+
def working_day?(date)
|
81
|
+
date = date.to_date
|
82
|
+
extra_working_dates.include?(date) ||
|
83
|
+
working_days.include?(date.strftime("%a").downcase)
|
84
|
+
end
|
85
|
+
|
86
|
+
def holiday?(date)
|
87
|
+
holidays.include?(date.to_date)
|
61
88
|
end
|
62
89
|
|
63
90
|
# Roll forward to the next business day. If the date given is a business
|
@@ -79,19 +106,19 @@ module Business
|
|
79
106
|
# Roll forward to the next business day regardless of whether the given
|
80
107
|
# date is a business day or not.
|
81
108
|
def next_business_day(date)
|
82
|
-
|
109
|
+
loop do
|
83
110
|
date += day_interval_for(date)
|
84
|
-
|
85
|
-
|
111
|
+
break date if business_day?(date)
|
112
|
+
end
|
86
113
|
end
|
87
114
|
|
88
115
|
# Roll backward to the previous business day regardless of whether the given
|
89
116
|
# date is a business day or not.
|
90
117
|
def previous_business_day(date)
|
91
|
-
|
118
|
+
loop do
|
92
119
|
date -= day_interval_for(date)
|
93
|
-
|
94
|
-
|
120
|
+
break date if business_day?(date)
|
121
|
+
end
|
95
122
|
end
|
96
123
|
|
97
124
|
# Add a number of business days to a date. If a non-business day is given,
|
@@ -102,9 +129,10 @@ module Business
|
|
102
129
|
def add_business_days(date, delta)
|
103
130
|
date = roll_forward(date)
|
104
131
|
delta.times do
|
105
|
-
|
132
|
+
loop do
|
106
133
|
date += day_interval_for(date)
|
107
|
-
|
134
|
+
break date if business_day?(date)
|
135
|
+
end
|
108
136
|
end
|
109
137
|
date
|
110
138
|
end
|
@@ -117,9 +145,10 @@ module Business
|
|
117
145
|
def subtract_business_days(date, delta)
|
118
146
|
date = roll_backward(date)
|
119
147
|
delta.times do
|
120
|
-
|
148
|
+
loop do
|
121
149
|
date -= day_interval_for(date)
|
122
|
-
|
150
|
+
break date if business_day?(date)
|
151
|
+
end
|
123
152
|
end
|
124
153
|
date
|
125
154
|
end
|
@@ -127,8 +156,11 @@ module Business
|
|
127
156
|
# Count the number of business days between two dates.
|
128
157
|
# This method counts from start of date1 to start of date2. So,
|
129
158
|
# business_days_between(mon, weds) = 2 (assuming no holidays)
|
159
|
+
# rubocop:disable Metrics/AbcSize
|
160
|
+
# rubocop:disable Metrics/MethodLength
|
130
161
|
def business_days_between(date1, date2)
|
131
|
-
date1
|
162
|
+
date1 = date1.to_date
|
163
|
+
date2 = date2.to_date
|
132
164
|
|
133
165
|
# To optimise this method we split the range into full weeks and a
|
134
166
|
# remaining period.
|
@@ -150,14 +182,16 @@ module Business
|
|
150
182
|
num_biz_days -= holidays.count do |holiday|
|
151
183
|
in_range = full_weeks_range.cover?(holiday)
|
152
184
|
# Only pick a holiday if its on a working day (e.g., not a weekend)
|
153
|
-
on_biz_day = working_days.include?(holiday.strftime(
|
185
|
+
on_biz_day = working_days.include?(holiday.strftime("%a").downcase)
|
154
186
|
in_range && on_biz_day
|
155
187
|
end
|
156
188
|
|
157
|
-
remaining_range = (date2-remaining_days...date2)
|
189
|
+
remaining_range = (date2 - remaining_days...date2)
|
158
190
|
# Loop through each day in remaining_range and count if a business day
|
159
191
|
num_biz_days + remaining_range.count { |a| business_day?(a) }
|
160
192
|
end
|
193
|
+
# rubocop:enable Metrics/AbcSize
|
194
|
+
# rubocop:enable Metrics/MethodLength
|
161
195
|
|
162
196
|
def day_interval_for(date)
|
163
197
|
date.is_a?(Date) ? 1 : 3600 * 24
|
@@ -170,9 +204,12 @@ module Business
|
|
170
204
|
raise "Invalid day #{day}" unless DAY_NAMES.include?(normalised_day)
|
171
205
|
end
|
172
206
|
end
|
173
|
-
extra_working_dates_names = @extra_working_dates.map
|
207
|
+
extra_working_dates_names = @extra_working_dates.map do |d|
|
208
|
+
d.strftime("%a").downcase
|
209
|
+
end
|
174
210
|
return if (extra_working_dates_names & @working_days).none?
|
175
|
-
|
211
|
+
|
212
|
+
raise ArgumentError, "Extra working dates cannot be on working days"
|
176
213
|
end
|
177
214
|
|
178
215
|
def parse_dates(dates)
|
@@ -182,8 +219,6 @@ module Business
|
|
182
219
|
# Internal method for assigning holidays from a calendar config.
|
183
220
|
def set_holidays(holidays)
|
184
221
|
@holidays = parse_dates(holidays)
|
185
|
-
return if (@holidays & @extra_working_dates).none?
|
186
|
-
raise ArgumentError, 'Holidays cannot be extra working dates'
|
187
222
|
end
|
188
223
|
|
189
224
|
def set_extra_working_dates(extra_working_dates)
|
@@ -192,7 +227,7 @@ module Business
|
|
192
227
|
|
193
228
|
# If no working days are provided in the calendar config, these are used.
|
194
229
|
def default_working_days
|
195
|
-
%w
|
230
|
+
%w[mon tue wed thu fri]
|
196
231
|
end
|
197
232
|
end
|
198
233
|
end
|