business_calendar 0.0.4
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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +1 -0
- data/business_calendar.gemspec +27 -0
- data/data/holidays.yml +38 -0
- data/lib/business_calendar.rb +27 -0
- data/lib/business_calendar/calendar.rb +111 -0
- data/lib/business_calendar/holiday_determiner.rb +24 -0
- data/lib/business_calendar/version.rb +3 -0
- data/spec/acceptance_gb_spec.rb +34 -0
- data/spec/acceptance_us_spec.rb +48 -0
- data/spec/business_calendar/holiday_determiner_spec.rb +36 -0
- data/spec/business_calendar_spec.rb +109 -0
- data/spec/spec_helper.rb +19 -0
- metadata +137 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: d7f0e2f419f6a231337604c75979a9a8621e07df
|
|
4
|
+
data.tar.gz: 7c569d7e47f024718fbf2810b28f3a14bdf854bd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3a003dc3a36d6d9fc25743e4f0387ce7b738a11515efa50e8f5badb31af4fb0dea912b69debfd9bb49897f49ba0e83472d17958af157ce86859b43c61f20eaab
|
|
7
|
+
data.tar.gz: 6e0c666edabf29fd49a7d442c98b40133da419bf56b9571c85b110025a8a4ede613bb38c0047cded18a819f155ee67416c587bbbffb427070bf4bb9ffd548d1b
|
data/.gitignore
ADDED
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2014 Robert Nubel
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# BusinessCalendar
|
|
2
|
+
|
|
3
|
+
Need to know what days you can actually debit a customer on in excruciating detail? Fed up with singleton-based gems
|
|
4
|
+
that can't handle your complex, multi-international special needs? *So* over extending core objects like Numeric
|
|
5
|
+
and Date just to make your code a little cleaner? Well, do I have the gem for you! **BusinessCalendar** is a
|
|
6
|
+
reasonably light-weight solution, concerned primarily with banking holidays in different countries.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Add this line to your application's Gemfile:
|
|
11
|
+
|
|
12
|
+
gem 'business_calendar'
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
|
|
16
|
+
$ bundle
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Basic usage
|
|
21
|
+
|
|
22
|
+
Instantiate a calendar object with:
|
|
23
|
+
|
|
24
|
+
bc = BusinessCalendar.for(:US)
|
|
25
|
+
|
|
26
|
+
This will automatically load holidays based on the US banking holiday schedule, as configured in `data/holidays.yml`.
|
|
27
|
+
Currently, this gem supports `:GB` and `:US` regions.
|
|
28
|
+
|
|
29
|
+
Now, you can use it:
|
|
30
|
+
|
|
31
|
+
bc.is_business_day? Date.parse('2014-03-10') # => true
|
|
32
|
+
bc.add_business_day Date.parse('2014-03-10') # => #<Date 2014-03-11>
|
|
33
|
+
bc.nearest_business_day Date.parse('2014-03-08') # => #<Date 2014-03-10>
|
|
34
|
+
bc.add_business_days Date.parse('2014-03-10'), 2 # => #<Date 2014-03-12>
|
|
35
|
+
bc.preceding_business_day Date.parse('2014-03-11') # => #<Date 2014-03-10>
|
|
36
|
+
|
|
37
|
+
And in multi-value contexts, too!
|
|
38
|
+
|
|
39
|
+
bc.add_business_day [ Date.parse('2014-03-10'), Date.parse('2014-03-11') ]
|
|
40
|
+
# => [ #<Date 2014-03-11>, #<Date 2014-03-12> ]
|
|
41
|
+
|
|
42
|
+
### Non-standard holidays
|
|
43
|
+
|
|
44
|
+
If you need a different list of holidays, you can skip the porcelain and create the calendar yourself with a custom holiday-determiner proc:
|
|
45
|
+
|
|
46
|
+
holiday_tester = Proc.new { |date| MY_HOLIDAY_DATES.include? date }
|
|
47
|
+
bc = BusinessCalendar::Calendar.new(holiday_tester)
|
|
48
|
+
|
|
49
|
+
## Contributing
|
|
50
|
+
|
|
51
|
+
1. Fork it
|
|
52
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
53
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
54
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
55
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'business_calendar/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "business_calendar"
|
|
8
|
+
spec.version = BusinessCalendar::VERSION
|
|
9
|
+
spec.authors = ["Robert Nubel"]
|
|
10
|
+
spec.email = ["rnubel@enova.com"]
|
|
11
|
+
spec.description = %q{Helper gem for dealing with business days and date adjustment in multiple countries.}
|
|
12
|
+
spec.summary = %q{Country-aware business-date logic and handling.}
|
|
13
|
+
spec.homepage = ""
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files`.split($/)
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
spec.add_dependency "holidays", "~> 1.0"
|
|
22
|
+
|
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
|
24
|
+
spec.add_development_dependency "rake"
|
|
25
|
+
spec.add_development_dependency "rspec"
|
|
26
|
+
spec.add_development_dependency "timecop"
|
|
27
|
+
end
|
data/data/holidays.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
US:
|
|
2
|
+
regions:
|
|
3
|
+
- 'us'
|
|
4
|
+
holiday_names:
|
|
5
|
+
- "New Year's Day"
|
|
6
|
+
- "Martin Luther King, Jr. Day"
|
|
7
|
+
- "Presidents' Day"
|
|
8
|
+
- "Memorial Day"
|
|
9
|
+
- "Independence Day"
|
|
10
|
+
- "Labor Day"
|
|
11
|
+
- "Columbus Day"
|
|
12
|
+
- "Veterans Day"
|
|
13
|
+
- "Thanksgiving"
|
|
14
|
+
- "Christmas Day"
|
|
15
|
+
|
|
16
|
+
GB:
|
|
17
|
+
regions:
|
|
18
|
+
- 'gb'
|
|
19
|
+
- 'gb_eng'
|
|
20
|
+
holiday_names:
|
|
21
|
+
- "Good Friday"
|
|
22
|
+
- "Easter Sunday"
|
|
23
|
+
- "Easter Monday"
|
|
24
|
+
- "May Day"
|
|
25
|
+
- "Bank Holiday"
|
|
26
|
+
- "Christmas Day"
|
|
27
|
+
- "Boxing Day"
|
|
28
|
+
- "New Year's Day"
|
|
29
|
+
additions:
|
|
30
|
+
- '2002-06-04' # Summer Banking Holiday
|
|
31
|
+
- '2012-06-04'
|
|
32
|
+
removals:
|
|
33
|
+
- '2002-05-28'
|
|
34
|
+
- '2012-05-28'
|
|
35
|
+
|
|
36
|
+
CN:
|
|
37
|
+
regions: []
|
|
38
|
+
holiday_names: [] #FIXME
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module BusinessCalendar
|
|
2
|
+
CountryNotSupported = Class.new(StandardError)
|
|
3
|
+
|
|
4
|
+
def self.for(country)
|
|
5
|
+
Calendar.new(holiday_determiner(country))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
def self.holiday_determiner(country)
|
|
10
|
+
cfg = config(country) or raise CountryNotSupported.new(country.inspect)
|
|
11
|
+
@holiday_procs ||= {}
|
|
12
|
+
@holiday_procs[country] ||= HolidayDeterminer.new(
|
|
13
|
+
cfg["regions"],
|
|
14
|
+
cfg["holiday_names"],
|
|
15
|
+
:additions => (cfg["additions"] || []).map { |s| Date.parse s },
|
|
16
|
+
:removals => (cfg["removals"] || []).map { |s| Date.parse s } )
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.config(country)
|
|
20
|
+
@config ||= YAML.load_file(File.join(File.dirname(File.expand_path(__FILE__)), '../data/holidays.yml'))
|
|
21
|
+
@config[country.to_s]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
require 'yaml'
|
|
26
|
+
require 'business_calendar/calendar'
|
|
27
|
+
require 'business_calendar/holiday_determiner'
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
class BusinessCalendar::Calendar
|
|
2
|
+
attr_reader :holiday_determiner
|
|
3
|
+
|
|
4
|
+
# @param [Proc[Date -> Boolean]] a proc which returns whether or not a date is a
|
|
5
|
+
# holiday.
|
|
6
|
+
def initialize(holiday_determiner)
|
|
7
|
+
@holiday_determiner = holiday_determiner
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# @param [Date] date
|
|
11
|
+
# @return [Boolean] Whether or not this calendar's list of holidays includes <date>.
|
|
12
|
+
def is_holiday?(date)
|
|
13
|
+
holiday_determiner.call(date)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param [Date] date
|
|
17
|
+
# @return [Boolean] Whether or not banking can be done on <date>.
|
|
18
|
+
def is_business_day?(date)
|
|
19
|
+
return false if date.saturday? || date.sunday?
|
|
20
|
+
return false if is_holiday?(date)
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param [Date or Array] date_or_dates The day(s) to start from.
|
|
25
|
+
# @param [Integer] num Number of days to advance. If negative,
|
|
26
|
+
# this will call `subtract_business_days`.
|
|
27
|
+
# @param [Symbol] direction Either `:forward` or `:backward`; the
|
|
28
|
+
# direction to move if today is not a
|
|
29
|
+
# business day.
|
|
30
|
+
# @return [Date] The result of adding <num> business days to <date>.
|
|
31
|
+
# @note 'Adding a business day' means moving one full business day from the
|
|
32
|
+
# start of the given day -- meaning that adding one business day to a
|
|
33
|
+
# Saturday will return the following Tuesday, not Monday.
|
|
34
|
+
def add_business_days(date_or_dates, num=1, initial_direction = :forward)
|
|
35
|
+
return subtract_business_days(date_or_dates, -num) if num < 0
|
|
36
|
+
|
|
37
|
+
with_one_or_many(date_or_dates) do |date|
|
|
38
|
+
start = nearest_business_day(date, initial_direction)
|
|
39
|
+
num.times.reduce(start) { |d| following_business_day(d) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
alias :add_business_day :add_business_days
|
|
43
|
+
|
|
44
|
+
# @param [Date or Array] date_or_dates The day(s) to start from.
|
|
45
|
+
# @param [Integer] num Number of days to retreat. If negative,
|
|
46
|
+
# this will call `add_business_days`.
|
|
47
|
+
# @return [Date] The business date to which adding <num> business dats
|
|
48
|
+
# would arrive at <date>.
|
|
49
|
+
def subtract_business_days(date_or_dates, num=1)
|
|
50
|
+
return add_business_days(date_or_dates, -num) if num < 0
|
|
51
|
+
|
|
52
|
+
with_one_or_many(date_or_dates) do |date|
|
|
53
|
+
num.times.reduce(date) { |d| preceding_business_day(d) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
alias :subtract_business_day :subtract_business_days
|
|
57
|
+
|
|
58
|
+
# @param [Date or Array] date_or_dates The day(s) to start from.
|
|
59
|
+
# @return [Date] The business day immediately prior to <date>.
|
|
60
|
+
def preceding_business_day(date_or_dates)
|
|
61
|
+
with_one_or_many(date_or_dates) do |date|
|
|
62
|
+
begin
|
|
63
|
+
date = date - 1
|
|
64
|
+
end until is_business_day? date
|
|
65
|
+
date
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @param [Date or Array] date_or_dates The day(s) to start from.
|
|
70
|
+
# @param [Symbol] direction Either `:forward` or `:backward`; the
|
|
71
|
+
# direction to move if today is not a
|
|
72
|
+
# business day.
|
|
73
|
+
# @return [Date] The nearest (going <direction> in time) business day from
|
|
74
|
+
# <date>. If <date> is already a business day, it will be returned.
|
|
75
|
+
def nearest_business_day(date_or_dates, direction = :forward)
|
|
76
|
+
with_one_or_many(date_or_dates) do |date|
|
|
77
|
+
until is_business_day? date
|
|
78
|
+
date += case direction
|
|
79
|
+
when :forward
|
|
80
|
+
1
|
|
81
|
+
when :backward
|
|
82
|
+
-1
|
|
83
|
+
else
|
|
84
|
+
raise ArgumentError, "Invalid direction supplied: '#{direction}' should instead be :forward or :backward"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
date
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @param [Date or Array] date_or_dates The day(s) to start from.
|
|
92
|
+
# @return [Date] The nearest (going forward in time) business day from <date>
|
|
93
|
+
# which is not <date> itself.
|
|
94
|
+
def following_business_day(date_or_dates)
|
|
95
|
+
with_one_or_many(date_or_dates) do |date|
|
|
96
|
+
nearest_business_day(date + 1)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
def with_one_or_many(thing_or_things)
|
|
102
|
+
is_array = thing_or_things.is_a? Enumerable
|
|
103
|
+
things = is_array ? thing_or_things : [thing_or_things]
|
|
104
|
+
|
|
105
|
+
results = things.collect do |thing|
|
|
106
|
+
yield thing
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return is_array ? results : results.first
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require 'holidays'
|
|
2
|
+
|
|
3
|
+
class BusinessCalendar::HolidayDeterminer
|
|
4
|
+
attr_reader :regions, :holiday_names, :additions, :removals
|
|
5
|
+
|
|
6
|
+
def initialize(regions, holiday_names, opts = {})
|
|
7
|
+
@regions = regions
|
|
8
|
+
@holiday_names = holiday_names
|
|
9
|
+
@additions = opts[:additions] || []
|
|
10
|
+
@removals = opts[:removals] || []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(date)
|
|
14
|
+
if additions.include? date
|
|
15
|
+
true
|
|
16
|
+
elsif removals.include? date
|
|
17
|
+
false
|
|
18
|
+
else
|
|
19
|
+
Holidays.between(date, date, @regions, :observed)
|
|
20
|
+
.select { |h| @holiday_names.include? h[:name] }
|
|
21
|
+
.size > 0
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "GB bank holidays" do
|
|
4
|
+
%w(
|
|
5
|
+
2012-06-04
|
|
6
|
+
2014-04-18
|
|
7
|
+
2014-04-21
|
|
8
|
+
2014-05-05
|
|
9
|
+
2014-05-26
|
|
10
|
+
2014-08-25
|
|
11
|
+
2014-12-25
|
|
12
|
+
2014-12-26
|
|
13
|
+
2015-01-01
|
|
14
|
+
2015-04-03
|
|
15
|
+
2015-04-06
|
|
16
|
+
2015-05-04
|
|
17
|
+
2015-05-25
|
|
18
|
+
2015-08-31
|
|
19
|
+
2015-12-25
|
|
20
|
+
2015-12-28
|
|
21
|
+
).map { |x| Date.parse x }.each do |expected_holiday|
|
|
22
|
+
it "treats #{expected_holiday} as a holiday" do
|
|
23
|
+
BusinessCalendar.for(:GB).is_holiday?(expected_holiday).should be_true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
%w(
|
|
28
|
+
2012-05-28
|
|
29
|
+
).map { |x| Date.parse x }.each do |date|
|
|
30
|
+
it "treats #{date} as not a holiday" do
|
|
31
|
+
BusinessCalendar.for(:GB).is_holiday?(date).should be_false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "US holidays" do
|
|
4
|
+
%w(
|
|
5
|
+
2014-05-26
|
|
6
|
+
2014-07-04
|
|
7
|
+
2014-09-01
|
|
8
|
+
2014-10-13
|
|
9
|
+
2014-11-11
|
|
10
|
+
2014-11-27
|
|
11
|
+
2014-12-25
|
|
12
|
+
2015-01-01
|
|
13
|
+
2015-01-19
|
|
14
|
+
2015-02-16
|
|
15
|
+
2015-05-25
|
|
16
|
+
2015-07-03
|
|
17
|
+
2015-09-07
|
|
18
|
+
2015-10-12
|
|
19
|
+
2015-11-11
|
|
20
|
+
2015-11-26
|
|
21
|
+
2015-12-25
|
|
22
|
+
2016-01-01
|
|
23
|
+
2016-01-18
|
|
24
|
+
2016-02-15
|
|
25
|
+
2016-05-30
|
|
26
|
+
2016-07-04
|
|
27
|
+
2016-09-05
|
|
28
|
+
2016-10-10
|
|
29
|
+
2016-11-11
|
|
30
|
+
2016-11-24
|
|
31
|
+
2016-12-26
|
|
32
|
+
2017-01-02
|
|
33
|
+
2017-01-16
|
|
34
|
+
2017-02-20
|
|
35
|
+
2017-05-29
|
|
36
|
+
2017-07-04
|
|
37
|
+
2017-09-04
|
|
38
|
+
2017-10-09
|
|
39
|
+
2017-11-10
|
|
40
|
+
2017-11-23
|
|
41
|
+
2017-12-25
|
|
42
|
+
2018-01-01
|
|
43
|
+
).map { |x| Date.parse x }.each do |expected_holiday|
|
|
44
|
+
it "treats #{expected_holiday} as a holiday" do
|
|
45
|
+
BusinessCalendar.for(:US).is_holiday?(expected_holiday).should be_true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe BusinessCalendar::HolidayDeterminer do
|
|
4
|
+
let(:regions) { [ :us ] }
|
|
5
|
+
let(:opts) {{}}
|
|
6
|
+
subject { BusinessCalendar::HolidayDeterminer.new(regions, ["Independence Day"], opts) }
|
|
7
|
+
|
|
8
|
+
it "initializes with list of regions and a list of accepted holidays" do
|
|
9
|
+
subject.regions.should == regions
|
|
10
|
+
subject.holiday_names.should == ["Independence Day"]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "knows which dates are holidays" do
|
|
14
|
+
# check for Independence Day's presence
|
|
15
|
+
(Date.today.year - 1 .. Date.today.year + 10).each do |year|
|
|
16
|
+
fourth = Date.new year, 7, 4
|
|
17
|
+
obs_date = fourth.sunday? ? fourth + 1 : (fourth.saturday? ? fourth - 1 : fourth)
|
|
18
|
+
subject.call(obs_date).should be_true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "with a list of exceptions and additions" do
|
|
23
|
+
let(:opts) {{
|
|
24
|
+
:removals => ['2101-07-04'.to_date], # Moved in honor of Great Lizard-Emperor Krall's birthday,
|
|
25
|
+
:additions => ['2101-07-05'.to_date] # probably.
|
|
26
|
+
}}
|
|
27
|
+
|
|
28
|
+
it "correctly determines false for the exceptions" do
|
|
29
|
+
subject.call('2101-07-04'.to_date).should be_false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "correctly determines true for the additions" do
|
|
33
|
+
subject.call('2101-07-05'.to_date).should be_true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe BusinessCalendar do
|
|
4
|
+
before { Timecop.freeze('2014-04-28'.to_date) }
|
|
5
|
+
subject { BusinessCalendar.for(country) }
|
|
6
|
+
|
|
7
|
+
shared_examples_for "standard business time" do
|
|
8
|
+
specify "a weekend is not a business day" do
|
|
9
|
+
subject.is_business_day?('2014-03-09'.to_date).should be_false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
specify "a normal weekday is a business day" do
|
|
13
|
+
subject.is_business_day?('2014-03-10'.to_date).should be_true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
specify 'the nearest business day to monday is monday in both directions' do
|
|
17
|
+
subject.nearest_business_day('2014-03-10'.to_date) .should == '2014-03-10'.to_date
|
|
18
|
+
subject.nearest_business_day('2014-03-10'.to_date, :backward) .should == '2014-03-10'.to_date
|
|
19
|
+
subject.nearest_business_day('2014-03-10'.to_date, :forward) .should == '2014-03-10'.to_date
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
specify 'the nearest business day to a saturday, forward, is monday' do
|
|
23
|
+
subject.nearest_business_day('2014-03-08'.to_date) .should == '2014-03-10'.to_date
|
|
24
|
+
subject.nearest_business_day('2014-03-08'.to_date, :forward).should == '2014-03-10'.to_date
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
specify 'the nearest business day to a saturday, backward, is friday' do
|
|
28
|
+
subject.nearest_business_day('2014-03-08'.to_date, :backward).should == '2014-03-07'.to_date
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
specify 'one business day added to a monday is the next day' do
|
|
32
|
+
subject.add_business_day('2014-03-10'.to_date).should == '2014-03-11'.to_date
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
specify 'one business day added to a friday is the coming monday' do
|
|
36
|
+
subject.add_business_day('2014-03-07'.to_date).should == '2014-03-10'.to_date
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
specify 'one business day added to a weekend is the coming tuesday' do
|
|
40
|
+
subject.add_business_day('2014-03-08'.to_date).should == '2014-03-11'.to_date
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
specify 'one business day added to a monday as well as a tuesday is tuesday and wednesday' do
|
|
44
|
+
d1, d2 = subject.add_business_days ['2014-03-10'.to_date, '2014-03-11'.to_date]
|
|
45
|
+
d1.should == '2014-03-11'.to_date
|
|
46
|
+
d2.should == '2014-03-12'.to_date
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
specify 'the following business day of a saturday is the coming monday' do
|
|
50
|
+
subject.following_business_day('2014-03-08'.to_date).should == '2014-03-10'.to_date
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
specify 'a saturday plus zero business days is the nearest business day, monday' do
|
|
54
|
+
subject.add_business_days('2014-03-08'.to_date, 0).should == '2014-03-10'.to_date
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
specify 'a saturday plus zero business days with initial direction backward is the nearest preceding business day, friday' do
|
|
58
|
+
subject.add_business_days('2014-03-08'.to_date, 0, :backward).should == '2014-03-07'.to_date
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
specify 'the following business day of a monday is tuesday' do
|
|
62
|
+
subject.following_business_day('2014-03-10'.to_date).should == '2014-03-11'.to_date
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
specify 'the preceding business day of a monday is friday' do
|
|
66
|
+
subject.preceding_business_day('2014-03-10'.to_date).should == '2014-03-07'.to_date
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
specify 'a monday less three business days is the previous wednesday' do
|
|
70
|
+
subject.add_business_days('2014-03-10'.to_date, -3).should == '2014-03-05'.to_date
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
specify 'a saturday less one business day is the previous friday' do
|
|
74
|
+
subject.subtract_business_day('2014-03-08'.to_date).should == '2014-03-07'.to_date
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context "in the US" do
|
|
79
|
+
let(:country) { :US }
|
|
80
|
+
|
|
81
|
+
it_behaves_like "standard business time"
|
|
82
|
+
|
|
83
|
+
specify "American Independence Day is not a business day" do
|
|
84
|
+
subject.is_business_day?('2014-07-04'.to_date).should be_false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
context "in the UK" do
|
|
89
|
+
let(:country) { :GB }
|
|
90
|
+
|
|
91
|
+
it_behaves_like "standard business time"
|
|
92
|
+
|
|
93
|
+
specify "American Independence Day is a business day" do
|
|
94
|
+
subject.is_business_day?('2014-07-04'.to_date).should be_true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
specify 'Boxing Day is observed on a weekday' do
|
|
98
|
+
subject.is_business_day?('2015-12-28'.to_date).should be_false
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
context "in an unknown country" do
|
|
103
|
+
let(:country) { :GBXX }
|
|
104
|
+
|
|
105
|
+
it "raises an informative error when building the calendar" do
|
|
106
|
+
expect { subject }.to raise_error BusinessCalendar::CountryNotSupported
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'bundler/setup'
|
|
2
|
+
Bundler.setup
|
|
3
|
+
|
|
4
|
+
require 'business_calendar'
|
|
5
|
+
require 'date'
|
|
6
|
+
require 'timecop'
|
|
7
|
+
|
|
8
|
+
# I'm not depending on ActiveSupport just for this.
|
|
9
|
+
class String
|
|
10
|
+
def to_date
|
|
11
|
+
Date.parse self
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Date
|
|
16
|
+
def inspect
|
|
17
|
+
"#<Date #{strftime("%Y-%m-%d")}>"
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: business_calendar
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Robert Nubel
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2014-03-31 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: holidays
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.3'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.3'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: timecop
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
description: Helper gem for dealing with business days and date adjustment in multiple
|
|
84
|
+
countries.
|
|
85
|
+
email:
|
|
86
|
+
- rnubel@enova.com
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- ".gitignore"
|
|
92
|
+
- ".rspec"
|
|
93
|
+
- Gemfile
|
|
94
|
+
- LICENSE.txt
|
|
95
|
+
- README.md
|
|
96
|
+
- Rakefile
|
|
97
|
+
- business_calendar.gemspec
|
|
98
|
+
- data/holidays.yml
|
|
99
|
+
- lib/business_calendar.rb
|
|
100
|
+
- lib/business_calendar/calendar.rb
|
|
101
|
+
- lib/business_calendar/holiday_determiner.rb
|
|
102
|
+
- lib/business_calendar/version.rb
|
|
103
|
+
- spec/acceptance_gb_spec.rb
|
|
104
|
+
- spec/acceptance_us_spec.rb
|
|
105
|
+
- spec/business_calendar/holiday_determiner_spec.rb
|
|
106
|
+
- spec/business_calendar_spec.rb
|
|
107
|
+
- spec/spec_helper.rb
|
|
108
|
+
homepage: ''
|
|
109
|
+
licenses:
|
|
110
|
+
- MIT
|
|
111
|
+
metadata: {}
|
|
112
|
+
post_install_message:
|
|
113
|
+
rdoc_options: []
|
|
114
|
+
require_paths:
|
|
115
|
+
- lib
|
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
requirements: []
|
|
127
|
+
rubyforge_project:
|
|
128
|
+
rubygems_version: 2.2.0
|
|
129
|
+
signing_key:
|
|
130
|
+
specification_version: 4
|
|
131
|
+
summary: Country-aware business-date logic and handling.
|
|
132
|
+
test_files:
|
|
133
|
+
- spec/acceptance_gb_spec.rb
|
|
134
|
+
- spec/acceptance_us_spec.rb
|
|
135
|
+
- spec/business_calendar/holiday_determiner_spec.rb
|
|
136
|
+
- spec/business_calendar_spec.rb
|
|
137
|
+
- spec/spec_helper.rb
|