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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31ba206f257f17ef5bdfa3c60e9e81f2e5e2e71436a701513643f90e783d844d
4
- data.tar.gz: bda97edfe037b2f509b4d037d46a39b89c5ed96130872c6e090da461c6783418
3
+ metadata.gz: b251f5b883fe268c312d1d85b129f20244ca8729a427784f8e5c0b87681ff646
4
+ data.tar.gz: '05779798d8c21739f31a604b6e46c063f62189b0a7e0a1064e674c1c5fbe9278'
5
5
  SHA512:
6
- metadata.gz: 07576b028c3b7c31fd5be9fc1c1588b31b76b629d6381c73566a828db6add04ee47f7f24091274ef55c0441666551922a35b97ccbb112224fe85d3bf24865dd0
7
- data.tar.gz: 81aae610dd327487aa95b11b04b12d5d909f3296c36a2d49ff9147dc1a2276b4e024fffe2fe5cb8fa072a60b2a667d7b5f0cf207e919e2bc796d6693d01a731a
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: circleci/ruby:<< parameters.ruby-version >>
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.4.9", "2.5.8", "2.6.6", "2.7.1"]
37
+ ruby-version: ["2.6.7", "2.7.3", "3.0.1", "3.1.0"]
@@ -0,0 +1,7 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ open-pull-requests-limit: 10
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
- ruby-2.5.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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
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("lib/calendars") # your_project/lib/calendars/ contains bacs.yml
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
- lib = File.expand_path('../lib', __FILE__)
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 'business/version'
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 = %q{Date calculations based on business calendars}
12
- spec.description = %q{Date calculations based on business calendars}
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.4.1"
25
+ spec.add_development_dependency "rspec_junit_formatter", "~> 0.5.1"
26
+ spec.add_development_dependency "rubocop", "~> 1.25.0"
23
27
  end
@@ -1,8 +1,13 @@
1
- require 'yaml'
2
- require 'date'
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 = calendar_directories.detect do |path|
17
- if path.is_a?(Hash)
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
- break YAML.load_file(File.join(path, "#{calendar_name}.yml"))
23
- end
24
+ unless (data.keys - VALID_KEYS).empty?
25
+ raise "Only valid keys are: #{VALID_KEYS.join(', ')}"
24
26
  end
25
27
 
26
- raise "No such calendar '#{calendar_name}'" unless data
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
- valid_keys = %w(holidays working_days extra_working_dates)
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
- unless (data.keys - valid_keys).empty?
31
- raise "Only valid keys are: #{valid_keys.join(', ')}"
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
- def initialize(config)
57
- set_extra_working_dates(config[:extra_working_dates])
58
- set_working_days(config[:working_days])
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
- return true if extra_working_dates.include?(date)
67
- return false unless working_days.include?(date.strftime('%a').downcase)
68
- return false if holidays.include?(date)
69
- true
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
- begin
109
+ loop do
92
110
  date += day_interval_for(date)
93
- end until business_day?(date)
94
- date
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
- begin
118
+ loop do
101
119
  date -= day_interval_for(date)
102
- end until business_day?(date)
103
- date
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
- begin
132
+ loop do
115
133
  date += day_interval_for(date)
116
- end until business_day?(date)
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
- begin
148
+ loop do
130
149
  date -= day_interval_for(date)
131
- end until business_day?(date)
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, date2 = date1.to_date, date2.to_date
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('%a').downcase)
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 { |d| d.strftime("%a").downcase }
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
- raise ArgumentError, 'Extra working dates cannot be on working days'
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( mon tue wed thu fri )
226
+ %w[mon tue wed thu fri]
205
227
  end
206
228
  end
207
229
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Business
2
- VERSION = "2.0.0"
4
+ VERSION = "2.3.0"
3
5
  end
data/lib/business.rb CHANGED
@@ -1 +1,3 @@
1
- require 'business/calendar'
1
+ # frozen_string_literal: true
2
+
3
+ require "business/calendar"