business 1.17.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 269d15c8779ee827afd572090f68bdcacd07a2e370403b39eb2f57b7198d60ed
4
- data.tar.gz: 0ba04f3d70c319474a8f71d8f8f8930da4ad1ba7bf55943146a61f2104f26450
3
+ metadata.gz: 9a053f4e9b8b8fa5ed407b58d1e81155dac75f1f0c305410403011c608c69c60
4
+ data.tar.gz: 90cd733d6991516ae8c438824e669850d554147fbb47172f1c4f0dd0bdd44e37
5
5
  SHA512:
6
- metadata.gz: c096b7e12f640b5dec2a8477d4a67e08f1e36a89efeeb38e12ef9c1f31c5e10bd0d5bc3ef09a918f476e5b3da4cb46f195bf2e0f761ebe50a52a318c6fcc2bc5
7
- data.tar.gz: 34e96d59c5863db36b3ac58f26c5c744c44507ab463e8897fbd2213787ba27ade73363a7da5e9761c85a3f23d55718d0a1a61c08176024533a563e03fe1eb409
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
- ruby-2.5.1
1
+ 2.7.1
data/CHANGELOG.md CHANGED
@@ -1,4 +1,32 @@
1
- ## 1.17.0 - September 2, 2019
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
@@ -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
@@ -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
- [![Build status](https://travis-ci.org/gocardless/business.svg?branch=master)](https://travis-ci.org/gocardless/business)
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
- ## Documentation
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
- To get business, simply:
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
- $ gem install business
42
+ gem install business
14
43
  ```
15
44
 
16
- ### Getting started
45
+ If you are using a Gemfile:
17
46
 
18
- Get started with business by creating an instance of the calendar class,
19
- passing in a hash that specifies with days of the week are considered working
20
- days, and which days are holidays.
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
- `extra_working_dates` key makes the calendar to consider a weekend day as a working day.
63
+ ### Use a calendar file
30
64
 
31
- A few calendar configs are bundled with the gem (see [lib/business/data]((lib/business/data)) for
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
- ```ruby
37
- calendar = Business::Calendar.load("weekdays")
38
- calendar = Business::Calendar.load_cached("weekdays")
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
- If `working_days` is missing, then common default is used (mon-fri).
42
- If `holidays` is missing, "no holidays" assumed.
43
- If `extra_working_dates` is missing, then no changes in `working_days` will happen.
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
- Elements of `holidays` and `extra_working_dates` may be
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
- - 2017-01-08 # Same as January 8th, 2017
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
- ### Checking for business days
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
- ### Custom calendars
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
- Business::Calendar.additional_load_paths = ['path/to/your/calendar/directory']
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
- You can then load the calendar as normal.
148
+ ## Business day arithmetic
77
149
 
78
- ### Business day arithmetic
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
- exists for this purpose. We previously used business_time, but encountered
134
- several issues that prompted us to start business.
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
- Firstly, business_time works by monkey-patching `Date`, `Time`, and `FixNum`.
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
- Secondly, business_time supports calculations on times as well as dates. For
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
- ![I'm late for business](http://3.bp.blogspot.com/-aq4iOz2OZzs/Ty8xaQwMhtI/AAAAAAAABrM/-vn4tcRA9-4/s1600/daily-morning-awesomeness-243.jpeg)
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
- 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.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
@@ -1 +1,3 @@
1
- require 'business/calendar'
1
+ # frozen_string_literal: true
2
+
3
+ require "business/calendar"
@@ -1,63 +1,90 @@
1
- require 'yaml'
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 :additional_load_paths
11
+ attr_accessor :load_paths
7
12
  end
8
13
 
9
14
  def self.calendar_directories
10
- directories = @additional_load_paths || []
11
- directories + [File.join(File.dirname(__FILE__), 'data')]
15
+ @load_paths
12
16
  end
13
17
  private_class_method :calendar_directories
14
18
 
15
- def self.load(calendar)
16
- directory = calendar_directories.find do |dir|
17
- File.exists?(File.join(dir, "#{calendar}.yml"))
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
- yaml = YAML.load_file(File.join(directory, "#{calendar}.yml"))
22
- valid_keys = %w(holidays working_days extra_working_dates)
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
- unless (yaml.keys - valid_keys).empty?
25
- raise "Only valid keys are: #{valid_keys.join(', ')}"
26
- end
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
- self.new(holidays: yaml['holidays'], working_days: yaml['working_days'],
29
- extra_working_dates: yaml['extra_working_dates'])
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
- def initialize(config)
48
- set_extra_working_dates(config[:extra_working_dates])
49
- set_working_days(config[:working_days])
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
- return true if extra_working_dates.include?(date)
58
- return false unless working_days.include?(date.strftime('%a').downcase)
59
- return false if holidays.include?(date)
60
- 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)
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
- begin
109
+ loop do
83
110
  date += day_interval_for(date)
84
- end until business_day?(date)
85
- date
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
- begin
118
+ loop do
92
119
  date -= day_interval_for(date)
93
- end until business_day?(date)
94
- date
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
- begin
132
+ loop do
106
133
  date += day_interval_for(date)
107
- end until business_day?(date)
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
- begin
148
+ loop do
121
149
  date -= day_interval_for(date)
122
- end until business_day?(date)
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, date2 = date1.to_date, date2.to_date
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('%a').downcase)
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 { |d| d.strftime("%a").downcase }
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
- raise ArgumentError, 'Extra working dates cannot be on working days'
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( mon tue wed thu fri )
230
+ %w[mon tue wed thu fri]
196
231
  end
197
232
  end
198
233
  end