business_calendar 0.0.18 → 1.0.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: 82cbe11f46934df2bcc820bd3725e10601f7e2942ef46285e191e96af392faee
4
- data.tar.gz: 1bc29d426eb24de502a7b62c26300e841b914c74f66c6ab99193e2b862031103
3
+ metadata.gz: f7e437b2a3580ac2cac07f883e3acc42dfa39e0bd3506e95e1e46a1441d14f7d
4
+ data.tar.gz: 13769d2f539ae93f3579c1b46378c5da5708b039b63f186e910d9f528e4b5041
5
5
  SHA512:
6
- metadata.gz: 8aeaca2443bb3a4cfdf40dd6a016803b3e16bbf714104efb725ca417efcb9aecd76a5dd9cc7b334c6a2c79e66a882db31254b72cf834892a00a00b90be3da5f6
7
- data.tar.gz: 76cc84dc341780de4f5c90899f101d0cc3215fc3b930602f982be66bf2f74b876503ad067abfa56d28be10fd5acee278db90a49e95740fe66c5e7a1f7e098de2
6
+ metadata.gz: 625699922d3aa15522193ab57de09fa8ac5c0d13623afd47bfdbda65b2e2b2798d623ea316c0db5570b3e0c4c69de18cb4a47793167bcacf458642c516a2e0af
7
+ data.tar.gz: de9860000e204fc1fb05a853c437277bda370033046cc2ff43d954af432b715d39bba09f8a9d085621876f2cf508e6434063ad8ea238e01cde8eea955b7b1f42
data/.travis.yml CHANGED
@@ -1,21 +1,21 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  cache: bundler
4
- rvm:
5
- - 2.3.1
6
4
 
7
- script: bundle exec rspec
5
+ matrix:
6
+ include:
7
+ - rmv: ree
8
+ gemfile: gemfiles/ree.gemfile
9
+ script: bundle exec rspec
10
+ - rvm: 1.9.3
11
+ script: bundle exec rspec
12
+ - rvm: 2.3.1
13
+ script: bundle exec rspec
8
14
 
9
15
  env:
10
16
  global:
11
17
  secure: klQ3BQNdKkGIq3Zfv8Sr6oZqaMg9GHx+LGhynQ5xt7A6SGvxbMuoXJlE9wW4me6u+B9S2D+q+pdKgGPE/+fHsVt/d0zDXRnrjVyo1eexT220AMfpjiFDAtya8sAcuuAICLhw8AmzTjns8yAWInv5U5vC6oejkLA71FXtHU//210=
12
18
 
13
- matrix:
14
- include:
15
- - rvm: ree
16
- gemfile: gemfiles/ree.gemfile
17
- env: TEST_SUITE=spec
18
-
19
19
  deploy:
20
20
  provider: rubygems
21
21
  api_key:
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # business_calendar changes by version
2
+
3
+ 1.0.0
4
+ ---------
5
+
6
+ - Initial version 1 release
7
+ - Add option to fetch holidays from a URL [#19]
8
+ - Install new dependencies while preserving support for ruby 1.8.7 [#20] and [#22]
9
+ - Add option to treat weekends as business days [#21]
data/README.md CHANGED
@@ -61,6 +61,35 @@ holiday_tester = Proc.new { |date| MY_HOLIDAY_DATES.include? date }
61
61
  bc = BusinessCalendar::Calendar.new(holiday_tester)
62
62
  ```
63
63
 
64
+ You can also create an API that returns a list of holidays, and point BusinessCalendar to the API.
65
+
66
+ The API needs to respond to an `HTTP GET` with status `200` and a JSON response with field `holidays` containing a list of ISO 8601 string dates:
67
+
68
+ ```json
69
+ {
70
+ "holidays": [
71
+ "2018-10-08",
72
+ "2018-11-12",
73
+ "2018-11-22"
74
+ ]
75
+ }
76
+ ```
77
+
78
+ With this option, holiday dates are *temporarily* cached (with a default ttl of 300s). This is so that changes to the data being returned by the API will not necessitate restarting every application/process that uses BusinessCalendar with that API.
79
+
80
+ Usage:
81
+
82
+ ```ruby
83
+ # additions = URI to hit for dates to be added to holiday list. Set to nil if none
84
+ # removals = URI to hit for dates to be removed from holiday list. Set to nil if none
85
+ #
86
+ # opts = Set same config options (regions, holiday_names, additions_only)
87
+ # as the YAML files, with the additional option "ttl" to set ttl on
88
+ # cached dates. Defaults to 300s.
89
+
90
+ bc = BusinessCalendar.for_endpoint('https://some.test/calendars/2018', 'https://some.test/calendars/2018_removals')
91
+ ```
92
+
64
93
  ## Contributing
65
94
 
66
95
  1. Fork it
@@ -18,11 +18,15 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+
21
22
  spec.add_dependency "holidays", "~> 1.0"
23
+ spec.add_dependency "faraday"
24
+ spec.add_dependency "faraday-conductivity"
22
25
 
23
26
  spec.add_development_dependency "bundler", "~> 1.3"
24
27
  spec.add_development_dependency "rake"
25
28
  spec.add_development_dependency "rspec", "~> 3.2"
29
+ spec.add_development_dependency "webmock"
26
30
  spec.add_development_dependency "simplecov"
27
31
  spec.add_development_dependency "pry"
28
32
  end
data/gemfiles/ree.gemfile CHANGED
@@ -1,7 +1,10 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'rake', '< 11'
4
- gem 'json', '< 2'
5
- gem 'pry', '< 0.10'
3
+ gem 'addressable', '< 2.4.0'
4
+ gem 'json', '< 2'
5
+ gem 'faraday', '< 0.10'
6
+ gem 'hashdiff', '0.3.0'
7
+ gem 'pry', '< 0.10'
6
8
 
9
+ # Specify your gem's dependencies in business_calendar.gemspec
7
10
  gemspec :path => '../'
@@ -1,3 +1,6 @@
1
+ require 'faraday'
2
+ require 'faraday/conductivity'
3
+
1
4
  module BusinessCalendar
2
5
  CountryNotSupported = Class.new(StandardError)
3
6
  OrganizationNotSupported = Class.new(StandardError)
@@ -6,12 +9,17 @@ module BusinessCalendar
6
9
  if options["use_cached_calendar"]
7
10
  calendar_cache[country] ||= Calendar.new(holiday_determiner(country))
8
11
  else
9
- Calendar.new(holiday_determiner(country))
12
+ Calendar.new(holiday_determiner(country), options)
10
13
  end
11
14
  end
12
15
 
13
- def for_organization(org)
14
- Calendar.new(holiday_determiner_for_organization(org))
16
+ def for_organization(org, options = {})
17
+ Calendar.new(holiday_determiner_for_organization(org), options)
18
+ end
19
+
20
+ def for_endpoint(additions, removals, options = {})
21
+ ttl = options["ttl"] || 300
22
+ Calendar.new(holiday_determiner_for_endpoint(additions, removals, options), options.merge({"ttl" => ttl}))
15
23
  end
16
24
 
17
25
  private
@@ -38,6 +46,29 @@ module BusinessCalendar
38
46
  :additions_only => cfg['additions_only'] )
39
47
  end
40
48
 
49
+ def holiday_determiner_for_endpoint(additions_endpoint, removals_endpoint, opts)
50
+ client = Faraday.new do |conn|
51
+ conn.response :selective_errors
52
+ conn.adapter :net_http
53
+ end
54
+
55
+ additions = if additions_endpoint
56
+ Proc.new { JSON.parse(client.get(additions_endpoint).body).fetch('holidays').map { |s| Date.parse s } }
57
+ end
58
+
59
+ removals = if removals_endpoint
60
+ Proc.new { JSON.parse(client.get(removals_endpoint).body).fetch('holidays').map { |s| Date.parse s } }
61
+ end
62
+
63
+ HolidayDeterminer.new(
64
+ opts["regions"] || [],
65
+ opts["holiday_names"] || [],
66
+ :additions => additions,
67
+ :removals => removals,
68
+ :additions_only => opts["additions_only"] || []
69
+ )
70
+ end
71
+
41
72
  def calendar_cache
42
73
  @calendar_cache ||= {}
43
74
  end
@@ -3,8 +3,9 @@ class BusinessCalendar::Calendar
3
3
 
4
4
  # @param [Proc[Date -> Boolean]] a proc which returns whether or not a date is a
5
5
  # holiday.
6
- def initialize(holiday_determiner)
7
- @is_holiday = {}
6
+ def initialize(holiday_determiner, options = {})
7
+ @options = options
8
+ @holiday_cache = {}
8
9
  @holiday_determiner = holiday_determiner
9
10
  end
10
11
 
@@ -12,14 +13,16 @@ class BusinessCalendar::Calendar
12
13
  # @return [Boolean] Whether or not this calendar's list of holidays includes <date>.
13
14
  def is_holiday?(date)
14
15
  date = date.send(:to_date) if date.respond_to?(:to_date, true)
15
- return @is_holiday[date] unless @is_holiday[date].nil?
16
- @is_holiday[date] = holiday_determiner.call(date)
16
+
17
+ clear_cache if @options["ttl"]
18
+
19
+ @holiday_cache[date] ||= holiday_determiner.call(date)
17
20
  end
18
21
 
19
22
  # @param [Date] date
20
23
  # @return [Boolean] Whether or not banking can be done on <date>.
21
24
  def is_business_day?(date)
22
- return false if date.saturday? || date.sunday?
25
+ return false if !@options["business_weekends"] && (date.saturday? || date.sunday?)
23
26
  return false if is_holiday?(date)
24
27
  true
25
28
  end
@@ -114,4 +117,11 @@ class BusinessCalendar::Calendar
114
117
  yield thing_or_things
115
118
  end
116
119
  end
120
+
121
+ def clear_cache
122
+ if !@issued_at || (Time.now - @issued_at) >= @options["ttl"]
123
+ @issued_at = Time.now
124
+ @holiday_cache = {}
125
+ end
126
+ end
117
127
  end
@@ -18,7 +18,16 @@ class BusinessCalendar::HolidayDeterminer
18
18
  false
19
19
  elsif !additions_only
20
20
  Holidays.between(date, date, @regions, :observed).
21
- any? { |h| @holiday_names.include? h[:name] }
21
+ any? { |h| @holiday_names.include? h[:name] }
22
22
  end
23
23
  end
24
+
25
+ private
26
+ def additions
27
+ @additions.is_a?(Proc) ? @additions.call : @additions
28
+ end
29
+
30
+ def removals
31
+ @removals.is_a?(Proc) ? @removals.call : @removals
32
+ end
24
33
  end
@@ -1,3 +1,3 @@
1
1
  module BusinessCalendar
2
- VERSION = "0.0.18"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -75,11 +75,59 @@ describe BusinessCalendar do
75
75
  end
76
76
  end
77
77
 
78
+ shared_examples_for "weekends as business days" do
79
+ specify "a weekend is a business day" do
80
+ expect(subject.is_business_day?('2014-03-09'.to_date)).to be true
81
+ end
82
+
83
+ specify "a normal weekday is a business day" do
84
+ expect(subject.is_business_day?('2014-03-10'.to_date)).to be true
85
+ end
86
+
87
+ specify 'the nearest business day to a saturday in any direction is saturday' do
88
+ expect(subject.nearest_business_day('2014-03-08'.to_date)).to eq '2014-03-08'.to_date
89
+ expect(subject.nearest_business_day('2014-03-08'.to_date, :forward)).to eq '2014-03-08'.to_date
90
+ expect(subject.nearest_business_day('2014-03-08'.to_date, :backward)).to eq '2014-03-08'.to_date
91
+ end
92
+
93
+ specify 'one business day added to a friday is saturday' do
94
+ expect(subject.add_business_day('2014-03-07'.to_date)).to eq '2014-03-08'.to_date
95
+ end
96
+
97
+ specify 'one business day added to a weekend is the next day' do
98
+ expect(subject.add_business_day('2014-03-08'.to_date)).to eq '2014-03-09'.to_date
99
+ expect(subject.add_business_day('2014-03-09'.to_date)).to eq '2014-03-10'.to_date
100
+ end
101
+
102
+ specify 'the following business day of a saturday is sunday' do
103
+ expect(subject.following_business_day('2014-03-08'.to_date)).to eq '2014-03-09'.to_date
104
+ end
105
+
106
+ specify 'a saturday plus zero business days is still saturday' do
107
+ expect(subject.add_business_days('2014-03-08'.to_date, 0)).to eq '2014-03-08'.to_date
108
+ expect(subject.add_business_days('2014-03-08'.to_date, 0, :backward)).to eq '2014-03-08'.to_date
109
+ end
110
+
111
+ specify 'the preceding business day of a monday is sunday' do
112
+ expect(subject.preceding_business_day('2014-03-10'.to_date)).to eq '2014-03-09'.to_date
113
+ end
114
+
115
+ specify 'a monday less three business days is the previous friday' do
116
+ expect(subject.add_business_days('2014-03-10'.to_date, -3)).to eq '2014-03-07'.to_date
117
+ end
118
+ end
119
+
78
120
  context "in the US" do
79
121
  let(:country) { :US }
80
122
 
81
123
  it_behaves_like "standard business time"
82
124
 
125
+ context "with weekends as business days" do
126
+ subject { BusinessCalendar.for(country, {"business_weekends" => true}) }
127
+
128
+ it_behaves_like "weekends as business days"
129
+ end
130
+
83
131
  specify "American Independence Day is not a business day" do
84
132
  expect(subject.is_business_day?('2014-07-04'.to_date)).to be false
85
133
  end
@@ -94,6 +142,12 @@ describe BusinessCalendar do
94
142
 
95
143
  it_behaves_like "standard business time"
96
144
 
145
+ context "with weekends as business days" do
146
+ subject { BusinessCalendar.for(country, {"business_weekends" => true}) }
147
+
148
+ it_behaves_like "weekends as business days"
149
+ end
150
+
97
151
  specify "American Independence Day is a business day" do
98
152
  expect(subject.is_business_day?('2014-07-04'.to_date)).to be true
99
153
  end
@@ -110,4 +164,74 @@ describe BusinessCalendar do
110
164
  expect { subject }.to raise_error BusinessCalendar::CountryNotSupported
111
165
  end
112
166
  end
167
+
168
+ context "with an API endpoint" do
169
+ let(:additions) { 'http://fakeendpoint.test/additions' }
170
+ let(:removals) { 'http://fakeendpoint.test/removals' }
171
+
172
+ subject { BusinessCalendar.for_endpoint(additions, removals) }
173
+
174
+ before do
175
+ stub_request(:get, additions).to_return(
176
+ :status => 200,
177
+ :body => {'holidays' => ['2014-07-04', '2014-07-05']}.to_json
178
+ )
179
+
180
+ stub_request(:get, removals).to_return(
181
+ :status => 200,
182
+ :body => {'holidays' => ['2014-12-24', '2014-12-25']}.to_json
183
+ )
184
+ end
185
+
186
+ it_behaves_like "standard business time"
187
+
188
+ context "with weekends as business days" do
189
+ subject { BusinessCalendar.for_endpoint(additions, removals, {"business_weekends" => true}) }
190
+
191
+ it_behaves_like "weekends as business days"
192
+ end
193
+
194
+ it 'hits the configured endpoint for each call to an addition or removal' do
195
+ subject.is_business_day?('2014-07-03'.to_date)
196
+ subject.is_business_day?('2014-07-04'.to_date)
197
+ subject.is_holiday?('2014-07-06'.to_date)
198
+ subject.is_holiday?('2014-12-24'.to_date)
199
+
200
+ expect(a_request(:get, additions)).to have_been_made.times(4)
201
+ expect(a_request(:get, removals)).to have_been_made.times(3)
202
+ end
203
+
204
+ it 'caches holidays for 5 min' do
205
+ start = Time.now
206
+
207
+ allow(Time).to receive(:now) { start }
208
+
209
+ subject.is_business_day?('2014-07-04'.to_date)
210
+ subject.is_business_day?('2014-07-04'.to_date)
211
+
212
+ expect(a_request(:get, additions)).to have_been_made.times(1)
213
+
214
+ allow(Time).to receive(:now) { start + 301 }
215
+
216
+ subject.is_business_day?('2014-07-04'.to_date)
217
+
218
+ expect(a_request(:get, additions)).to have_been_made.times(2)
219
+ end
220
+
221
+ context 'http request fails' do
222
+ before { stub_request(:get, additions).to_return(:status => 500) }
223
+
224
+ it 'raises an error' do
225
+ expect { subject.is_business_day?('2014-07-04'.to_date) }.to raise_error Faraday::ClientError
226
+ end
227
+ end
228
+
229
+ specify "date included in endpoint's holiday list is not a business day" do
230
+ expect(subject.is_business_day?('2014-07-05'.to_date)).to be false
231
+ end
232
+
233
+ specify 'a time is converted to a date' do
234
+ expect(subject.is_holiday?(Time.parse('2014-07-04'))).to be true
235
+ end
236
+ end
113
237
  end
data/spec/spec_helper.rb CHANGED
@@ -5,9 +5,9 @@ require 'simplecov'
5
5
 
6
6
  require 'business_calendar'
7
7
  require 'date'
8
+ require 'webmock/rspec'
8
9
  require 'pry'
9
10
 
10
-
11
11
  # I'm not depending on ActiveSupport just for this.
12
12
  class String
13
13
  def to_date
@@ -20,3 +20,9 @@ class Date
20
20
  "#<Date #{strftime("%Y-%m-%d")}>"
21
21
  end
22
22
  end
23
+
24
+ RSpec.configure do |config|
25
+ config.before do
26
+ WebMock.disable_net_connect!
27
+ end
28
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: business_calendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.18
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Nubel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-07 00:00:00.000000000 Z
11
+ date: 2018-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: holidays
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday-conductivity
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: bundler
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +94,20 @@ dependencies:
66
94
  - - "~>"
67
95
  - !ruby/object:Gem::Version
68
96
  version: '3.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
69
111
  - !ruby/object:Gem::Dependency
70
112
  name: simplecov
71
113
  requirement: !ruby/object:Gem::Requirement
@@ -106,6 +148,7 @@ files:
106
148
  - ".rspec"
107
149
  - ".simplecov"
108
150
  - ".travis.yml"
151
+ - CHANGELOG.md
109
152
  - Gemfile
110
153
  - LICENSE.txt
111
154
  - README.md
@@ -150,7 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
193
  version: '0'
151
194
  requirements: []
152
195
  rubyforge_project:
153
- rubygems_version: 2.7.6
196
+ rubygems_version: 2.7.8
154
197
  signing_key:
155
198
  specification_version: 4
156
199
  summary: Country-aware business-date logic and handling.