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 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