working_hours 0.9.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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +150 -0
- data/Rakefile +11 -0
- data/gemfiles/Gemfile.activesupport-3.2.x +5 -0
- data/gemfiles/Gemfile.activesupport-4.0.x +5 -0
- data/gemfiles/Gemfile.activesupport-4.1.x +5 -0
- data/gemfiles/Gemfile.activesupport-head +5 -0
- data/lib/working_hours/computation.rb +186 -0
- data/lib/working_hours/config.rb +152 -0
- data/lib/working_hours/core_ext/date_and_time.rb +57 -0
- data/lib/working_hours/core_ext/fixnum.rb +15 -0
- data/lib/working_hours/deep_freeze.rb +12 -0
- data/lib/working_hours/duration.rb +44 -0
- data/lib/working_hours/duration_proxy.rb +23 -0
- data/lib/working_hours/module.rb +7 -0
- data/lib/working_hours/version.rb +3 -0
- data/lib/working_hours.rb +3 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/working_hours/computation_spec.rb +316 -0
- data/spec/working_hours/config_spec.rb +231 -0
- data/spec/working_hours/core_ext/date_and_time_spec.rb +181 -0
- data/spec/working_hours/core_ext/fixnum_spec.rb +13 -0
- data/spec/working_hours/duration_proxy_spec.rb +31 -0
- data/spec/working_hours/duration_spec.rb +78 -0
- data/spec/working_hours_spec.rb +14 -0
- data/working_hours.gemspec +28 -0
- metadata +168 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'working_hours/computation'
|
3
|
+
|
4
|
+
module WorkingHours
|
5
|
+
class Duration
|
6
|
+
include Computation
|
7
|
+
|
8
|
+
attr_accessor :value, :kind
|
9
|
+
|
10
|
+
SUPPORTED_KINDS = [:days, :hours, :minutes, :seconds]
|
11
|
+
|
12
|
+
def initialize(value, kind)
|
13
|
+
raise ArgumentError.new("Invalid working time unit: #{kind}") unless SUPPORTED_KINDS.include?(kind)
|
14
|
+
@value = value
|
15
|
+
@kind = kind
|
16
|
+
end
|
17
|
+
|
18
|
+
# Computation methods
|
19
|
+
def until(time = ::Time.current)
|
20
|
+
send("add_#{@kind}", time, -@value)
|
21
|
+
end
|
22
|
+
alias :ago :until
|
23
|
+
|
24
|
+
def since(time = ::Time.current)
|
25
|
+
send("add_#{@kind}", time, @value)
|
26
|
+
end
|
27
|
+
alias :from_now :since
|
28
|
+
|
29
|
+
# Value object methods
|
30
|
+
def -@
|
31
|
+
Duration.new(-value, kind)
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(other)
|
35
|
+
self.class == other.class and kind == other.kind and value == other.value
|
36
|
+
end
|
37
|
+
alias :eql? :==
|
38
|
+
|
39
|
+
def hash
|
40
|
+
[self.class, kind, value].hash
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "working_hours/duration"
|
2
|
+
|
3
|
+
module WorkingHours
|
4
|
+
class DurationProxy
|
5
|
+
|
6
|
+
attr_accessor :value
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
Duration::SUPPORTED_KINDS.each do |kind|
|
13
|
+
define_method kind do
|
14
|
+
Duration.new(@value, kind)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Singular version
|
18
|
+
define_method kind[0..-2] do
|
19
|
+
Duration.new(@value, kind)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
require 'working_hours'
|
9
|
+
require 'timecop'
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.run_all_when_everything_filtered = true
|
13
|
+
config.filter_run :focus
|
14
|
+
|
15
|
+
# Run specs in random order to surface order dependencies. If you find an
|
16
|
+
# order dependency and want to debug it, you can fix the order by providing
|
17
|
+
# the seed, which is printed after each run.
|
18
|
+
# --seed 1234
|
19
|
+
config.order = 'random'
|
20
|
+
|
21
|
+
config.before :each do
|
22
|
+
WorkingHours::Config.reset!
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WorkingHours::Computation do
|
4
|
+
include WorkingHours::Computation
|
5
|
+
|
6
|
+
describe '#add_days' do
|
7
|
+
it 'can add working days to date' do
|
8
|
+
date = Date.new(1991, 11, 15) #Friday
|
9
|
+
expect(add_days(date, 2)).to eq(Date.new(1991, 11, 19)) # Tuesday
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'can substract working days from date' do
|
13
|
+
date = Date.new(1991, 11, 15) #Friday
|
14
|
+
expect(add_days(date, -7)).to eq(Date.new(1991, 11, 6)) # Wednesday
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'can add working days to time' do
|
18
|
+
time = Time.local(1991, 11, 15, 14, 00, 42)
|
19
|
+
expect(add_days(time, 1)).to eq(Time.local(1991, 11, 18, 14, 00, 42)) # Monday
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'can add working days to ActiveSupport::TimeWithZone' do
|
23
|
+
time = Time.utc(1991, 11, 15, 14, 00, 42)
|
24
|
+
time_monday = Time.utc(1991, 11, 18, 14, 00, 42)
|
25
|
+
time_with_zone = ActiveSupport::TimeWithZone.new(time, 'Tokyo')
|
26
|
+
expect(add_days(time_with_zone, 1)).to eq(ActiveSupport::TimeWithZone.new(time_monday, 'Tokyo'))
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'skips non worked days' do
|
30
|
+
time = Date.new(2014, 4, 7) # Monday
|
31
|
+
WorkingHours::Config.working_hours = {mon: {'09:00' => '17:00'}, wed: {'09:00' => '17:00'}}
|
32
|
+
expect(add_days(time, 1)).to eq(Date.new(2014, 4, 9)) # Wednesday
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'skips holidays' do
|
36
|
+
time = Date.new(2014, 4, 7) # Monday
|
37
|
+
WorkingHours::Config.holidays = [Date.new(2014, 4, 8)] # Tuesday
|
38
|
+
expect(add_days(time, 1)).to eq(Date.new(2014, 4, 9)) # Wednesday
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'skips holidays and non worked days' do
|
42
|
+
time = Date.new(2014, 4, 7) # Monday
|
43
|
+
WorkingHours::Config.holidays = [Date.new(2014, 4, 9)] # Wednesday
|
44
|
+
WorkingHours::Config.working_hours = {mon: {'09:00' => '17:00'}, wed: {'09:00' => '17:00'}}
|
45
|
+
expect(add_days(time, 3)).to eq(Date.new(2014, 4, 21))
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'accepts time given from any time zone' do
|
49
|
+
time = Time.utc(1991, 11, 14, 21, 0, 0) # Thursday 21 pm UTC
|
50
|
+
WorkingHours::Config.time_zone = 'Tokyo' # But we are at tokyo, so it's already Friday 6 am
|
51
|
+
monday = Time.new(1991, 11, 18, 6, 0, 0, "+09:00") # so one working day later, we are monday (Tokyo)
|
52
|
+
expect(add_days(time, 1)).to eq(monday)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#add_hours' do
|
57
|
+
it 'adds working hours' do
|
58
|
+
time = Time.utc(1991, 11, 15, 14, 00, 42) # Friday
|
59
|
+
expect(add_hours(time, 2)).to eq(Time.utc(1991, 11, 15, 16, 00, 42))
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'can substract working hours' do
|
63
|
+
time = Time.utc(1991, 11, 18, 14, 00, 42) # Monday
|
64
|
+
expect(add_hours(time, -7)).to eq(Time.utc(1991, 11, 15, 15, 00, 42)) # Friday
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'accepts time given from any time zone' do
|
68
|
+
time = Time.utc(1991, 11, 15, 7, 0, 0) # Friday 7 am UTC
|
69
|
+
WorkingHours::Config.time_zone = 'Tokyo' # But we are at tokyo, so it's already 4 pm
|
70
|
+
monday = Time.new(1991, 11, 18, 11, 0, 0, "+09:00") # so 3 working hours later, we are monday (Tokyo)
|
71
|
+
expect(add_hours(time, 3)).to eq(monday)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'moves correctly with multiple timespans' do
|
75
|
+
WorkingHours::Config.working_hours = {mon: {'07:00' => '12:00', '13:00' => '18:00'}}
|
76
|
+
time = Time.utc(1991, 11, 11, 5) # Monday 6 am UTC
|
77
|
+
expect(add_hours(time, 6)).to eq(Time.utc(1991, 11, 11, 14))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe '#add_minutes' do
|
82
|
+
it 'adds working minutes' do
|
83
|
+
time = Time.utc(1991, 11, 15, 16, 30, 42) # Friday
|
84
|
+
expect(add_minutes(time, 45)).to eq(Time.utc(1991, 11, 18, 9, 15, 42))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '#add_seconds' do
|
89
|
+
it 'adds working seconds' do
|
90
|
+
time = Time.utc(1991, 11, 15, 16, 59, 42) # Friday
|
91
|
+
expect(add_seconds(time, 120)).to eq(Time.utc(1991, 11, 18, 9, 1, 42))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe '#advance_to_working_time' do
|
96
|
+
it 'jumps non-working day' do
|
97
|
+
WorkingHours::Config.holidays = [Date.new(2014, 5, 1)]
|
98
|
+
expect(advance_to_working_time(Time.utc(2014, 5, 1, 12, 0))).to eq(Time.utc(2014, 5, 2, 9, 0))
|
99
|
+
expect(advance_to_working_time(Time.utc(2014, 6, 1, 12, 0))).to eq(Time.utc(2014, 6, 2, 9, 0))
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'returns self during working hours' do
|
103
|
+
expect(advance_to_working_time(Time.utc(2014, 4, 7, 9, 0))).to eq(Time.utc(2014, 4, 7, 9, 0))
|
104
|
+
expect(advance_to_working_time(Time.utc(2014, 4, 7, 16, 59))).to eq(Time.utc(2014, 4, 7, 16, 59))
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'jumps outside working hours' do
|
108
|
+
expect(advance_to_working_time(Time.utc(2014, 4, 7, 8, 59))).to eq(Time.utc(2014, 4, 7, 9, 0))
|
109
|
+
expect(advance_to_working_time(Time.utc(2014, 4, 7, 17, 0))).to eq(Time.utc(2014, 4, 8, 9, 0))
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'move between timespans' do
|
113
|
+
WorkingHours::Config.working_hours = {mon: {'07:00' => '12:00', '13:00' => '18:00'}}
|
114
|
+
expect(advance_to_working_time(Time.utc(2014, 4, 7, 11, 59))).to eq(Time.utc(2014, 4, 7, 11, 59))
|
115
|
+
expect(advance_to_working_time(Time.utc(2014, 4, 7, 12, 0))).to eq(Time.utc(2014, 4, 7, 13, 0))
|
116
|
+
expect(advance_to_working_time(Time.utc(2014, 4, 7, 12, 59))).to eq(Time.utc(2014, 4, 7, 13, 0))
|
117
|
+
expect(advance_to_working_time(Time.utc(2014, 4, 7, 13, 0))).to eq(Time.utc(2014, 4, 7, 13, 0))
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'works with any input timezone (converts to config)' do
|
121
|
+
# Monday 0 am (-09:00) is 9am in UTC time, working time!
|
122
|
+
expect(advance_to_working_time(Time.new(2014, 4, 7, 0, 0, 0 , "-09:00"))).to eq(Time.utc(2014, 4, 7, 9))
|
123
|
+
expect(advance_to_working_time(Time.new(2014, 4, 7, 22, 0, 0 , "+02:00"))).to eq(Time.utc(2014, 4, 8, 9))
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe '#return_to_working_time' do
|
128
|
+
it 'jumps non-working day' do
|
129
|
+
WorkingHours::Config.holidays = [Date.new(2014, 5, 1)]
|
130
|
+
expect(return_to_working_time(Time.utc(2014, 5, 1, 12, 0))).to eq(Time.utc(2014, 4, 30, 17))
|
131
|
+
expect(return_to_working_time(Time.utc(2014, 6, 1, 12, 0))).to eq(Time.utc(2014, 5, 30, 17))
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'returns self during working hours' do
|
135
|
+
expect(return_to_working_time(Time.utc(2014, 4, 7, 9, 1))).to eq(Time.utc(2014, 4, 7, 9, 1))
|
136
|
+
expect(return_to_working_time(Time.utc(2014, 4, 7, 17, 0))).to eq(Time.utc(2014, 4, 7, 17, 0))
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'jumps outside working hours' do
|
140
|
+
expect(return_to_working_time(Time.utc(2014, 4, 7, 17, 1))).to eq(Time.utc(2014, 4, 7, 17, 0))
|
141
|
+
expect(return_to_working_time(Time.utc(2014, 4, 8, 9, 0))).to eq(Time.utc(2014, 4, 7, 17, 0))
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'move between timespans' do
|
145
|
+
WorkingHours::Config.working_hours = {mon: {'07:00' => '12:00', '13:00' => '18:00'}}
|
146
|
+
expect(return_to_working_time(Time.utc(2014, 4, 7, 13, 1))).to eq(Time.utc(2014, 4, 7, 13, 1))
|
147
|
+
expect(return_to_working_time(Time.utc(2014, 4, 7, 13, 0))).to eq(Time.utc(2014, 4, 7, 12, 0))
|
148
|
+
expect(return_to_working_time(Time.utc(2014, 4, 7, 12, 1))).to eq(Time.utc(2014, 4, 7, 12, 0))
|
149
|
+
expect(return_to_working_time(Time.utc(2014, 4, 7, 12, 0))).to eq(Time.utc(2014, 4, 7, 12, 0))
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'works with any input timezone (converts to config)' do
|
153
|
+
# Monday 1 am (-09:00) is 10am in UTC time, working time!
|
154
|
+
expect(return_to_working_time(Time.new(2014, 4, 7, 1, 0, 0 , "-09:00"))).to eq(Time.utc(2014, 4, 7, 10))
|
155
|
+
expect(return_to_working_time(Time.new(2014, 4, 7, 22, 0, 0 , "+02:00"))).to eq(Time.utc(2014, 4, 7, 17))
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe '#working_day?' do
|
160
|
+
it 'returns true on working day' do
|
161
|
+
expect(working_day?(Date.new(2014, 4, 7))).to be(true)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'skips holidays' do
|
165
|
+
WorkingHours::Config.holidays = [Date.new(2014, 5, 1)]
|
166
|
+
expect(working_day?(Date.new(2014, 5, 1))).to be(false)
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'skips non working days' do
|
170
|
+
expect(working_day?(Date.new(2014, 4, 6))).to be(false)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe '#in_working_hours?' do
|
175
|
+
it 'returns false in non-working day' do
|
176
|
+
WorkingHours::Config.holidays = [Date.new(2014, 5, 1)]
|
177
|
+
expect(in_working_hours?(Time.utc(2014, 5, 1, 12, 0))).to be(false)
|
178
|
+
expect(in_working_hours?(Time.utc(2014, 6, 1, 12, 0))).to be(false)
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'returns true during working hours' do
|
182
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 9, 0))).to be(true)
|
183
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 16, 59))).to be(true)
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'returns false outside working hours' do
|
187
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 8, 59))).to be(false)
|
188
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 17, 0))).to be(false)
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'works with multiple timespan' do
|
192
|
+
WorkingHours::Config.working_hours = {mon: {'07:00' => '12:00', '13:00' => '18:00'}}
|
193
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 11, 59))).to be(true)
|
194
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 12, 0))).to be(false)
|
195
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 12, 59))).to be(false)
|
196
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 13, 0))).to be(true)
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'works with any timezone' do
|
200
|
+
# Monday 00:00 am UTC is 09:00 am Tokyo, working time !
|
201
|
+
WorkingHours::Config.time_zone = 'Tokyo'
|
202
|
+
expect(in_working_hours?(Time.utc(2014, 4, 7, 0, 0))).to be(true)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe '#working_days_between' do
|
207
|
+
it 'returns 0 if same date' do
|
208
|
+
expect(working_days_between(
|
209
|
+
Date.new(1991, 11, 15), # friday
|
210
|
+
Date.new(1991, 11, 15)
|
211
|
+
)).to eq(0)
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'returns 0 if time in same day' do
|
215
|
+
expect(working_days_between(
|
216
|
+
Time.utc(1991, 11, 15, 8), # friday
|
217
|
+
Time.utc(1991, 11, 15, 4)
|
218
|
+
)).to eq(0)
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'counts working days' do
|
222
|
+
expect(working_days_between(
|
223
|
+
Date.new(1991, 11, 15), # friday to friday
|
224
|
+
Date.new(1991, 11, 22)
|
225
|
+
)).to eq(5)
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'returns negative if params are reversed' do
|
229
|
+
expect(working_days_between(
|
230
|
+
Date.new(1991, 11, 22), # friday to friday
|
231
|
+
Date.new(1991, 11, 15)
|
232
|
+
)).to eq(-5)
|
233
|
+
end
|
234
|
+
|
235
|
+
context 'consider time at end of day' do
|
236
|
+
it 'returns 0 from friday to saturday' do
|
237
|
+
expect(working_days_between(
|
238
|
+
Date.new(1991, 11, 15), # friday to saturday
|
239
|
+
Date.new(1991, 11, 16)
|
240
|
+
)).to eq(0)
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'returns 1 from sunday to monday' do
|
244
|
+
expect(working_days_between(
|
245
|
+
Date.new(1991, 11, 17), # sunday to monday
|
246
|
+
Date.new(1991, 11, 18)
|
247
|
+
)).to eq(1)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe '#working_time_between' do
|
253
|
+
it 'returns 0 if same time' do
|
254
|
+
expect(working_time_between(
|
255
|
+
Time.utc(2014, 4, 7, 8),
|
256
|
+
Time.utc(2014, 4, 7, 8)
|
257
|
+
)).to eq(0)
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'returns 0 during non working time' do
|
261
|
+
expect(working_time_between(
|
262
|
+
Time.utc(2014, 4, 11, 20), # Friday evening
|
263
|
+
Time.utc(2014, 4, 14, 5) # Monday early
|
264
|
+
)).to eq(0)
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'ignores miliseconds' do
|
268
|
+
expect(working_time_between(
|
269
|
+
Time.utc(2014, 4, 13, 9, 10, 24.01),
|
270
|
+
Time.utc(2014, 4, 14, 9, 10, 24.02),
|
271
|
+
)).to eq(624)
|
272
|
+
end
|
273
|
+
|
274
|
+
it 'returns distance in same period' do
|
275
|
+
expect(working_time_between(
|
276
|
+
Time.utc(2014, 4, 7, 10),
|
277
|
+
Time.utc(2014, 4, 7, 15)
|
278
|
+
)).to eq(5.hours)
|
279
|
+
end
|
280
|
+
|
281
|
+
it 'returns negative if params are reversed' do
|
282
|
+
expect(working_time_between(
|
283
|
+
Time.utc(2014, 4, 7, 15),
|
284
|
+
Time.utc(2014, 4, 7, 10)
|
285
|
+
)).to eq(-5.hours)
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'returns full day if outside period' do
|
289
|
+
expect(working_time_between(
|
290
|
+
Time.utc(2014, 4, 7, 7),
|
291
|
+
Time.utc(2014, 4, 7, 20)
|
292
|
+
)).to eq(8.hours)
|
293
|
+
end
|
294
|
+
|
295
|
+
it 'handles multiple timespans' do
|
296
|
+
WorkingHours::Config.working_hours = {
|
297
|
+
mon: {'07:00' => '12:00', '13:00' => '18:00'}
|
298
|
+
}
|
299
|
+
expect(working_time_between(
|
300
|
+
Time.utc(2014, 4, 7, 11, 59),
|
301
|
+
Time.utc(2014, 4, 7, 13, 1)
|
302
|
+
)).to eq(2.minutes)
|
303
|
+
expect(working_time_between(
|
304
|
+
Time.utc(2014, 4, 7, 11),
|
305
|
+
Time.utc(2014, 4, 14, 13)
|
306
|
+
)).to eq(11.hours)
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'works with any timezone (converts to config)' do
|
310
|
+
expect(working_time_between(
|
311
|
+
Time.new(2014, 4, 7, 1, 0, 0, "-09:00"), # Monday 10am in UTC
|
312
|
+
Time.new(2014, 4, 7, 15, 0, 0, "-04:00"), # Monday 7pm in UTC
|
313
|
+
)).to eq(7.hours)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WorkingHours::Config do
|
4
|
+
|
5
|
+
describe '.working_hours' do
|
6
|
+
|
7
|
+
let(:config) { WorkingHours::Config.working_hours }
|
8
|
+
|
9
|
+
it 'has a default config' do
|
10
|
+
expect(config).to be_kind_of(Hash)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'is thread safe' do
|
14
|
+
expect {
|
15
|
+
Thread.new {
|
16
|
+
WorkingHours::Config.working_hours = {:mon => {'08:00' => '14:00'}}
|
17
|
+
}.join
|
18
|
+
}.not_to change { WorkingHours::Config.working_hours }
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'is fiber safe' do
|
22
|
+
expect {
|
23
|
+
Fiber.new {
|
24
|
+
WorkingHours::Config.working_hours = {:mon => {'08:00' => '14:00'}}
|
25
|
+
}.resume
|
26
|
+
}.not_to change { WorkingHours::Config.working_hours }
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should have a key for each week day' do
|
30
|
+
[:mon, :tue, :wed, :thu, :fri].each do |d|
|
31
|
+
expect(config[d]).to be_kind_of(Hash)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should be changeable' do
|
36
|
+
time_sheet = {:mon => {'08:00' => '14:00'}}
|
37
|
+
WorkingHours::Config.working_hours = time_sheet
|
38
|
+
expect(config).to eq(time_sheet)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should support multiple timespan per day' do
|
42
|
+
time_sheet = {:mon => {'08:00' => '12:00', '14:00' => '18:00'}}
|
43
|
+
WorkingHours::Config.working_hours = time_sheet
|
44
|
+
expect(config).to eq(time_sheet)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can't be modified once precompiled" do
|
48
|
+
time_sheet = {:mon => {'08:00' => '14:00'}}
|
49
|
+
WorkingHours::Config.working_hours = time_sheet
|
50
|
+
expect {
|
51
|
+
WorkingHours::Config.working_hours[:tue] = {'08:00' => '14:00'}
|
52
|
+
}.to raise_error(RuntimeError, "can't modify frozen Hash")
|
53
|
+
expect {
|
54
|
+
WorkingHours::Config.working_hours[:mon]['08:00'] = '15:00'
|
55
|
+
}.to raise_error(RuntimeError, "can't modify frozen Hash")
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'validations' do
|
59
|
+
it 'rejects empty hash' do
|
60
|
+
expect {
|
61
|
+
WorkingHours::Config.working_hours = {}
|
62
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "No working hours given")
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'rejects invalid day' do
|
66
|
+
expect {
|
67
|
+
WorkingHours::Config.working_hours = {:mon => 1, 'tuesday' => 2, 'wed' => 3}
|
68
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid day identifier(s): tuesday, wed - must be 3 letter symbols")
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'rejects other type than hash' do
|
72
|
+
expect {
|
73
|
+
WorkingHours::Config.working_hours = {:mon => []}
|
74
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid type for `mon`: Array - must be Hash")
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'rejects empty range' do
|
78
|
+
expect {
|
79
|
+
WorkingHours::Config.working_hours = {:mon => {}}
|
80
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "No working hours given for day `mon`")
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'rejects invalid time format' do
|
84
|
+
expect {
|
85
|
+
WorkingHours::Config.working_hours = {:mon => {'8:0' => '12:00'}}
|
86
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid time: 8:0 - must be 'HH:MM'")
|
87
|
+
|
88
|
+
expect {
|
89
|
+
WorkingHours::Config.working_hours = {:mon => {'08:00' => '24:00'}}
|
90
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid time: 24:00 - must be 'HH:MM'")
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'rejects invalid range' do
|
94
|
+
expect {
|
95
|
+
WorkingHours::Config.working_hours = {:mon => {'12:30' => '12:00'}}
|
96
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid range: 12:30 => 12:00 - ends before it starts")
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'rejects overlapping range' do
|
100
|
+
expect {
|
101
|
+
WorkingHours::Config.working_hours = {:mon => {'08:00' => '13:00', '12:00' => '18:00'}}
|
102
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid range: 12:00 => 18:00 - overlaps previous range")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '.holidays' do
|
108
|
+
let (:config) { WorkingHours::Config.holidays }
|
109
|
+
|
110
|
+
it 'has a default config' do
|
111
|
+
expect(config).to eq([])
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should be changeable' do
|
115
|
+
WorkingHours::Config.holidays = [Date.today]
|
116
|
+
expect(config).to eq([Date.today])
|
117
|
+
end
|
118
|
+
|
119
|
+
it "can't be modified once precompiled" do
|
120
|
+
expect {
|
121
|
+
WorkingHours::Config.holidays << Date.today
|
122
|
+
}.to raise_error(RuntimeError, "can't modify frozen Array")
|
123
|
+
end
|
124
|
+
|
125
|
+
describe 'validation' do
|
126
|
+
it 'rejects other type than array' do
|
127
|
+
expect {
|
128
|
+
WorkingHours::Config.holidays = {}
|
129
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid type for holidays: Hash - must be Array")
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'rejects invalid day' do
|
133
|
+
expect {
|
134
|
+
WorkingHours::Config.holidays = [Date.today, 42]
|
135
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid holiday: 42 - must be Date")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe '.time_zone' do
|
141
|
+
let (:config) { WorkingHours::Config.time_zone }
|
142
|
+
|
143
|
+
it 'defaults to UTC' do
|
144
|
+
expect(config).to eq(ActiveSupport::TimeZone['UTC'])
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should accept a String' do
|
148
|
+
WorkingHours::Config.time_zone = 'Tokyo'
|
149
|
+
expect(config).to eq(ActiveSupport::TimeZone['Tokyo'])
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'should accept a TimeZone' do
|
153
|
+
WorkingHours::Config.time_zone = ActiveSupport::TimeZone['Tokyo']
|
154
|
+
expect(config).to eq(ActiveSupport::TimeZone['Tokyo'])
|
155
|
+
end
|
156
|
+
|
157
|
+
it "can't be modified once precompiled" do
|
158
|
+
expect {
|
159
|
+
WorkingHours::Config.time_zone.instance_variable_set(:@name, 'Bordeaux')
|
160
|
+
}.to raise_error(RuntimeError, "can't modify frozen ActiveSupport::TimeZone")
|
161
|
+
end
|
162
|
+
|
163
|
+
describe 'validation' do
|
164
|
+
it 'rejects invalid types' do
|
165
|
+
expect {
|
166
|
+
WorkingHours::Config.time_zone = 02
|
167
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid time zone: 2 - must be String or ActiveSupport::TimeZone")
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'rejects unknown time zone' do
|
171
|
+
expect {
|
172
|
+
WorkingHours::Config.time_zone = 'Bordeaux'
|
173
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Unknown time zone: Bordeaux")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe '.precompiled' do
|
179
|
+
subject { WorkingHours::Config.precompiled }
|
180
|
+
|
181
|
+
it 'computes an optimized version' do
|
182
|
+
expect(subject).to eq({
|
183
|
+
:working_hours => [nil, {32400=>61200}, {32400=>61200}, {32400=>61200}, {32400=>61200}, {32400=>61200}],
|
184
|
+
:holidays => Set.new([]),
|
185
|
+
:time_zone => ActiveSupport::TimeZone['UTC']
|
186
|
+
})
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'changes if working_hours changes' do
|
190
|
+
expect {
|
191
|
+
WorkingHours::Config.working_hours = {:mon => {'08:00' => '14:00'}}
|
192
|
+
}.to change {
|
193
|
+
WorkingHours::Config.precompiled[:working_hours]
|
194
|
+
}.from(
|
195
|
+
[nil, {32400=>61200}, {32400=>61200}, {32400=>61200}, {32400=>61200}, {32400=>61200}]
|
196
|
+
).to(
|
197
|
+
[nil, {28800=>50400}]
|
198
|
+
)
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'changes if time_zone changes' do
|
202
|
+
expect {
|
203
|
+
WorkingHours::Config.time_zone = 'Tokyo'
|
204
|
+
}.to change {
|
205
|
+
WorkingHours::Config.precompiled[:time_zone]
|
206
|
+
}.from(ActiveSupport::TimeZone['UTC']).to(ActiveSupport::TimeZone['Tokyo'])
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'changes if holidays changes' do
|
210
|
+
expect {
|
211
|
+
WorkingHours::Config.holidays = [Date.new(2014, 8, 1), Date.new(2014, 7, 1)]
|
212
|
+
}.to change {
|
213
|
+
WorkingHours::Config.precompiled[:holidays]
|
214
|
+
}.from(Set.new([])).to(Set.new([Date.new(2014, 8, 1), Date.new(2014, 7, 1)]))
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'changes if config is reset' do
|
218
|
+
WorkingHours::Config.time_zone = 'Tokyo'
|
219
|
+
expect {
|
220
|
+
WorkingHours::Config.reset!
|
221
|
+
}.to change {
|
222
|
+
WorkingHours::Config.precompiled[:time_zone]
|
223
|
+
}.from(ActiveSupport::TimeZone['Tokyo']).to(ActiveSupport::TimeZone['UTC'])
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'is computed only once' do
|
227
|
+
expect(WorkingHours::Config).to receive(:compile_time).exactly(10).times
|
228
|
+
3.times { WorkingHours::Config.precompiled }
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|