business 2.0.0 → 2.3.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 +5 -2
- data/.github/dependabot.yml +7 -0
- data/.rubocop.yml +16 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +19 -0
- data/Gemfile +2 -0
- data/README.md +19 -2
- data/business.gemspec +10 -6
- data/lib/business/calendar.rb +72 -50
- data/lib/business/version.rb +3 -1
- data/lib/business.rb +3 -1
- data/spec/{calendar_spec.rb → business/calendar_spec.rb} +280 -81
- data/spec/fixtures/calendars/ecb.yml +1 -1
- metadata +40 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b251f5b883fe268c312d1d85b129f20244ca8729a427784f8e5c0b87681ff646
|
4
|
+
data.tar.gz: '05779798d8c21739f31a604b6e46c063f62189b0a7e0a1064e674c1c5fbe9278'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d117a929340ee63c4978d6a0869e3e99fb585c002ac609a9d12d047aee7a96f75fc04f7a03bda894591348d23a3d3f43a32c1e33366d2bdb77d2ce9481c7e21
|
7
|
+
data.tar.gz: b2b9acd515306f0460e3e48aa89a0c0296b8bd9eebf9a4b51a4a3731fd94b7096ca521fd50db9ace4ba97bbcb7bb24e17cc6d38c488e6855bc8b34c1d0cd982f
|
data/.circleci/config.yml
CHANGED
@@ -3,7 +3,7 @@ version: 2.1
|
|
3
3
|
jobs:
|
4
4
|
test:
|
5
5
|
docker:
|
6
|
-
- image:
|
6
|
+
- image: cimg/ruby:<< parameters.ruby-version >>
|
7
7
|
parameters:
|
8
8
|
ruby-version:
|
9
9
|
type: string
|
@@ -23,6 +23,9 @@ jobs:
|
|
23
23
|
- run:
|
24
24
|
name: Run tests
|
25
25
|
command: bundle exec rspec
|
26
|
+
- run:
|
27
|
+
name: Run rubocop
|
28
|
+
command: bundle exec rubocop --parallel --extra-details --display-style-guide
|
26
29
|
|
27
30
|
workflows:
|
28
31
|
default:
|
@@ -31,4 +34,4 @@ workflows:
|
|
31
34
|
name: Ruby << matrix.ruby-version >>
|
32
35
|
matrix:
|
33
36
|
parameters:
|
34
|
-
ruby-version: ["2.
|
37
|
+
ruby-version: ["2.6.7", "2.7.3", "3.0.1", "3.1.0"]
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,16 @@
|
|
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
|
14
|
+
|
15
|
+
Style/HashSyntax:
|
16
|
+
Enabled: false
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.1.0
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
## Upcoming
|
2
|
+
|
3
|
+
## 2.3.0 - Jan 31, 2022
|
4
|
+
|
5
|
+
- Added permitted classes to YAML's `safe_load` #112 - thanks @attack
|
6
|
+
- Removed support for Ruby 2.4, 2.5
|
7
|
+
|
8
|
+
## 2.2.1 - March 9, 2021
|
9
|
+
|
10
|
+
- Fix regression on `Calendar#new` #83 - thanks @ineu!
|
11
|
+
|
12
|
+
## 2.2.0 - March 4, 2021
|
13
|
+
|
14
|
+
- Add `Business::Calendar#name` - thanks @mattmcf!
|
15
|
+
|
16
|
+
## 2.1.0 - June 8, 2020
|
17
|
+
|
18
|
+
- Add seperate `working_day?` and `holiday?` methods to the calendar
|
19
|
+
|
1
20
|
## 2.0.0 - May 4, 2020
|
2
21
|
|
3
22
|
🚨 **BREAKING CHANGES** 🚨
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -23,7 +23,7 @@ We have removed the bundled calendars as of version 2.0.0, if you need the calen
|
|
23
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
24
|
|
25
25
|
```ruby
|
26
|
-
Business::Calendar.load_paths
|
26
|
+
Business::Calendar.load_paths = ["lib/calendars"] # your_project/lib/calendars/ contains bacs.yml
|
27
27
|
Business::Calendar.load("bacs")
|
28
28
|
```
|
29
29
|
|
@@ -52,10 +52,13 @@ gem "business", "~> 2.0"
|
|
52
52
|
|
53
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.
|
54
54
|
|
55
|
+
Additionally each calendar instance can be given a name. This can come in handy if you use multiple calendars.
|
56
|
+
|
55
57
|
```ruby
|
56
58
|
calendar = Business::Calendar.new(
|
59
|
+
name: 'my calendar',
|
57
60
|
working_days: %w( mon tue wed thu fri ),
|
58
|
-
holidays: ["01/01/2014", "03/01/2014"] # array items are either parseable date strings, or real Date objects
|
61
|
+
holidays: ["01/01/2014", "03/01/2014"], # array items are either parseable date strings, or real Date objects
|
59
62
|
extra_working_dates: [nil], # Makes the calendar to consider a weekend day as a working day.
|
60
63
|
)
|
61
64
|
```
|
@@ -131,6 +134,20 @@ calendar.business_day?(Date.parse("Sunday, 8 June 2014"))
|
|
131
134
|
# => false
|
132
135
|
```
|
133
136
|
|
137
|
+
More specifically you can check if a given `business_day?` is either a `working_day?` or a `holiday?` using methods on `Business::Calendar`.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# Assuming "Monday, 9 June 2014" is a holiday
|
141
|
+
calendar.working_day?(Date.parse("Monday, 9 June 2014"))
|
142
|
+
# => true
|
143
|
+
calendar.holiday?(Date.parse("Monday, 9 June 2014"))
|
144
|
+
# => true
|
145
|
+
# Monday is a working day, but we have a holiday so it's not
|
146
|
+
# a business day
|
147
|
+
calendar.business_day?(Date.parse("Monday, 9 June 2014"))
|
148
|
+
# => false
|
149
|
+
```
|
150
|
+
|
134
151
|
## Business day arithmetic
|
135
152
|
|
136
153
|
The `add_business_days` and `subtract_business_days` are used to perform business day arithmetic on dates.
|
data/business.gemspec
CHANGED
@@ -1,23 +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.31.0"
|
21
24
|
spec.add_development_dependency "rspec", "~> 3.1"
|
22
|
-
spec.add_development_dependency "rspec_junit_formatter", "~> 0.
|
25
|
+
spec.add_development_dependency "rspec_junit_formatter", "~> 0.5.1"
|
26
|
+
spec.add_development_dependency "rubocop", "~> 1.25.0"
|
23
27
|
end
|
data/lib/business/calendar.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "date"
|
5
|
+
require "pathname"
|
3
6
|
|
4
7
|
module Business
|
5
8
|
class Calendar
|
9
|
+
VALID_KEYS = %w[holidays working_days extra_working_dates].freeze
|
10
|
+
|
6
11
|
class << self
|
7
12
|
attr_accessor :load_paths
|
8
13
|
end
|
@@ -13,60 +18,73 @@ module Business
|
|
13
18
|
private_class_method :calendar_directories
|
14
19
|
|
15
20
|
def self.load(calendar_name)
|
16
|
-
data =
|
17
|
-
|
18
|
-
break path[calendar_name] if path[calendar_name]
|
19
|
-
else
|
20
|
-
next unless File.exists?(File.join(path, "#{calendar_name}.yml"))
|
21
|
+
data = find_calendar_data(calendar_name)
|
22
|
+
raise "No such calendar '#{calendar_name}'" unless data
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
+
unless (data.keys - VALID_KEYS).empty?
|
25
|
+
raise "Only valid keys are: #{VALID_KEYS.join(', ')}"
|
24
26
|
end
|
25
27
|
|
26
|
-
|
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
|
27
35
|
|
28
|
-
|
36
|
+
def self.find_calendar_data(calendar_name)
|
37
|
+
calendar_directories.detect do |path|
|
38
|
+
if path.is_a?(Hash)
|
39
|
+
break path[calendar_name] if path[calendar_name]
|
40
|
+
else
|
41
|
+
calendar_path = Pathname.new(path).join("#{calendar_name}.yml")
|
42
|
+
next unless calendar_path.exist?
|
29
43
|
|
30
|
-
|
31
|
-
|
44
|
+
break YAML.safe_load(calendar_path.read, permitted_classes: [Date])
|
45
|
+
end
|
32
46
|
end
|
33
|
-
|
34
|
-
self.new(
|
35
|
-
holidays: data['holidays'],
|
36
|
-
working_days: data['working_days'],
|
37
|
-
extra_working_dates: data['extra_working_dates'],
|
38
|
-
)
|
39
47
|
end
|
40
48
|
|
41
49
|
@lock = Mutex.new
|
42
50
|
def self.load_cached(calendar)
|
43
51
|
@lock.synchronize do
|
44
|
-
@cache ||= {
|
45
|
-
unless @cache.include?(calendar)
|
46
|
-
@cache[calendar] = self.load(calendar)
|
47
|
-
end
|
52
|
+
@cache ||= {}
|
53
|
+
@cache[calendar] = self.load(calendar) unless @cache.include?(calendar)
|
48
54
|
@cache[calendar]
|
49
55
|
end
|
50
56
|
end
|
51
57
|
|
52
58
|
DAY_NAMES = %( mon tue wed thu fri sat sun )
|
53
59
|
|
54
|
-
attr_reader :holidays, :working_days, :extra_working_dates
|
60
|
+
attr_reader :name, :holidays, :working_days, :extra_working_dates
|
61
|
+
|
62
|
+
def initialize(name: nil, 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)
|
55
67
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
set_holidays(config[:holidays])
|
68
|
+
unless (@holidays & @extra_working_dates).none?
|
69
|
+
raise ArgumentError, "Holidays cannot be extra working dates"
|
70
|
+
end
|
60
71
|
end
|
61
72
|
|
62
73
|
# Return true if the date given is a business day (typically that means a
|
63
74
|
# non-weekend day) and not a holiday.
|
64
75
|
def business_day?(date)
|
65
76
|
date = date.to_date
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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)
|
70
88
|
end
|
71
89
|
|
72
90
|
# Roll forward to the next business day. If the date given is a business
|
@@ -88,19 +106,19 @@ module Business
|
|
88
106
|
# Roll forward to the next business day regardless of whether the given
|
89
107
|
# date is a business day or not.
|
90
108
|
def next_business_day(date)
|
91
|
-
|
109
|
+
loop do
|
92
110
|
date += day_interval_for(date)
|
93
|
-
|
94
|
-
|
111
|
+
break date if business_day?(date)
|
112
|
+
end
|
95
113
|
end
|
96
114
|
|
97
115
|
# Roll backward to the previous business day regardless of whether the given
|
98
116
|
# date is a business day or not.
|
99
117
|
def previous_business_day(date)
|
100
|
-
|
118
|
+
loop do
|
101
119
|
date -= day_interval_for(date)
|
102
|
-
|
103
|
-
|
120
|
+
break date if business_day?(date)
|
121
|
+
end
|
104
122
|
end
|
105
123
|
|
106
124
|
# Add a number of business days to a date. If a non-business day is given,
|
@@ -111,9 +129,10 @@ module Business
|
|
111
129
|
def add_business_days(date, delta)
|
112
130
|
date = roll_forward(date)
|
113
131
|
delta.times do
|
114
|
-
|
132
|
+
loop do
|
115
133
|
date += day_interval_for(date)
|
116
|
-
|
134
|
+
break date if business_day?(date)
|
135
|
+
end
|
117
136
|
end
|
118
137
|
date
|
119
138
|
end
|
@@ -126,9 +145,10 @@ module Business
|
|
126
145
|
def subtract_business_days(date, delta)
|
127
146
|
date = roll_backward(date)
|
128
147
|
delta.times do
|
129
|
-
|
148
|
+
loop do
|
130
149
|
date -= day_interval_for(date)
|
131
|
-
|
150
|
+
break date if business_day?(date)
|
151
|
+
end
|
132
152
|
end
|
133
153
|
date
|
134
154
|
end
|
@@ -137,7 +157,8 @@ module Business
|
|
137
157
|
# This method counts from start of date1 to start of date2. So,
|
138
158
|
# business_days_between(mon, weds) = 2 (assuming no holidays)
|
139
159
|
def business_days_between(date1, date2)
|
140
|
-
date1
|
160
|
+
date1 = date1.to_date
|
161
|
+
date2 = date2.to_date
|
141
162
|
|
142
163
|
# To optimise this method we split the range into full weeks and a
|
143
164
|
# remaining period.
|
@@ -159,11 +180,11 @@ module Business
|
|
159
180
|
num_biz_days -= holidays.count do |holiday|
|
160
181
|
in_range = full_weeks_range.cover?(holiday)
|
161
182
|
# Only pick a holiday if its on a working day (e.g., not a weekend)
|
162
|
-
on_biz_day = working_days.include?(holiday.strftime(
|
183
|
+
on_biz_day = working_days.include?(holiday.strftime("%a").downcase)
|
163
184
|
in_range && on_biz_day
|
164
185
|
end
|
165
186
|
|
166
|
-
remaining_range = (date2-remaining_days...date2)
|
187
|
+
remaining_range = (date2 - remaining_days...date2)
|
167
188
|
# Loop through each day in remaining_range and count if a business day
|
168
189
|
num_biz_days + remaining_range.count { |a| business_day?(a) }
|
169
190
|
end
|
@@ -179,9 +200,12 @@ module Business
|
|
179
200
|
raise "Invalid day #{day}" unless DAY_NAMES.include?(normalised_day)
|
180
201
|
end
|
181
202
|
end
|
182
|
-
extra_working_dates_names = @extra_working_dates.map
|
203
|
+
extra_working_dates_names = @extra_working_dates.map do |d|
|
204
|
+
d.strftime("%a").downcase
|
205
|
+
end
|
183
206
|
return if (extra_working_dates_names & @working_days).none?
|
184
|
-
|
207
|
+
|
208
|
+
raise ArgumentError, "Extra working dates cannot be on working days"
|
185
209
|
end
|
186
210
|
|
187
211
|
def parse_dates(dates)
|
@@ -191,8 +215,6 @@ module Business
|
|
191
215
|
# Internal method for assigning holidays from a calendar config.
|
192
216
|
def set_holidays(holidays)
|
193
217
|
@holidays = parse_dates(holidays)
|
194
|
-
return if (@holidays & @extra_working_dates).none?
|
195
|
-
raise ArgumentError, 'Holidays cannot be extra working dates'
|
196
218
|
end
|
197
219
|
|
198
220
|
def set_extra_working_dates(extra_working_dates)
|
@@ -201,7 +223,7 @@ module Business
|
|
201
223
|
|
202
224
|
# If no working days are provided in the calendar config, these are used.
|
203
225
|
def default_working_days
|
204
|
-
%w
|
226
|
+
%w[mon tue wed thu fri]
|
205
227
|
end
|
206
228
|
end
|
207
229
|
end
|
data/lib/business/version.rb
CHANGED
data/lib/business.rb
CHANGED