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.
@@ -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
@@ -0,0 +1,7 @@
1
+ require 'working_hours/version'
2
+ require 'working_hours/computation'
3
+ require 'working_hours/duration'
4
+
5
+ module WorkingHours
6
+ extend WorkingHours::Computation
7
+ end
@@ -0,0 +1,3 @@
1
+ module WorkingHours
2
+ VERSION = "0.9.0"
3
+ end
@@ -0,0 +1,3 @@
1
+ require "working_hours/module"
2
+ require "working_hours/core_ext/fixnum"
3
+ require "working_hours/core_ext/date_and_time"
@@ -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