business_calendar 0.0.16 → 2.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: bd465969e54c82daeb4bab3cf79c057ea98a82217c9ac21c7e18c3e85b646be3
4
- data.tar.gz: 5bea806cad1523d54e768970aae19267946d1785bf311af50033543d978fbe00
3
+ metadata.gz: 565c792e03739cdb3e5f506afba3f18c45098b204dda20051978333bcb5a5acc
4
+ data.tar.gz: 39de2f975e53ab66e32408c7591ec620b7596784277eebf1994c7231ecdd27af
5
5
  SHA512:
6
- metadata.gz: 194d44405e1c634c8a2f66e14b3085a804c0caebfd1a665ad3d09b94797cc04c6c833a4de6aa944849374334790329f964f141cda255d9b9e59cada605125858
7
- data.tar.gz: 2fc970adc520f9b2583d088a3b8b6b9ccf8fdbf9af69062df114a776825b643ba7c46e91fdb00ae3d1ff5fd605f95238978d12082e93a43e6a963e2503f28603
6
+ metadata.gz: 6a671409be0e036f24dd0d15a8125702b37c0218387026a2a609cd1c22a6a8b25523dd4a78a16b2b5dfb688230fe2286bbf3f9f273cbdf26f0bca3a74b3b3d30
7
+ data.tar.gz: 59d51361f35e15405f208288c9865e1d602fdb4baef607a509e191273c5ba1f38600a6d70a80e76104dc944cdab8539b25c33c8881a811dde5d11aec8a91d00e
@@ -0,0 +1,30 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ continue-on-error: ${{ matrix.experimental }}
13
+ strategy:
14
+ matrix:
15
+ ruby-version: [2.3.1, 2.7.0]
16
+ experimental: [false]
17
+ include:
18
+ - ruby-version: head
19
+ experimental: true
20
+
21
+ steps:
22
+ - uses: actions/checkout@v2
23
+ - name: Set up Ruby ${{ matrix.ruby-version }}
24
+ uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: ${{ matrix.ruby-version }}
27
+ - name: Install dependencies
28
+ run: bundle install
29
+ - name: Run tests
30
+ run: bundle exec rspec
@@ -0,0 +1,29 @@
1
+ name: Ruby Gem
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - '*'
7
+
8
+ jobs:
9
+ build:
10
+ name: Build + Publish
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - name: Set up Ruby 2.7
16
+ uses: actions/setup-ruby@v1
17
+ with:
18
+ ruby-version: 2.7
19
+
20
+ - name: Publish to RubyGems
21
+ run: |
22
+ mkdir -p $HOME/.gem
23
+ touch $HOME/.gem/credentials
24
+ chmod 0600 $HOME/.gem/credentials
25
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
26
+ gem build *.gemspec
27
+ gem push *.gem
28
+ env:
29
+ GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
@@ -0,0 +1,24 @@
1
+ # business_calendar changes by version
2
+
3
+ 2.0.0
4
+ ---------
5
+
6
+ - Remove UK weekend holidays, replace with the actual observed holiday dates [#26]
7
+
8
+ 1.1.0
9
+ ---------
10
+
11
+ - Cache parsed responses from API endpoints for TTL (Time to Live) duration.
12
+ - Increase default TTL duration from 5 minutes to 1 day.
13
+ Holidays are not expected to frequently change.
14
+ - Allow disabling cache clearing by setting `ttl` to `false`.
15
+ - Set hard limit to size of memoized holidays cache,
16
+ since large quantities of user supplied dates could consume excessive memory / cause DoS.
17
+
18
+ 1.0.0
19
+ ---------
20
+
21
+ - Initial version 1 release
22
+ - Add option to fetch holidays from a URL [#19]
23
+ - Install new dependencies while preserving support for ruby 1.8.7 [#20] and [#22]
24
+ - Add option to treat weekends as business days [#21]
data/Gemfile CHANGED
@@ -1,6 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'coveralls'
4
-
5
3
  # Specify your gem's dependencies in business_calendar.gemspec
6
4
  gemspec
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # BusinessCalendar
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/business_calendar.svg)](https://rubygems.org/gems/business_calendar)
4
- [![Build Status](https://travis-ci.org/enova/business_calendar.svg)](https://travis-ci.org/enova/business_calendar)
5
- [![Coverage Status](https://coveralls.io/repos/github/enova/business_calendar/badge.svg?branch=master)](https://coveralls.io/github/enova/business_calendar?branch=master)
6
4
  [![Dependency Status](https://gemnasium.com/enova/business_calendar.svg)](https://gemnasium.com/enova/business_calendar)
7
5
 
8
6
  Need to know what days you can actually debit a customer on in excruciating detail? Fed up with singleton-based gems
@@ -32,7 +30,7 @@ Instantiate a calendar object with:
32
30
  bc = BusinessCalendar.for(:US)
33
31
  ```
34
32
 
35
- This will automatically load holidays based on the US banking holiday schedule, as configured in `data/holidays.yml`.
33
+ This will automatically load holidays based on the US banking holiday schedule, as configured in `data/US.yml`.
36
34
  Currently, this gem supports `:GB` and `:US` regions.
37
35
 
38
36
  Now, you can use it:
@@ -61,6 +59,35 @@ holiday_tester = Proc.new { |date| MY_HOLIDAY_DATES.include? date }
61
59
  bc = BusinessCalendar::Calendar.new(holiday_tester)
62
60
  ```
63
61
 
62
+ You can also create an API that returns a list of holidays, and point BusinessCalendar to the API.
63
+
64
+ 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:
65
+
66
+ ```json
67
+ {
68
+ "holidays": [
69
+ "2018-10-08",
70
+ "2018-11-12",
71
+ "2018-11-22"
72
+ ]
73
+ }
74
+ ```
75
+
76
+ 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.
77
+
78
+ Usage:
79
+
80
+ ```ruby
81
+ # additions = URI to hit for dates to be added to holiday list. Set to nil if none
82
+ # removals = URI to hit for dates to be removed from holiday list. Set to nil if none
83
+ #
84
+ # opts = Set same config options (regions, holiday_names, additions_only)
85
+ # as the YAML files, with the additional option "ttl" to set ttl on
86
+ # cached dates. Defaults to 300s.
87
+
88
+ bc = BusinessCalendar.for_endpoint('https://some.test/calendars/2018', 'https://some.test/calendars/2018_removals')
89
+ ```
90
+
64
91
  ## Contributing
65
92
 
66
93
  1. Fork it
@@ -19,10 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "holidays", "~> 1.0"
22
+ spec.add_dependency "faraday"
22
23
 
23
- spec.add_development_dependency "bundler", "~> 1.3"
24
24
  spec.add_development_dependency "rake"
25
25
  spec.add_development_dependency "rspec", "~> 3.2"
26
- spec.add_development_dependency "simplecov"
26
+ spec.add_development_dependency "webmock"
27
27
  spec.add_development_dependency "pry"
28
28
  end
@@ -26,12 +26,12 @@ GB:
26
26
  - '2015-12-26'
27
27
  - '2016-12-25'
28
28
  - '2017-01-01'
29
- - '2020-12-26'
30
- - '2021-12-25'
31
- - '2021-12-26'
32
- - '2022-01-01'
33
- - '2022-12-25'
34
- - '2023-01-01'
29
+ - '2020-12-28'
30
+ - '2021-12-27'
31
+ - '2021-12-28'
32
+ - '2022-01-03'
33
+ - '2022-12-27'
34
+ - '2023-01-02'
35
35
  removals:
36
36
  - '2002-05-28'
37
37
  - '2007-04-08'
@@ -44,9 +44,9 @@ GB:
44
44
  - '2013-03-31'
45
45
  - '2014-04-20'
46
46
  - '2015-12-28'
47
- - '2020-12-28'
48
- - '2021-12-27'
49
- - '2021-12-28'
50
- - '2022-01-03'
51
- - '2022-12-27'
52
- - '2023-01-02'
47
+ - '2020-12-26'
48
+ - '2021-12-25'
49
+ - '2021-12-26'
50
+ - '2022-01-01'
51
+ - '2022-12-25'
52
+ - '2023-01-01'
@@ -21,6 +21,7 @@ captalys:
21
21
  - '2017-11-15'
22
22
  - '2017-11-20'
23
23
  - '2017-12-25'
24
+ - '2017-12-29'
24
25
  - '2018-01-01'
25
26
  - '2018-01-25'
26
27
  - '2018-02-12'
@@ -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 => '../'
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'json', '< 2.3.0'
4
+
5
+ # Specify your gem's dependencies in business_calendar.gemspec
6
+ gemspec :path => '../'
@@ -1,13 +1,23 @@
1
+ require 'faraday'
2
+
1
3
  module BusinessCalendar
2
4
  CountryNotSupported = Class.new(StandardError)
3
5
  OrganizationNotSupported = Class.new(StandardError)
4
6
  class << self
5
- def for(country)
6
- Calendar.new(holiday_determiner(country))
7
+ def for(country, options = {})
8
+ if options["use_cached_calendar"]
9
+ calendar_cache[country] ||= Calendar.new(holiday_determiner(country))
10
+ else
11
+ Calendar.new(holiday_determiner(country), options)
12
+ end
13
+ end
14
+
15
+ def for_organization(org, options = {})
16
+ Calendar.new(holiday_determiner_for_organization(org), options)
7
17
  end
8
18
 
9
- def for_organization(org)
10
- Calendar.new(holiday_determiner_for_organization(org))
19
+ def for_endpoint(additions, removals, options = {})
20
+ Calendar.new(holiday_determiner_for_endpoint(additions, removals, options), options)
11
21
  end
12
22
 
13
23
  private
@@ -34,6 +44,38 @@ module BusinessCalendar
34
44
  :additions_only => cfg['additions_only'] )
35
45
  end
36
46
 
47
+ def holiday_dates_for_endpoint(client, endpoint)
48
+ Proc.new { JSON.parse(client.get(endpoint).body).fetch('holidays').map { |s| Date.parse s } }
49
+ end
50
+
51
+ def holiday_determiner_for_endpoint(additions_endpoint, removals_endpoint, opts)
52
+ client = Faraday.new do |conn|
53
+ conn.response :raise_error
54
+ conn.adapter :net_http
55
+ end
56
+
57
+ additions = if additions_endpoint
58
+ holiday_dates_for_endpoint(client, additions_endpoint)
59
+ end
60
+
61
+ removals = if removals_endpoint
62
+ holiday_dates_for_endpoint(client, removals_endpoint)
63
+ end
64
+
65
+ HolidayDeterminer.new(
66
+ opts["regions"] || [],
67
+ opts["holiday_names"] || [],
68
+ :additions => additions,
69
+ :removals => removals,
70
+ :additions_only => opts["additions_only"] || [],
71
+ :ttl => opts['ttl']
72
+ )
73
+ end
74
+
75
+ def calendar_cache
76
+ @calendar_cache ||= {}
77
+ end
78
+
37
79
  def config(country)
38
80
  @config ||= load_config
39
81
  @config[country.to_s]
@@ -1,9 +1,14 @@
1
1
  class BusinessCalendar::Calendar
2
+ DEFAULT_TIME_TO_LIVE = 24 * 60 * 60
2
3
  attr_reader :holiday_determiner
3
4
 
4
5
  # @param [Proc[Date -> Boolean]] a proc which returns whether or not a date is a
5
6
  # holiday.
6
- def initialize(holiday_determiner)
7
+ def initialize(holiday_determiner, options = {})
8
+ ttl = options['ttl']
9
+ @time_to_live = ttl.nil? ? DEFAULT_TIME_TO_LIVE : ttl
10
+ @options = options
11
+ @holiday_cache = {}
7
12
  @holiday_determiner = holiday_determiner
8
13
  end
9
14
 
@@ -11,13 +16,16 @@ class BusinessCalendar::Calendar
11
16
  # @return [Boolean] Whether or not this calendar's list of holidays includes <date>.
12
17
  def is_holiday?(date)
13
18
  date = date.send(:to_date) if date.respond_to?(:to_date, true)
14
- holiday_determiner.call(date)
19
+
20
+ clear_cache if should_clear_cache?
21
+
22
+ @holiday_cache[date] ||= holiday_determiner.call(date)
15
23
  end
16
24
 
17
25
  # @param [Date] date
18
26
  # @return [Boolean] Whether or not banking can be done on <date>.
19
27
  def is_business_day?(date)
20
- return false if date.saturday? || date.sunday?
28
+ return false if !@options['business_weekends'] && (date.saturday? || date.sunday?)
21
29
  return false if is_holiday?(date)
22
30
  true
23
31
  end
@@ -36,8 +44,9 @@ class BusinessCalendar::Calendar
36
44
  return subtract_business_days(date_or_dates, -num) if num < 0
37
45
 
38
46
  with_one_or_many(date_or_dates) do |date|
39
- start = nearest_business_day(date, initial_direction)
40
- num.times.reduce(start) { |d, _| following_business_day(d) }
47
+ d = nearest_business_day(date, initial_direction)
48
+ num.times { d = following_business_day(d) }
49
+ d
41
50
  end
42
51
  end
43
52
  alias :add_business_day :add_business_days
@@ -51,7 +60,8 @@ class BusinessCalendar::Calendar
51
60
  return add_business_days(date_or_dates, -num) if num < 0
52
61
 
53
62
  with_one_or_many(date_or_dates) do |date|
54
- num.times.reduce(date) { |d, _| preceding_business_day(d) }
63
+ num.times { date = preceding_business_day(date) }
64
+ date
55
65
  end
56
66
  end
57
67
  alias :subtract_business_day :subtract_business_days
@@ -101,14 +111,26 @@ class BusinessCalendar::Calendar
101
111
  end
102
112
 
103
113
  private
104
- def with_one_or_many(thing_or_things)
105
- is_array = thing_or_things.is_a? Enumerable
106
- things = is_array ? thing_or_things : [thing_or_things]
107
114
 
108
- results = things.collect do |thing|
109
- yield thing
110
- end
115
+ def should_clear_cache?
116
+ return false unless @time_to_live
117
+
118
+ # limit size using a heuristic, to prevent cache growing arbitrarily large
119
+ !@last_cleared || (Time.now - @last_cleared) >= @time_to_live || @holiday_cache.size > 365 * 3
120
+ end
121
+
122
+ def clear_cache
123
+ @last_cleared = Time.now
124
+ @holiday_cache = {}
125
+ end
111
126
 
112
- return is_array ? results : results.first
127
+ def with_one_or_many(thing_or_things)
128
+ if thing_or_things.is_a? Enumerable
129
+ thing_or_things.collect do |thing|
130
+ yield thing
131
+ end
132
+ else
133
+ yield thing_or_things
134
+ end
113
135
  end
114
136
  end
@@ -1,9 +1,12 @@
1
1
  require 'holidays'
2
2
 
3
3
  class BusinessCalendar::HolidayDeterminer
4
+ DEFAULT_TIME_TO_LIVE = 24 * 60 * 60
4
5
  attr_reader :regions, :holiday_names, :additions, :removals, :additions_only
5
6
 
6
7
  def initialize(regions, holiday_names, opts = {})
8
+ ttl = opts[:ttl]
9
+ @time_to_live = ttl.nil? ? DEFAULT_TIME_TO_LIVE : ttl
7
10
  @regions = regions
8
11
  @holiday_names = holiday_names
9
12
  @additions = opts[:additions] || []
@@ -12,13 +15,37 @@ class BusinessCalendar::HolidayDeterminer
12
15
  end
13
16
 
14
17
  def call(date)
18
+ clear_cache if should_clear_cache?
19
+
15
20
  if additions.include? date
16
21
  true
17
22
  elsif removals.include? date
18
23
  false
19
24
  elsif !additions_only
20
25
  Holidays.between(date, date, @regions, :observed).
21
- any? { |h| @holiday_names.include? h[:name] }
26
+ any? { |h| @holiday_names.include? h[:name] }
22
27
  end
23
28
  end
29
+
30
+ private
31
+
32
+ def should_clear_cache?
33
+ return false unless @time_to_live
34
+
35
+ !@last_cleared || (Time.now - @last_cleared) >= @time_to_live
36
+ end
37
+
38
+ def clear_cache
39
+ @last_cleared = Time.now
40
+ @additions_cache = nil
41
+ @removals_cache = nil
42
+ end
43
+
44
+ def additions
45
+ @additions_cache ||= @additions.is_a?(Proc) ? @additions.call : @additions
46
+ end
47
+
48
+ def removals
49
+ @removals_cache ||= @removals.is_a?(Proc) ? @removals.call : @removals
50
+ end
24
51
  end
@@ -1,3 +1,3 @@
1
1
  module BusinessCalendar
2
- VERSION = "0.0.16"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -20,12 +20,12 @@ describe "GB bank holidays" do
20
20
  2015-12-26
21
21
  2016-12-25
22
22
  2017-01-01
23
- 2020-12-26
24
- 2021-12-25
25
- 2021-12-26
26
- 2022-01-01
27
- 2022-12-25
28
- 2023-01-01
23
+ 2020-12-28
24
+ 2021-12-27
25
+ 2021-12-28
26
+ 2022-01-03
27
+ 2022-12-27
28
+ 2023-01-02
29
29
  ).map { |x| Date.parse x }.each do |expected_holiday|
30
30
  it "treats #{expected_holiday} as a holiday" do
31
31
  expect(BusinessCalendar.for(:GB).is_holiday?(expected_holiday)).to be true
@@ -35,12 +35,12 @@ describe "GB bank holidays" do
35
35
  %w(
36
36
  2012-05-28
37
37
  2015-12-28
38
- 2020-12-28
39
- 2021-12-27
40
- 2021-12-28
41
- 2022-01-03
42
- 2022-12-27
43
- 2023-01-02
38
+ 2020-12-26
39
+ 2021-12-25
40
+ 2021-12-26
41
+ 2022-01-01
42
+ 2022-12-25
43
+ 2023-01-01
44
44
  ).map { |x| Date.parse x }.each do |date|
45
45
  it "treats #{date} as not a holiday" do
46
46
  expect(BusinessCalendar.for(:GB).is_holiday?(date)).to be false
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe BusinessCalendar::HolidayDeterminer do
4
4
  let(:regions) { [ :us ] }
5
- let(:opts) {{}}
5
+ let(:opts) { {} }
6
6
  subject { BusinessCalendar::HolidayDeterminer.new(regions, ["Independence Day"], opts) }
7
7
 
8
8
  it "initializes with list of regions and a list of accepted holidays" do
@@ -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,218 @@ 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 and then reuses the cached result' do
195
+ subject.is_business_day?('2014-07-03'.to_date)
196
+ subject.is_business_day?('2014-07-03'.to_date)
197
+ subject.is_business_day?('2014-07-04'.to_date)
198
+ subject.is_holiday?('2014-07-06'.to_date)
199
+ subject.is_holiday?('2014-07-06'.to_date)
200
+ subject.is_holiday?('2014-12-24'.to_date)
201
+
202
+ expect(a_request(:get, additions)).to have_been_made.times(1)
203
+ expect(a_request(:get, removals)).to have_been_made.times(1)
204
+ end
205
+
206
+ context 'after 24 hours without specifying a Time to Live override' do
207
+ subject { BusinessCalendar.for_endpoint(additions, removals) }
208
+ let!(:start) { Time.now }
209
+ let!(:one_day) { 86400 }
210
+
211
+ it 'expires the holidays cache' do
212
+ allow(Time).to receive(:now) { start }
213
+
214
+ subject.is_business_day?('2014-01-01'.to_date)
215
+
216
+ # initial request was made
217
+ expect(a_request(:get, additions)).to have_been_made.times(1)
218
+
219
+ subject.is_business_day?('2014-07-04'.to_date)
220
+ subject.is_business_day?('2014-11-28'.to_date)
221
+
222
+ # cache from initial request was still used
223
+ expect(a_request(:get, additions)).to have_been_made.times(1)
224
+
225
+ # 24 hours + 1 second have passed
226
+ # cache should be cleared and fresh API request made
227
+ allow(Time).to receive(:now) { start + one_day + 1 }
228
+
229
+ subject.is_business_day?('2014-01-01'.to_date)
230
+
231
+ # 2nd request was made
232
+ expect(a_request(:get, additions)).to have_been_made.times(2)
233
+
234
+ subject.is_business_day?('2014-07-04'.to_date)
235
+ subject.is_business_day?('2014-11-28'.to_date)
236
+
237
+ # 2nd request is now cached, so no new request should have been issued
238
+ expect(a_request(:get, additions)).to have_been_made.times(2)
239
+ end
240
+ end
241
+
242
+ context 'turning off Time to Live functionality' do
243
+ subject { BusinessCalendar.for_endpoint(additions, removals, {'ttl' => false}) }
244
+ let!(:start) { Time.now }
245
+ let!(:one_day) { 86400 }
246
+
247
+ it 'will never clear the cache' do
248
+ allow(Time).to receive(:now) { start }
249
+
250
+ subject.is_business_day?('2014-01-01'.to_date)
251
+ expect(a_request(:get, additions)).to have_been_made.times(1)
252
+
253
+ allow(Time).to receive(:now) { start + 301 }
254
+ subject.is_business_day?('2014-01-01'.to_date)
255
+ subject.is_business_day?('2014-07-04'.to_date)
256
+ subject.is_business_day?('2014-07-04'.to_date)
257
+ subject.is_business_day?('2014-11-28'.to_date)
258
+ expect(a_request(:get, additions)).to have_been_made.times(1)
259
+
260
+ allow(Time).to receive(:now) { start + one_day + 1 }
261
+ subject.is_business_day?('2014-01-01'.to_date)
262
+ subject.is_business_day?('2014-07-04'.to_date)
263
+ subject.is_business_day?('2014-11-28'.to_date)
264
+ expect(a_request(:get, additions)).to have_been_made.times(1)
265
+ end
266
+ end
267
+
268
+ context 'setting Time to Live override to zero' do
269
+ subject { BusinessCalendar.for_endpoint(additions, removals, {'ttl' => 0}) }
270
+ let!(:start) { Time.now }
271
+
272
+ it 'will always clear the cache' do
273
+ allow(Time).to receive(:now) { start }
274
+
275
+ subject.is_holiday?('2014-01-01'.to_date)
276
+ expect(a_request(:get, additions)).to have_been_made.times(1)
277
+
278
+ allow(Time).to receive(:now) { start + 301 }
279
+
280
+ subject.is_holiday?('2014-01-01'.to_date)
281
+ subject.is_holiday?('2014-07-04'.to_date)
282
+ subject.is_holiday?('2014-07-04'.to_date)
283
+ subject.is_holiday?('2014-11-28'.to_date)
284
+
285
+ expect(a_request(:get, additions)).to have_been_made.times(5)
286
+
287
+ subject.is_holiday?('2014-01-01'.to_date)
288
+ subject.is_holiday?('2014-01-01'.to_date)
289
+ subject.is_holiday?('2014-07-04'.to_date)
290
+ subject.is_holiday?('2014-11-28'.to_date)
291
+ expect(a_request(:get, additions)).to have_been_made.times(9)
292
+ end
293
+ end
294
+
295
+ context 'using a 5 minute Time to Live override' do
296
+ subject { BusinessCalendar.for_endpoint(additions, removals, {'ttl' => 300}) }
297
+ let!(:start) { Time.now }
298
+
299
+ it 'expires the holidays cache after the specified time has elapsed' do
300
+ allow(Time).to receive(:now) { start }
301
+
302
+ # initial request made and cached
303
+ subject.is_business_day?('2014-01-01'.to_date)
304
+ subject.is_business_day?('2014-07-04'.to_date)
305
+ subject.is_business_day?('2014-11-28'.to_date)
306
+ expect(a_request(:get, additions)).to have_been_made.times(1)
307
+
308
+ # 120 seconds pass, cache should still be used
309
+ allow(Time).to receive(:now) { start + 120 }
310
+ subject.is_business_day?('2014-01-01'.to_date)
311
+ subject.is_business_day?('2014-07-04'.to_date)
312
+ subject.is_business_day?('2014-11-28'.to_date)
313
+ expect(a_request(:get, additions)).to have_been_made.times(1)
314
+
315
+ # 301 seconds pass, cache is expired and 2nd request made
316
+ allow(Time).to receive(:now) { start + 301 }
317
+ subject.is_business_day?('2014-01-01'.to_date)
318
+ subject.is_business_day?('2014-07-04'.to_date)
319
+ subject.is_business_day?('2014-11-28'.to_date)
320
+ expect(a_request(:get, additions)).to have_been_made.times(2)
321
+
322
+ # 2nd request is now cached, so no new request should have been issued
323
+ subject.is_business_day?('2014-01-01'.to_date)
324
+ subject.is_business_day?('2014-07-04'.to_date)
325
+ subject.is_business_day?('2014-11-28'.to_date)
326
+ expect(a_request(:get, additions)).to have_been_made.times(2)
327
+ end
328
+ end
329
+
330
+ context 'with a few years in dates filling the cache' do
331
+ let!(:start) { Time.now }
332
+
333
+ # NOTE: this test cheats a bit to test class internals / implementation
334
+ it 'will clear out the holiday cache but keep the cached API result' do
335
+ allow(Time).to receive(:now) { start }
336
+ subject.is_business_day?('2014-07-04'.to_date)
337
+ expect(a_request(:get, additions)).to have_been_made.times(1)
338
+
339
+ expect(a_request(:get, additions)).to have_been_made.times(1)
340
+ (2014..2017).each do |year|
341
+ (1..12).each do |month|
342
+ (1..28).each do |day|
343
+ subject.is_business_day?("#{year}-#{month}-#{day}".to_date)
344
+ end
345
+ end
346
+ end
347
+
348
+ # about to reach cache size threshold
349
+ expect(subject.instance_variable_get('@holiday_cache').size).to be(960)
350
+
351
+ (1..7).each do |month|
352
+ (1..28).each do |day|
353
+ subject.is_business_day?("#2018-#{month}-#{day}".to_date)
354
+ end
355
+ end
356
+
357
+ # now holiday cache will have been cleared and should be small again
358
+ expect(subject.instance_variable_get('@holiday_cache').size).to be(4)
359
+
360
+ # and cached API response was still used
361
+ expect(a_request(:get, additions)).to have_been_made.times(1)
362
+ end
363
+ end
364
+
365
+ context 'http request fails' do
366
+ before { stub_request(:get, additions).to_return(:status => 500) }
367
+
368
+ it 'raises an error' do
369
+ expect { subject.is_business_day?('2014-07-04'.to_date) }.to raise_error Faraday::Error
370
+ end
371
+ end
372
+
373
+ specify "date included in endpoint's holiday list is not a business day" do
374
+ expect(subject.is_business_day?('2014-07-05'.to_date)).to be false
375
+ end
376
+
377
+ specify 'a time is converted to a date' do
378
+ expect(subject.is_holiday?(Time.parse('2014-07-04'))).to be true
379
+ end
380
+ end
113
381
  end
@@ -1,13 +1,11 @@
1
1
  require 'bundler/setup'
2
2
  Bundler.setup
3
3
 
4
- require 'simplecov'
5
-
6
4
  require 'business_calendar'
7
5
  require 'date'
6
+ require 'webmock/rspec'
8
7
  require 'pry'
9
8
 
10
-
11
9
  # I'm not depending on ActiveSupport just for this.
12
10
  class String
13
11
  def to_date
@@ -20,3 +18,9 @@ class Date
20
18
  "#<Date #{strftime("%Y-%m-%d")}>"
21
19
  end
22
20
  end
21
+
22
+ RSpec.configure do |config|
23
+ config.before do
24
+ WebMock.disable_net_connect!
25
+ end
26
+ 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.16
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Nubel
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-07 00:00:00.000000000 Z
11
+ date: 2020-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: holidays
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bundler
28
+ name: faraday
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.3'
34
- type: :development
33
+ version: '0'
34
+ type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.3'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '3.2'
69
69
  - !ruby/object:Gem::Dependency
70
- name: simplecov
70
+ name: webmock
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -102,10 +102,11 @@ executables: []
102
102
  extensions: []
103
103
  extra_rdoc_files: []
104
104
  files:
105
+ - ".github/workflows/ci.yml"
106
+ - ".github/workflows/gem-push.yml"
105
107
  - ".gitignore"
106
108
  - ".rspec"
107
- - ".simplecov"
108
- - ".travis.yml"
109
+ - CHANGELOG.md
109
110
  - Gemfile
110
111
  - LICENSE.txt
111
112
  - README.md
@@ -117,6 +118,7 @@ files:
117
118
  - data/US.yml
118
119
  - data/org/captalys.yml
119
120
  - gemfiles/ree.gemfile
121
+ - gemfiles/ruby_1.9.3.gemfile
120
122
  - lib/business_calendar.rb
121
123
  - lib/business_calendar/calendar.rb
122
124
  - lib/business_calendar/holiday_determiner.rb
@@ -134,7 +136,7 @@ homepage: ''
134
136
  licenses:
135
137
  - MIT
136
138
  metadata: {}
137
- post_install_message:
139
+ post_install_message:
138
140
  rdoc_options: []
139
141
  require_paths:
140
142
  - lib
@@ -149,9 +151,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
151
  - !ruby/object:Gem::Version
150
152
  version: '0'
151
153
  requirements: []
152
- rubyforge_project:
153
- rubygems_version: 2.7.6
154
- signing_key:
154
+ rubygems_version: 3.1.2
155
+ signing_key:
155
156
  specification_version: 4
156
157
  summary: Country-aware business-date logic and handling.
157
158
  test_files:
data/.simplecov DELETED
@@ -1,8 +0,0 @@
1
- unless RUBY_VERSION == "1.8.7"
2
- require 'coveralls'
3
-
4
- Coveralls.wear! do
5
- add_filter "version.rb"
6
- add_filter "spec/"
7
- end
8
- end
@@ -1,25 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.3.1
6
-
7
- script: bundle exec rspec
8
-
9
- env:
10
- global:
11
- secure: klQ3BQNdKkGIq3Zfv8Sr6oZqaMg9GHx+LGhynQ5xt7A6SGvxbMuoXJlE9wW4me6u+B9S2D+q+pdKgGPE/+fHsVt/d0zDXRnrjVyo1eexT220AMfpjiFDAtya8sAcuuAICLhw8AmzTjns8yAWInv5U5vC6oejkLA71FXtHU//210=
12
-
13
- matrix:
14
- include:
15
- - rvm: ree
16
- gemfile: gemfiles/ree.gemfile
17
- env: TEST_SUITE=spec
18
-
19
- deploy:
20
- provider: rubygems
21
- api_key:
22
- secure: "c9QcNX+nk0Yzl22ZVknZtv+/G4fUeLph0TH9PS6hQ92zywM61wtXTJExZdBHulvKAaQbKaq20LcFjLw41qwJc6tvIEYXpOinfOaiemTKaGmKscDrIi3wUAxfjmTacXZAaTdEORQVqTOmppqsuKnP7cqmSNxTSI901Sz5Cy6Jpl0="
23
- on:
24
- tags: true
25
- repo: enova/business_calendar