business 2.3.0 → 3.1.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: b251f5b883fe268c312d1d85b129f20244ca8729a427784f8e5c0b87681ff646
4
- data.tar.gz: '05779798d8c21739f31a604b6e46c063f62189b0a7e0a1064e674c1c5fbe9278'
3
+ metadata.gz: 9f04f06e8df7909e07f17eb5ef5066935eea815e7139260bc1542d785f999d3b
4
+ data.tar.gz: 68df07bcc51b18a1406badfaeeeb78c05412d726a506be589a7bdbd139105877
5
5
  SHA512:
6
- metadata.gz: 7d117a929340ee63c4978d6a0869e3e99fb585c002ac609a9d12d047aee7a96f75fc04f7a03bda894591348d23a3d3f43a32c1e33366d2bdb77d2ce9481c7e21
7
- data.tar.gz: b2b9acd515306f0460e3e48aa89a0c0296b8bd9eebf9a4b51a4a3731fd94b7096ca521fd50db9ace4ba97bbcb7bb24e17cc6d38c488e6855bc8b34c1d0cd982f
6
+ metadata.gz: 95d2bf588e43841cc35e48e416d610466a18738a777c812d7c7dff1d121d715d270247e5a2895753c3bc1742400f58e924a626dbe152e0d0ed5a9803241070d4
7
+ data.tar.gz: c7e583aba5f2870a3ddb0ec5e91ef6e64e03dfb5cf5de1a6d246bea345ef9cccb3304dc6d21edb3f5508ae62646b1f7dd8330578a1457b96eaf3bd46fa2a6faa
@@ -0,0 +1,36 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "master"
7
+ pull_request:
8
+
9
+ jobs:
10
+ rubocop:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v7
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ bundler-cache: true
18
+ # Maintain consistency with the TargetRubyVersion in .rubocop.yml
19
+ ruby-version: "3.3"
20
+ - name: Run rubocop
21
+ run: bundle exec rubocop --parallel --extra-details --display-style-guide
22
+
23
+ tests:
24
+ strategy:
25
+ matrix:
26
+ ruby-version: ["3.3", "3.4", "4.0"]
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v7
30
+ - name: Set up Ruby
31
+ uses: ruby/setup-ruby@v1
32
+ with:
33
+ bundler-cache: true
34
+ ruby-version: "${{ matrix.ruby-version }}"
35
+ - name: Run tests
36
+ run: bundle exec rspec
data/.rubocop.yml CHANGED
@@ -1,6 +1,10 @@
1
1
  inherit_gem:
2
2
  gc_ruboconfig: rubocop.yml
3
3
 
4
+ AllCops:
5
+ TargetRubyVersion: 3.3
6
+ NewCops: enable
7
+
4
8
  RSpec/NestedGroups:
5
9
  Exclude:
6
10
  - spec/business/calendar_spec.rb
@@ -12,5 +16,5 @@ Naming/AccessorMethodName:
12
16
  Gemspec/RequiredRubyVersion:
13
17
  Enabled: false
14
18
 
15
- Style/HashSyntax:
19
+ RSpec/IndexedLet:
16
20
  Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.1.0
1
+ 4.0.6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## Upcoming
2
2
 
3
+ ## 3.1.0 - Sept 28, 2026
4
+
5
+ - Fix `business_days_between` not counting extra working dates in full weeks #34 - thanks @anatoliliotych!
6
+
7
+ # 3.0.0 - Sept 29, 2026
8
+
9
+ - Removed support for Ruby 3.2 and earlier
10
+ - Prevent path traversal when loading calendar files #331
11
+
3
12
  ## 2.3.0 - Jan 31, 2022
4
13
 
5
14
  - Added permitted classes to YAML's `safe_load` #112 - thanks @attack
data/COMPATIBILITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Compatibility
2
+
3
+ ## v2.0.0 breaking changes
4
+
5
+ We have removed the bundled calendars as of version 2.0.0, if you need the calendars that were included:
6
+
7
+ - Download the calendars you wish to use from [v1.18.0](https://github.com/gocardless/business/tree/b12c186ca6fd4ffdac85175742ff7e4d0a705ef4/lib/business/data)
8
+ - Place them in a suitable directory in your project, typically `lib/calendars`
9
+ - 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
10
+
11
+ ```ruby
12
+ Business::Calendar.load_paths = ["lib/calendars"] # your_project/lib/calendars/ contains bacs.yml
13
+ Business::Calendar.load("bacs")
14
+ ```
15
+
16
+ If you wish to stay on the last version that contained bundled calendars, pin `business` to `v1.18.0`
17
+
18
+ ```ruby
19
+ # Gemfile
20
+ gem "business", "v1.18.0"
21
+ ```
data/Gemfile CHANGED
@@ -3,3 +3,9 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
+
7
+ group :test, :development do
8
+ gem "gc_ruboconfig", "~> 6"
9
+ gem "rspec", "~> 3.4"
10
+ gem "rubocop", "~> 1.91"
11
+ end
data/README.md CHANGED
@@ -1,39 +1,18 @@
1
1
  # Business
2
2
 
3
3
  [![Gem version](https://badge.fury.io/rb/business.svg)](http://badge.fury.io/rb/business)
4
- [![CircleCI](https://circleci.com/gh/gocardless/business.svg?style=svg)](https://circleci.com/gh/gocardless/business)
5
4
 
6
5
  Date calculations based on business calendars.
7
6
 
8
- - [v2.0.0 breaking changes](#v200-breaking-changes)
9
7
  - [Getting Started](#getting-started)
10
8
  - [Creating a calendar](#creating-a-calendar)
11
9
  - [Using a calendar file](#use-a-calendar-file)
12
10
  - [Checking for business days](#checking-for-business-days)
13
11
  - [Business day arithmetic](#business-day-arithmetic)
14
12
  - [But other libraries already do this](#but-other-libraries-already-do-this)
13
+ - [Compatibility](#compatibility)
15
14
  - [License & Contributing](#license--contributing)
16
15
 
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
16
  ## Getting started
38
17
 
39
18
  To install business, simply:
@@ -45,7 +24,7 @@ gem install business
45
24
  If you are using a Gemfile:
46
25
 
47
26
  ```ruby
48
- gem "business", "~> 2.0"
27
+ gem "business", "~> 3.0"
49
28
  ```
50
29
 
51
30
  ### Creating a calendar
@@ -67,7 +46,7 @@ calendar = Business::Calendar.new(
67
46
 
68
47
  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:
69
48
 
70
- 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].
49
+ 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).
71
50
 
72
51
  #### YAML file Structure
73
52
 
@@ -160,6 +139,16 @@ calendar.subtract_business_days(date, 4).strftime("%A, %d %B %Y")
160
139
  # => "Friday, 06 June 2014"
161
140
  ```
162
141
 
142
+ For getting the next or previous business day, instead of passing `1` to `add_business_days` or `subtract_business_days`, `next_business_day` and `previous_business_day` could be used respectively:
143
+
144
+ ```ruby
145
+ date = Date.parse("Friday, 13 June 2014")
146
+ calendar.next_business_day(date).strftime("%A, %d %B %Y")
147
+ # => "Monday, 16 June 2014"
148
+ calendar.previous_business_day(date).strftime("%A, %d %B %Y")
149
+ # => "Thursday, 12 June 2014"
150
+ ```
151
+
163
152
  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.
164
153
 
165
154
  ```ruby
@@ -189,6 +178,12 @@ Secondly, business_time supports calculations on times as well as dates. For our
189
178
 
190
179
  <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>
191
180
 
181
+ # Compatibility
182
+
183
+ ## v2.0.0 Breaking Changes
184
+
185
+ - See [COMPATIBILITY.md](COMPATIBILITY.md#v200-breaking-changes)
186
+
192
187
  ## License & Contributing
193
188
  - business is available as open source under the terms of the [MIT License](LICENSE).
194
189
  - Bug reports and pull requests are welcome on GitHub at https://github.com/gocardless/business.
data/business.gemspec CHANGED
@@ -8,7 +8,7 @@ require "business/version"
8
8
  Gem::Specification.new do |spec|
9
9
  spec.name = "business"
10
10
  spec.version = Business::VERSION
11
- spec.authors = ["Harry Marr"]
11
+ spec.authors = ["GoCcardless"]
12
12
  spec.email = ["developers@gocardless.com"]
13
13
  spec.summary = "Date calculations based on business calendars"
14
14
  spec.description = "Date calculations based on business calendars"
@@ -16,12 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.licenses = ["MIT"]
17
17
 
18
18
  spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
19
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
19
  spec.require_paths = ["lib"]
22
20
 
23
- spec.add_development_dependency "gc_ruboconfig", "~> 2.31.0"
24
- spec.add_development_dependency "rspec", "~> 3.1"
25
- spec.add_development_dependency "rspec_junit_formatter", "~> 0.5.1"
26
- spec.add_development_dependency "rubocop", "~> 1.25.0"
21
+ spec.metadata["rubygems_mfa_required"] = "true"
27
22
  end
@@ -8,6 +8,11 @@ module Business
8
8
  class Calendar
9
9
  VALID_KEYS = %w[holidays working_days extra_working_dates].freeze
10
10
 
11
+ # Calendar names are interpolated into a filesystem path, so restrict them to
12
+ # plain identifiers: anything containing path separators, "..", or an absolute
13
+ # path would otherwise load a file from outside the configured load_paths.
14
+ CALENDAR_NAME_FORMAT = /\A[a-zA-Z0-9_-]+\z/
15
+
11
16
  class << self
12
17
  attr_accessor :load_paths
13
18
  end
@@ -38,7 +43,7 @@ module Business
38
43
  if path.is_a?(Hash)
39
44
  break path[calendar_name] if path[calendar_name]
40
45
  else
41
- calendar_path = Pathname.new(path).join("#{calendar_name}.yml")
46
+ calendar_path = calendar_path_for(path, calendar_name)
42
47
  next unless calendar_path.exist?
43
48
 
44
49
  break YAML.safe_load(calendar_path.read, permitted_classes: [Date])
@@ -46,6 +51,17 @@ module Business
46
51
  end
47
52
  end
48
53
 
54
+ # Only applied to directory load_paths; hash load_paths are a plain key
55
+ # lookup and cannot escape anywhere, so their keys stay unrestricted.
56
+ def self.calendar_path_for(directory, calendar_name)
57
+ unless calendar_name.to_s.match?(CALENDAR_NAME_FORMAT)
58
+ raise ArgumentError, "invalid calendar name: #{calendar_name.inspect}"
59
+ end
60
+
61
+ Pathname.new(directory).join("#{calendar_name}.yml")
62
+ end
63
+ private_class_method :calendar_path_for
64
+
49
65
  @lock = Mutex.new
50
66
  def self.load_cached(calendar)
51
67
  @lock.synchronize do
@@ -65,7 +81,7 @@ module Business
65
81
  set_working_days(working_days)
66
82
  set_holidays(holidays)
67
83
 
68
- unless (@holidays & @extra_working_dates).none?
84
+ if @holidays.intersect?(@extra_working_dates)
69
85
  raise ArgumentError, "Holidays cannot be extra working dates"
70
86
  end
71
87
  end
@@ -129,10 +145,7 @@ module Business
129
145
  def add_business_days(date, delta)
130
146
  date = roll_forward(date)
131
147
  delta.times do
132
- loop do
133
- date += day_interval_for(date)
134
- break date if business_day?(date)
135
- end
148
+ date = next_business_day(date)
136
149
  end
137
150
  date
138
151
  end
@@ -145,10 +158,7 @@ module Business
145
158
  def subtract_business_days(date, delta)
146
159
  date = roll_backward(date)
147
160
  delta.times do
148
- loop do
149
- date -= day_interval_for(date)
150
- break date if business_day?(date)
151
- end
161
+ date = previous_business_day(date)
152
162
  end
153
163
  date
154
164
  end
@@ -184,7 +194,16 @@ module Business
184
194
  in_range && on_biz_day
185
195
  end
186
196
 
187
- remaining_range = (date2 - remaining_days...date2)
197
+ num_biz_days += extra_working_dates.count do |extra_day|
198
+ in_range = full_weeks_range.cover?(extra_day)
199
+ # Only pick an extra working date if it's on a non-working day (e.g., a
200
+ # weekend) and not a holiday, as the setters bypass that validation
201
+ on_non_working_day =
202
+ !working_days.include?(extra_day.strftime("%a").downcase)
203
+ in_range && on_non_working_day && !holiday?(extra_day)
204
+ end
205
+
206
+ remaining_range = ((date2 - remaining_days)...date2)
188
207
  # Loop through each day in remaining_range and count if a business day
189
208
  num_biz_days + remaining_range.count { |a| business_day?(a) }
190
209
  end
@@ -203,7 +222,7 @@ module Business
203
222
  extra_working_dates_names = @extra_working_dates.map do |d|
204
223
  d.strftime("%a").downcase
205
224
  end
206
- return if (extra_working_dates_names & @working_days).none?
225
+ return if !extra_working_dates_names.intersect?(@working_days)
207
226
 
208
227
  raise ArgumentError, "Extra working dates cannot be on working days"
209
228
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Business
4
- VERSION = "2.3.0"
4
+ VERSION = "3.1.0"
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "business/calendar"
4
4
  require "time"
5
+ require "fileutils"
5
6
 
6
7
  RSpec.configure do |config|
7
8
  config.mock_with(:rspec) { |mocks| mocks.verify_partial_doubles = true }
@@ -57,6 +58,71 @@ RSpec.describe Business::Calendar do
57
58
  specify { expect { load_calendar }.to raise_error(/No such calendar/) }
58
59
  end
59
60
 
61
+ context "when given a calendar name that escapes the load path" do
62
+ before do
63
+ outside = File.join(File.dirname(__FILE__), "../fixtures")
64
+ FileUtils.mkdir_p(File.join(outside, "outside"))
65
+ File.write(
66
+ File.join(outside, "outside", "escaped.yml"),
67
+ { "working_days" => ["monday"] }.to_yaml,
68
+ )
69
+ end
70
+
71
+ after do
72
+ FileUtils.rm_rf(File.join(File.dirname(__FILE__), "../fixtures", "outside"))
73
+ end
74
+
75
+ context "with a relative traversal" do
76
+ let(:calendar) { "../outside/escaped" }
77
+
78
+ specify do
79
+ expect { load_calendar }.
80
+ to raise_error(ArgumentError, /invalid calendar name/)
81
+ end
82
+ end
83
+
84
+ context "with an absolute path" do
85
+ let(:calendar) do
86
+ File.join(File.dirname(__FILE__), "../fixtures", "outside", "escaped")
87
+ end
88
+
89
+ specify do
90
+ expect { load_calendar }.
91
+ to raise_error(ArgumentError, /invalid calendar name/)
92
+ end
93
+ end
94
+
95
+ context "with a bare path separator" do
96
+ let(:calendar) { "outside/escaped" }
97
+
98
+ specify do
99
+ expect { load_calendar }.
100
+ to raise_error(ArgumentError, /invalid calendar name/)
101
+ end
102
+ end
103
+
104
+ context "with a name that is only dots" do
105
+ let(:calendar) { ".." }
106
+
107
+ specify do
108
+ expect { load_calendar }.
109
+ to raise_error(ArgumentError, /invalid calendar name/)
110
+ end
111
+ end
112
+
113
+ context "when the escaping name is a key in a hash load path" do
114
+ before do
115
+ described_class.load_paths = [{ "../outside/escaped" => dummy_calendar }]
116
+ end
117
+
118
+ let(:calendar) { "../outside/escaped" }
119
+
120
+ it "still resolves, since hash lookups touch no filesystem" do
121
+ expect(load_calendar).to be_a described_class
122
+ end
123
+ end
124
+ end
125
+
60
126
  context "when given a calendar that has invalid keys" do
61
127
  let(:calendar) { "invalid-keys" }
62
128
 
@@ -340,7 +406,7 @@ RSpec.describe Business::Calendar do
340
406
  context "followed by another non-business day" do
341
407
  let(:date) { date_class.parse("Saturday 5th Jan, 2013") }
342
408
 
343
- it { is_expected.to eq(date + 2 * day_interval) }
409
+ it { is_expected.to eq(date + (2 * day_interval)) }
344
410
  end
345
411
  end
346
412
  end
@@ -368,7 +434,7 @@ RSpec.describe Business::Calendar do
368
434
  context "preceeded by another non-business day" do
369
435
  let(:date) { date_class.parse("Sunday 6th Jan, 2013") }
370
436
 
371
- it { is_expected.to eq(date - 2 * day_interval) }
437
+ it { is_expected.to eq(date - (2 * day_interval)) }
372
438
  end
373
439
  end
374
440
  end
@@ -396,7 +462,7 @@ RSpec.describe Business::Calendar do
396
462
  context "followed by another non-business day" do
397
463
  let(:date) { date_class.parse("Saturday 5th Jan, 2013") }
398
464
 
399
- it { is_expected.to eq(date + 2 * day_interval) }
465
+ it { is_expected.to eq(date + (2 * day_interval)) }
400
466
  end
401
467
  end
402
468
  end
@@ -424,7 +490,7 @@ RSpec.describe Business::Calendar do
424
490
  context "preceeded by another non-business day" do
425
491
  let(:date) { date_class.parse("Sunday 6th Jan, 2013") }
426
492
 
427
- it { is_expected.to eq(date - 2 * day_interval) }
493
+ it { is_expected.to eq(date - (2 * day_interval)) }
428
494
  end
429
495
  end
430
496
  end
@@ -444,33 +510,33 @@ RSpec.describe Business::Calendar do
444
510
  context "and a period that includes only business days" do
445
511
  let(:date) { date_class.parse("Wednesday 2nd Jan, 2013") }
446
512
 
447
- it { is_expected.to eq(date + delta * day_interval) }
513
+ it { is_expected.to eq(date + (delta * day_interval)) }
448
514
  end
449
515
 
450
516
  context "and a period that includes a weekend" do
451
517
  let(:date) { date_class.parse("Friday 4th Jan, 2013") }
452
518
 
453
- it { is_expected.to eq(date + (delta + 2) * day_interval) }
519
+ it { is_expected.to eq(date + ((delta + 2) * day_interval)) }
454
520
  end
455
521
 
456
522
  context "and a period that includes a working date weekend" do
457
523
  let(:extra_working_dates) { ["Sunday 6th Jan, 2013"] }
458
524
  let(:date) { date_class.parse("Friday 4th Jan, 2013") }
459
525
 
460
- it { is_expected.to eq(date + (delta + 1) * day_interval) }
526
+ it { is_expected.to eq(date + ((delta + 1) * day_interval)) }
461
527
  end
462
528
 
463
529
  context "and a period that includes a holiday day" do
464
530
  let(:date) { date_class.parse("Monday 31st Dec, 2012") }
465
531
 
466
- it { is_expected.to eq(date + (delta + 1) * day_interval) }
532
+ it { is_expected.to eq(date + ((delta + 1) * day_interval)) }
467
533
  end
468
534
  end
469
535
 
470
536
  context "given a non-business day" do
471
537
  let(:date) { date_class.parse("Tuesday 1st Jan, 2013") }
472
538
 
473
- it { is_expected.to eq(date + (delta + 1) * day_interval) }
539
+ it { is_expected.to eq(date + ((delta + 1) * day_interval)) }
474
540
  end
475
541
  end
476
542
 
@@ -489,33 +555,33 @@ RSpec.describe Business::Calendar do
489
555
  context "and a period that includes only business days" do
490
556
  let(:date) { date_class.parse("Wednesday 2nd Jan, 2013") }
491
557
 
492
- it { is_expected.to eq(date - delta * day_interval) }
558
+ it { is_expected.to eq(date - (delta * day_interval)) }
493
559
  end
494
560
 
495
561
  context "and a period that includes a weekend" do
496
562
  let(:date) { date_class.parse("Monday 31st Dec, 2012") }
497
563
 
498
- it { is_expected.to eq(date - (delta + 2) * day_interval) }
564
+ it { is_expected.to eq(date - ((delta + 2) * day_interval)) }
499
565
  end
500
566
 
501
567
  context "and a period that includes a working date weekend" do
502
568
  let(:extra_working_dates) { ["Saturday 29th Dec, 2012"] }
503
569
  let(:date) { date_class.parse("Monday 31st Dec, 2012") }
504
570
 
505
- it { is_expected.to eq(date - (delta + 1) * day_interval) }
571
+ it { is_expected.to eq(date - ((delta + 1) * day_interval)) }
506
572
  end
507
573
 
508
574
  context "and a period that includes a holiday day" do
509
575
  let(:date) { date_class.parse("Friday 4th Jan, 2013") }
510
576
 
511
- it { is_expected.to eq(date - (delta + 1) * day_interval) }
577
+ it { is_expected.to eq(date - ((delta + 1) * day_interval)) }
512
578
  end
513
579
  end
514
580
 
515
581
  context "given a non-business day" do
516
582
  let(:date) { date_class.parse("Thursday 3rd Jan, 2013") }
517
583
 
518
- it { is_expected.to eq(date - (delta + 1) * day_interval) }
584
+ it { is_expected.to eq(date - ((delta + 1) * day_interval)) }
519
585
  end
520
586
  end
521
587
 
@@ -628,7 +694,7 @@ RSpec.describe Business::Calendar do
628
694
  let(:date_1) { "Wed 28/5/2014" }
629
695
  let(:date_2) { "Thu 12/6/2014" }
630
696
 
631
- it { is_expected.to eq(11) }
697
+ it { is_expected.to eq(12) }
632
698
  end
633
699
  end
634
700
 
@@ -651,7 +717,7 @@ RSpec.describe Business::Calendar do
651
717
  let(:date_1) { "Wed 25/6/2014" }
652
718
  let(:date_2) { "Tue 8/7/2014" }
653
719
 
654
- it { is_expected.to eq(8) }
720
+ it { is_expected.to eq(9) }
655
721
  end
656
722
  end
657
723
  end
@@ -683,7 +749,7 @@ RSpec.describe Business::Calendar do
683
749
  let(:date_1) { "Sat 31/5/2014" }
684
750
  let(:date_2) { "Fri 13/6/2014" }
685
751
 
686
- it { is_expected.to eq(8) }
752
+ it { is_expected.to eq(9) }
687
753
  end
688
754
  end
689
755
 
@@ -698,7 +764,7 @@ RSpec.describe Business::Calendar do
698
764
  let(:date_1) { "Sat 31/5/2014" }
699
765
  let(:date_2) { "Sun 8/6/2014" }
700
766
 
701
- it { is_expected.to be(5) }
767
+ it { is_expected.to eq(6) }
702
768
  end
703
769
 
704
770
  context "including business, weekend days, and holidays" do
@@ -709,9 +775,9 @@ RSpec.describe Business::Calendar do
709
775
 
710
776
  context "including business, weekend, holiday days & working date" do
711
777
  let(:date_1) { "Sat 31/5/2014" }
712
- let(:date_2) { "Sun 14/6/2014" }
778
+ let(:date_2) { "Sun 15/6/2014" }
713
779
 
714
- it { is_expected.to be(9) }
780
+ it { is_expected.to be(10) }
715
781
  end
716
782
  end
717
783
 
@@ -726,7 +792,7 @@ RSpec.describe Business::Calendar do
726
792
  let(:date_1) { "Sat 31/5/2014" }
727
793
  let(:date_2) { "Thu 12/6/2014" }
728
794
 
729
- it { is_expected.to eq(8) }
795
+ it { is_expected.to eq(9) }
730
796
  end
731
797
  end
732
798
 
@@ -746,9 +812,9 @@ RSpec.describe Business::Calendar do
746
812
  end
747
813
 
748
814
  context "including business, weekend days, holidays & working date" do
749
- let(:date_2) { "Tue 13/6/2014" }
815
+ let(:date_2) { "Fri 13/6/2014" }
750
816
 
751
- it { is_expected.to eq(8) }
817
+ it { is_expected.to eq(9) }
752
818
  end
753
819
  end
754
820
  end
@@ -807,10 +873,10 @@ RSpec.describe Business::Calendar do
807
873
  end
808
874
 
809
875
  context "including business/weekend days, holidays & working date" do
810
- let(:date_1) { "27/5/2014" }
876
+ let(:date_1) { "Tue 27/5/2014" }
811
877
  let(:date_2) { "Thu 12/6/2014" }
812
878
 
813
- it { is_expected.to eq(11) }
879
+ it { is_expected.to eq(12) }
814
880
  end
815
881
  end
816
882
 
@@ -850,13 +916,13 @@ RSpec.describe Business::Calendar do
850
916
  context "including working date, working & weekend days" do
851
917
  let(:date_2) { "Tue 10/6/2014" }
852
918
 
853
- it { is_expected.to eq(6) }
919
+ it { is_expected.to eq(7) }
854
920
  end
855
921
 
856
922
  context "including working date, working & weekend days & holiday" do
857
- let(:date_2) { "Tue 13/6/2014" }
923
+ let(:date_2) { "Fri 13/6/2014" }
858
924
 
859
- it { is_expected.to eq(8) }
925
+ it { is_expected.to eq(9) }
860
926
  end
861
927
  end
862
928
 
@@ -913,7 +979,7 @@ RSpec.describe Business::Calendar do
913
979
  let(:date_1) { "Sat 28/6/2014" }
914
980
  let(:date_2) { "Sat 5/7/2014" }
915
981
 
916
- it { is_expected.to eq(4) }
982
+ it { is_expected.to eq(5) }
917
983
  end
918
984
  end
919
985
  end
@@ -933,6 +999,33 @@ RSpec.describe Business::Calendar do
933
999
  it { is_expected.to eq(4) }
934
1000
  end
935
1001
  end
1002
+
1003
+ context "if a calendar has an extra working date" do
1004
+ context "for a range less than a week long" do
1005
+ let(:date_1) { "Fri 30/5/2014" }
1006
+ let(:date_2) { "Tue 3/6/2014" }
1007
+
1008
+ it { is_expected.to eq(3) }
1009
+ end
1010
+
1011
+ context "for a range more than a week long" do
1012
+ let(:date_1) { "Mon 26/5/2014" }
1013
+ let(:date_2) { "Mon 9/6/2014" }
1014
+
1015
+ it { is_expected.to eq(10) }
1016
+ end
1017
+
1018
+ context "set after initialisation on a working day and a holiday" do
1019
+ before do
1020
+ calendar.set_extra_working_dates(["Wed 4/6/2014", "Sun 22/6/2014"])
1021
+ end
1022
+
1023
+ let(:date_1) { "Mon 2/6/2014" }
1024
+ let(:date_2) { "Mon 30/6/2014" }
1025
+
1026
+ it { is_expected.to eq(16) }
1027
+ end
1028
+ end
936
1029
  end
937
1030
  end
938
1031
 
metadata CHANGED
@@ -1,71 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: business
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
- - Harry Marr
8
- autorequire:
7
+ - GoCcardless
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2022-01-31 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: gc_ruboconfig
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: 2.31.0
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: 2.31.0
27
- - !ruby/object:Gem::Dependency
28
- name: rspec
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '3.1'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '3.1'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec_junit_formatter
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: 0.5.1
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: 0.5.1
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: 1.25.0
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: 1.25.0
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
69
12
  description: Date calculations based on business calendars
70
13
  email:
71
14
  - developers@gocardless.com
@@ -73,12 +16,13 @@ executables: []
73
16
  extensions: []
74
17
  extra_rdoc_files: []
75
18
  files:
76
- - ".circleci/config.yml"
77
19
  - ".github/dependabot.yml"
20
+ - ".github/workflows/tests.yml"
78
21
  - ".gitignore"
79
22
  - ".rubocop.yml"
80
23
  - ".ruby-version"
81
24
  - CHANGELOG.md
25
+ - COMPATIBILITY.md
82
26
  - Gemfile
83
27
  - LICENSE
84
28
  - README.md
@@ -93,8 +37,8 @@ files:
93
37
  homepage: https://github.com/gocardless/business
94
38
  licenses:
95
39
  - MIT
96
- metadata: {}
97
- post_install_message:
40
+ metadata:
41
+ rubygems_mfa_required: 'true'
98
42
  rdoc_options: []
99
43
  require_paths:
100
44
  - lib
@@ -109,12 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
53
  - !ruby/object:Gem::Version
110
54
  version: '0'
111
55
  requirements: []
112
- rubygems_version: 3.3.3
113
- signing_key:
56
+ rubygems_version: 4.0.16
114
57
  specification_version: 4
115
58
  summary: Date calculations based on business calendars
116
- test_files:
117
- - spec/business/calendar_spec.rb
118
- - spec/fixtures/calendars/bacs.yml
119
- - spec/fixtures/calendars/ecb.yml
120
- - spec/fixtures/calendars/invalid-keys.yml
59
+ test_files: []
data/.circleci/config.yml DELETED
@@ -1,37 +0,0 @@
1
- version: 2.1
2
-
3
- jobs:
4
- test:
5
- docker:
6
- - image: cimg/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.6.7", "2.7.3", "3.0.1", "3.1.0"]