working_hours 1.3.2 → 1.4.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 +4 -4
- data/CHANGELOG.md +4 -1
- data/README.md +11 -0
- data/lib/working_hours/computation.rb +15 -8
- data/lib/working_hours/config.rb +51 -16
- data/lib/working_hours/version.rb +1 -1
- data/spec/working_hours/computation_spec.rb +184 -4
- data/spec/working_hours/config_spec.rb +128 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2f3a760854612e389ff263f670c2ddbd3206c5895f13a753267e44a92ca12f1
|
4
|
+
data.tar.gz: f9f30c0aaf3c7004a68b7f18ef39767e30c497bedc14361c2ad450894272997e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c63b351486621dca69deb0b827dc51024f74b4304e6639c9272b01d5a5fd9a58013e1d9874ed13466ce6f5fb60ccab67c9f2712a0af2b4d5e4a94bbf214ac996
|
7
|
+
data.tar.gz: 8662b6a40f4ee6bc734cd2f87eecf1e6b51a474638dc5b9a867525cbdc0c7a4b329a34054fff395e71e36b5b93e26d01d3205b2965fd0573c70bbe764c1e450b
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
-
[Compare master with v1.
|
3
|
+
[Compare master with v1.4.0](https://github.com/intrepidd/working_hours/compare/v1.4.0...master)
|
4
|
+
|
5
|
+
# v1.4.0
|
6
|
+
* New config option: holiday_hours - [#37](https://github.com/Intrepidd/working_hours/pull/37)
|
4
7
|
|
5
8
|
# v1.3.2
|
6
9
|
* Improve support for time shifts - [#46](https://github.com/Intrepidd/working_hours/pull/46)
|
data/README.md
CHANGED
@@ -104,6 +104,17 @@ end
|
|
104
104
|
- ``holidays``
|
105
105
|
- ``time_zone``
|
106
106
|
|
107
|
+
### Holiday hours
|
108
|
+
Sometimes you need to configure different working hours as a one-off, e.g. the working day might end earlier on Christmas Eve.
|
109
|
+
|
110
|
+
You can configure this with the `holiday_hours` option, either as an override on the existing working hours, or as a set of hours that *are* being worked on a holiday day.
|
111
|
+
|
112
|
+
If *any* hours are set for a calendar day in `holiday_hours`, then the `working_hours` for that day will be ignored, and only the entries in `holiday_hours` taken into consideration.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
# Configure holiday hours
|
116
|
+
WorkingHours::Config.holiday_hours = {Date.new(2020, 12, 24) => {'09:00' => '12:00', '13:00' => '15:00'}}
|
117
|
+
```
|
107
118
|
|
108
119
|
## No core extensions / monkey patching
|
109
120
|
|
@@ -40,7 +40,7 @@ module WorkingHours
|
|
40
40
|
time = advance_to_working_time(time, config: config)
|
41
41
|
# look at working ranges
|
42
42
|
time_in_day = time.seconds_since_midnight
|
43
|
-
config
|
43
|
+
working_hours_for(time, config: config).each do |from, to|
|
44
44
|
if time_in_day >= from and time_in_day < to
|
45
45
|
# take all we can
|
46
46
|
take = [to - time_in_day, seconds].min
|
@@ -56,7 +56,8 @@ module WorkingHours
|
|
56
56
|
time = return_to_exact_working_time(time, config: config)
|
57
57
|
# look at working ranges
|
58
58
|
time_in_day = time.seconds_since_midnight
|
59
|
-
|
59
|
+
|
60
|
+
working_hours_for(time, config: config).reverse_each do |from, to|
|
60
61
|
if time_in_day > from and time_in_day <= to
|
61
62
|
# take all we can
|
62
63
|
take = [time_in_day - from, -seconds].min
|
@@ -80,7 +81,8 @@ module WorkingHours
|
|
80
81
|
end
|
81
82
|
# find first working range after time
|
82
83
|
time_in_day = time.seconds_since_midnight
|
83
|
-
|
84
|
+
|
85
|
+
working_hours_for(time, config: config).each do |from, to|
|
84
86
|
return time if time_in_day >= from and time_in_day < to
|
85
87
|
return move_time_of_day(time, from) if from >= time_in_day
|
86
88
|
end
|
@@ -99,7 +101,7 @@ module WorkingHours
|
|
99
101
|
end
|
100
102
|
# find next working range after time
|
101
103
|
time_in_day = time.seconds_since_midnight
|
102
|
-
config
|
104
|
+
working_hours_for(time, config: config).each do |from, to|
|
103
105
|
return move_time_of_day(time, to) if time_in_day < to
|
104
106
|
end
|
105
107
|
# if none is found, go to next day and loop
|
@@ -129,7 +131,7 @@ module WorkingHours
|
|
129
131
|
end
|
130
132
|
# find last working range before time
|
131
133
|
time_in_day = time.seconds_since_midnight
|
132
|
-
config
|
134
|
+
working_hours_for(time, config: config).reverse_each do |from, to|
|
133
135
|
return time if time_in_day > from and time_in_day <= to
|
134
136
|
return move_time_of_day(time, to) if to <= time_in_day
|
135
137
|
end
|
@@ -141,7 +143,9 @@ module WorkingHours
|
|
141
143
|
def working_day? time, config: nil
|
142
144
|
config ||= wh_config
|
143
145
|
time = in_config_zone(time, config: config)
|
144
|
-
|
146
|
+
|
147
|
+
(config[:working_hours][time.wday].present? && !config[:holidays].include?(time.to_date)) ||
|
148
|
+
config[:holiday_hours].include?(time.to_date)
|
145
149
|
end
|
146
150
|
|
147
151
|
def in_working_hours? time, config: nil
|
@@ -149,7 +153,7 @@ module WorkingHours
|
|
149
153
|
time = in_config_zone(time, config: config)
|
150
154
|
return false if not working_day?(time, config: config)
|
151
155
|
time_in_day = time.seconds_since_midnight
|
152
|
-
config
|
156
|
+
working_hours_for(time, config: config).each do |from, to|
|
153
157
|
return true if time_in_day >= from and time_in_day < to
|
154
158
|
end
|
155
159
|
false
|
@@ -183,7 +187,7 @@ module WorkingHours
|
|
183
187
|
from_was = from
|
184
188
|
# look at working ranges
|
185
189
|
time_in_day = from.seconds_since_midnight
|
186
|
-
config
|
190
|
+
working_hours_for(from, config: config).each do |begins, ends|
|
187
191
|
if time_in_day >= begins and time_in_day < ends
|
188
192
|
if (to - from) > (ends - time_in_day)
|
189
193
|
# take all the range and continue
|
@@ -246,5 +250,8 @@ module WorkingHours
|
|
246
250
|
end
|
247
251
|
end
|
248
252
|
|
253
|
+
def working_hours_for(time, config:)
|
254
|
+
config[:holiday_hours][time.to_date] || config[:working_hours][time.wday]
|
255
|
+
end
|
249
256
|
end
|
250
257
|
end
|
data/lib/working_hours/config.rb
CHANGED
@@ -4,13 +4,11 @@ module WorkingHours
|
|
4
4
|
InvalidConfiguration = Class.new StandardError
|
5
5
|
|
6
6
|
class Config
|
7
|
-
|
8
7
|
TIME_FORMAT = /\A([0-2][0-9])\:([0-5][0-9])(?:\:([0-5][0-9]))?\z/
|
9
8
|
DAYS_OF_WEEK = [:sun, :mon, :tue, :wed, :thu, :fri, :sat]
|
10
9
|
MIDNIGHT = Rational('86399.999999')
|
11
10
|
|
12
11
|
class << self
|
13
|
-
|
14
12
|
def working_hours
|
15
13
|
config[:working_hours]
|
16
14
|
end
|
@@ -33,9 +31,26 @@ module WorkingHours
|
|
33
31
|
config.delete :precompiled
|
34
32
|
end
|
35
33
|
|
34
|
+
def holiday_hours
|
35
|
+
config[:holiday_hours]
|
36
|
+
end
|
37
|
+
|
38
|
+
def holiday_hours=(val)
|
39
|
+
validate_holiday_hours! val
|
40
|
+
config[:holiday_hours] = val
|
41
|
+
global_config[:holiday_hours] = val
|
42
|
+
config.delete :precompiled
|
43
|
+
end
|
44
|
+
|
36
45
|
# Returns an optimized for computing version
|
37
46
|
def precompiled
|
38
|
-
config_hash = [
|
47
|
+
config_hash = [
|
48
|
+
config[:working_hours],
|
49
|
+
config[:holiday_hours],
|
50
|
+
config[:holidays],
|
51
|
+
config[:time_zone]
|
52
|
+
].hash
|
53
|
+
|
39
54
|
if config_hash != config[:config_hash]
|
40
55
|
config[:config_hash] = config_hash
|
41
56
|
config.delete :precompiled
|
@@ -43,14 +58,21 @@ module WorkingHours
|
|
43
58
|
|
44
59
|
config[:precompiled] ||= begin
|
45
60
|
validate_working_hours! config[:working_hours]
|
61
|
+
validate_holiday_hours! config[:holiday_hours]
|
46
62
|
validate_holidays! config[:holidays]
|
47
63
|
validate_time_zone! config[:time_zone]
|
48
|
-
compiled = { working_hours: Array.new(7) { Hash.new } }
|
64
|
+
compiled = { working_hours: Array.new(7) { Hash.new }, holiday_hours: {} }
|
49
65
|
working_hours.each do |day, hours|
|
50
66
|
hours.each do |start, finish|
|
51
67
|
compiled[:working_hours][DAYS_OF_WEEK.index(day)][compile_time(start)] = compile_time(finish)
|
52
68
|
end
|
53
69
|
end
|
70
|
+
holiday_hours.each do |day, hours|
|
71
|
+
compiled[:holiday_hours][day] = {}
|
72
|
+
hours.each do |start, finish|
|
73
|
+
compiled[:holiday_hours][day][compile_time(start)] = compile_time(finish)
|
74
|
+
end
|
75
|
+
end
|
54
76
|
compiled[:holidays] = Set.new(holidays)
|
55
77
|
compiled[:time_zone] = time_zone
|
56
78
|
compiled
|
@@ -72,16 +94,19 @@ module WorkingHours
|
|
72
94
|
Thread.current[:working_hours] = default_config
|
73
95
|
end
|
74
96
|
|
75
|
-
def with_config(working_hours: nil, holidays: nil, time_zone: nil)
|
97
|
+
def with_config(working_hours: nil, holiday_hours: nil, holidays: nil, time_zone: nil)
|
76
98
|
original_working_hours = self.working_hours
|
99
|
+
original_holiday_hours = self.holiday_hours
|
77
100
|
original_holidays = self.holidays
|
78
101
|
original_time_zone = self.time_zone
|
79
102
|
self.working_hours = working_hours if working_hours
|
103
|
+
self.holiday_hours = holiday_hours if holiday_hours
|
80
104
|
self.holidays = holidays if holidays
|
81
105
|
self.time_zone = time_zone if time_zone
|
82
106
|
yield
|
83
107
|
ensure
|
84
108
|
self.working_hours = original_working_hours
|
109
|
+
self.holiday_hours = original_holiday_hours
|
85
110
|
self.holidays = original_holidays
|
86
111
|
self.time_zone = original_time_zone
|
87
112
|
end
|
@@ -105,6 +130,7 @@ module WorkingHours
|
|
105
130
|
thu: {'09:00' => '17:00'},
|
106
131
|
fri: {'09:00' => '17:00'}
|
107
132
|
},
|
133
|
+
holiday_hours: {},
|
108
134
|
holidays: [],
|
109
135
|
time_zone: ActiveSupport::TimeZone['UTC']
|
110
136
|
}
|
@@ -120,14 +146,8 @@ module WorkingHours
|
|
120
146
|
time
|
121
147
|
end
|
122
148
|
|
123
|
-
def
|
124
|
-
|
125
|
-
raise InvalidConfiguration.new "No working hours given"
|
126
|
-
end
|
127
|
-
if (invalid_keys = (week.keys - DAYS_OF_WEEK)).any?
|
128
|
-
raise InvalidConfiguration.new "Invalid day identifier(s): #{invalid_keys.join(', ')} - must be 3 letter symbols"
|
129
|
-
end
|
130
|
-
week.each do |day, hours|
|
149
|
+
def validate_hours! dates
|
150
|
+
dates.each do |day, hours|
|
131
151
|
if not hours.is_a? Hash
|
132
152
|
raise InvalidConfiguration.new "Invalid type for `#{day}`: #{hours.class} - must be Hash"
|
133
153
|
elsif hours.empty?
|
@@ -151,6 +171,23 @@ module WorkingHours
|
|
151
171
|
end
|
152
172
|
end
|
153
173
|
|
174
|
+
def validate_working_hours! week
|
175
|
+
if week.empty?
|
176
|
+
raise InvalidConfiguration.new "No working hours given"
|
177
|
+
end
|
178
|
+
if (invalid_keys = (week.keys - DAYS_OF_WEEK)).any?
|
179
|
+
raise InvalidConfiguration.new "Invalid day identifier(s): #{invalid_keys.join(', ')} - must be 3 letter symbols"
|
180
|
+
end
|
181
|
+
validate_hours!(week)
|
182
|
+
end
|
183
|
+
|
184
|
+
def validate_holiday_hours! days
|
185
|
+
if (invalid_keys = (days.keys.reject{ |day| day.is_a?(Date) })).any?
|
186
|
+
raise InvalidConfiguration.new "Invalid day identifier(s): #{invalid_keys.join(', ')} - must be a Date object"
|
187
|
+
end
|
188
|
+
validate_hours!(days)
|
189
|
+
end
|
190
|
+
|
154
191
|
def validate_holidays! holidays
|
155
192
|
if not holidays.respond_to?(:to_a)
|
156
193
|
raise InvalidConfiguration.new "Invalid type for holidays: #{holidays.class} - must act like an array"
|
@@ -175,12 +212,10 @@ module WorkingHours
|
|
175
212
|
end
|
176
213
|
res
|
177
214
|
end
|
178
|
-
|
179
215
|
end
|
180
216
|
|
181
217
|
private
|
182
218
|
|
183
|
-
def initialize
|
184
|
-
end
|
219
|
+
def initialize; end
|
185
220
|
end
|
186
221
|
end
|
@@ -111,7 +111,7 @@ describe WorkingHours::Computation do
|
|
111
111
|
expect(add_seconds(time, 120)).to eq(Time.utc(1991, 11, 18, 9, 1, 42))
|
112
112
|
end
|
113
113
|
|
114
|
-
it '
|
114
|
+
it 'calls precompiled only once' do
|
115
115
|
precompiled = WorkingHours::Config.precompiled
|
116
116
|
expect(WorkingHours::Config).to receive(:precompiled).once.and_return(precompiled) # in_config_zone and add_seconds
|
117
117
|
time = Time.utc(1991, 11, 15, 16, 59, 42) # Friday
|
@@ -130,12 +130,94 @@ describe WorkingHours::Computation do
|
|
130
130
|
expect(add_seconds(time, -60)).to eq(Time.utc(2014, 4, 7, 23, 59, 00))
|
131
131
|
end
|
132
132
|
|
133
|
+
context 'with holiday hours' do
|
134
|
+
before do
|
135
|
+
WorkingHours::Config.working_hours = { thu: { '08:00' => '18:00' }, fri: { '08:00' => '18:00' } }
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'with a later starting hour' do
|
139
|
+
before do
|
140
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 27) => { '10:00' => '18:00' } }
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'adds working seconds' do
|
144
|
+
time = Time.utc(2019, 12, 27, 9)
|
145
|
+
expect(add_seconds(time, 120)).to eq(Time.utc(2019, 12, 27, 10, 2))
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'removes working seconds' do
|
149
|
+
time = Time.utc(2019, 12, 27, 9)
|
150
|
+
expect(add_seconds(time, -120)).to eq(Time.utc(2019, 12, 26, 17, 58))
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'working back from working hours' do
|
154
|
+
it 'moves to the previous working day' do
|
155
|
+
time = Time.utc(2019, 12, 27, 11)
|
156
|
+
expect(add_seconds(time, -2.hours)).to eq(Time.utc(2019, 12, 26, 17))
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'with an earlier ending hour' do
|
162
|
+
before do
|
163
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 27) => { '08:00' => '17:00' } }
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'adds working seconds' do
|
167
|
+
time = Time.utc(2019, 12, 27, 17, 59)
|
168
|
+
expect(add_seconds(time, 120)).to eq(Time.utc(2020, 1, 2, 8, 2))
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'removes working seconds' do
|
172
|
+
time = Time.utc(2019, 12, 27, 18)
|
173
|
+
expect(add_seconds(time, -120)).to eq(Time.utc(2019, 12, 27, 16, 58))
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'working forward from working hours' do
|
177
|
+
it 'moves to the next working day' do
|
178
|
+
time = Time.utc(2019, 12, 27, 16)
|
179
|
+
expect(add_seconds(time, 2.hours)).to eq(Time.utc(2020, 1, 2, 9))
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context 'with an earlier starting time in the second set of hours within a day' do
|
185
|
+
before do
|
186
|
+
WorkingHours::Config.working_hours = { thu: { '08:00' => '18:00' }, fri: { '08:00' => '12:00', '13:00' => '18:00' } }
|
187
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 27) => { '08:00' => '12:00', '14:00' => '18:00' } }
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'adds working seconds' do
|
191
|
+
time = Time.utc(2019, 12, 27, 12, 59)
|
192
|
+
expect(add_seconds(time, 120)).to eq(Time.utc(2019, 12, 27, 14, 2))
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'removes working seconds' do
|
196
|
+
time = Time.utc(2019, 12, 27, 14)
|
197
|
+
expect(add_seconds(time, -120)).to eq(Time.utc(2019, 12, 27, 11, 58))
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'from morning to afternoon' do
|
201
|
+
it 'takes into account the additional hour for lunch set in `holiday_hours`' do
|
202
|
+
time = Time.utc(2019, 12, 27, 10)
|
203
|
+
expect(add_seconds(time, 4.hours)).to eq(Time.utc(2019, 12, 27, 16))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
context 'from afternoon to morning' do
|
208
|
+
it 'takes into account the additional hour for lunch set in `holiday_hours`' do
|
209
|
+
time = Time.utc(2019, 12, 27, 16)
|
210
|
+
expect(add_seconds(time, -4.hours)).to eq(Time.utc(2019, 12, 27, 10))
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
133
216
|
it 'honors miliseconds in the base time and increment (but return rounded result)' do
|
134
217
|
# Rounding the base time or increments before the end would yield a wrong result
|
135
218
|
time = Time.utc(1991, 11, 15, 16, 59, 42.25) # +250ms
|
136
219
|
expect(add_seconds(time, 120.4)).to eq(Time.utc(1991, 11, 18, 9, 1, 43))
|
137
220
|
end
|
138
|
-
|
139
221
|
end
|
140
222
|
|
141
223
|
describe '#advance_to_working_time' do
|
@@ -174,6 +256,12 @@ describe WorkingHours::Computation do
|
|
174
256
|
expect(advance_to_working_time(Time.new(2014, 4, 7, 0, 0, 0)).zone).to eq('JST')
|
175
257
|
end
|
176
258
|
|
259
|
+
it 'jumps outside holiday hours' do
|
260
|
+
WorkingHours::Config.working_hours = { fri: { '08:00' => '18:00' } }
|
261
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 27) => { '10:00' => '18:00' } }
|
262
|
+
expect(advance_to_working_time(Time.utc(2019, 12, 27, 9))).to eq(Time.utc(2019, 12, 27, 10))
|
263
|
+
end
|
264
|
+
|
177
265
|
it 'do not leak nanoseconds when advancing' do
|
178
266
|
expect(advance_to_working_time(Time.utc(2014, 4, 7, 5, 0, 0, 123456.789))).to eq(Time.utc(2014, 4, 7, 9, 0, 0, 0))
|
179
267
|
end
|
@@ -206,7 +294,6 @@ describe WorkingHours::Computation do
|
|
206
294
|
end
|
207
295
|
|
208
296
|
describe '#advance_to_closing_time' do
|
209
|
-
|
210
297
|
it 'jumps non-working day' do
|
211
298
|
WorkingHours::Config.holidays = [Date.new(2014, 5, 1)]
|
212
299
|
holiday = Time.utc(2014, 5, 1, 12, 0)
|
@@ -307,6 +394,22 @@ describe WorkingHours::Computation do
|
|
307
394
|
expect(advance_to_closing_time(Time.new(2014, 4, 7, 0, 0, 0)).zone).to eq('JST')
|
308
395
|
end
|
309
396
|
|
397
|
+
context 'with holiday hours' do
|
398
|
+
before do
|
399
|
+
WorkingHours::Config.working_hours = { thu: { '08:00' => '18:00' }, fri: { '08:00' => '18:00' } }
|
400
|
+
end
|
401
|
+
|
402
|
+
it 'takes into account reduced holiday closing' do
|
403
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 27) => { '10:00' => '17:00' } }
|
404
|
+
expect(advance_to_closing_time(Time.new(2019, 12, 26, 20))).to eq(Time.new(2019, 12, 27, 17))
|
405
|
+
end
|
406
|
+
|
407
|
+
it 'takes into account extended holiday closing' do
|
408
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 26) => { '10:00' => '21:00' } }
|
409
|
+
expect(advance_to_closing_time(Time.new(2019, 12, 26, 20))).to eq(Time.new(2019, 12, 26, 21))
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
310
413
|
it 'do not leak nanoseconds when advancing' do
|
311
414
|
expect(advance_to_closing_time(Time.utc(2014, 4, 7, 5, 0, 0, 123456.789))).to eq(Time.utc(2014, 4, 7, 17, 0, 0, 0))
|
312
415
|
end
|
@@ -339,7 +442,6 @@ describe WorkingHours::Computation do
|
|
339
442
|
end
|
340
443
|
|
341
444
|
describe '#next_working_time' do
|
342
|
-
|
343
445
|
it 'jumps non-working day' do
|
344
446
|
WorkingHours::Config.holidays = [Date.new(2014, 5, 1)]
|
345
447
|
holiday = Time.utc(2014, 5, 1, 12, 0)
|
@@ -515,6 +617,23 @@ describe WorkingHours::Computation do
|
|
515
617
|
WorkingHours::Config.time_zone = 'Tokyo'
|
516
618
|
expect(in_working_hours?(Time.utc(2014, 4, 7, 0, 0))).to be(true)
|
517
619
|
end
|
620
|
+
|
621
|
+
context 'with holiday hours' do
|
622
|
+
before do
|
623
|
+
WorkingHours::Config.working_hours = { thu: { '08:00' => '18:00' }, fri: { '08:00' => '18:00' } }
|
624
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 27) => { '10:00' => '20:00' } }
|
625
|
+
end
|
626
|
+
|
627
|
+
it 'returns true during working hours' do
|
628
|
+
expect(in_working_hours?(Time.utc(2019, 12, 26, 9))).to be(true)
|
629
|
+
expect(in_working_hours?(Time.utc(2019, 12, 27, 19))).to be(true)
|
630
|
+
end
|
631
|
+
|
632
|
+
it 'returns false outside working hours' do
|
633
|
+
expect(in_working_hours?(Time.utc(2019, 12, 26, 7))).to be(false)
|
634
|
+
expect(in_working_hours?(Time.utc(2019, 12, 27, 9))).to be(false)
|
635
|
+
end
|
636
|
+
end
|
518
637
|
end
|
519
638
|
|
520
639
|
describe '#working_days_between' do
|
@@ -706,5 +825,66 @@ describe WorkingHours::Computation do
|
|
706
825
|
end
|
707
826
|
end
|
708
827
|
end
|
828
|
+
|
829
|
+
context 'with holiday hours' do
|
830
|
+
before do
|
831
|
+
WorkingHours::Config.working_hours = { mon: { '08:00' => '18:00' }, tue: { '08:00' => '18:00' } }
|
832
|
+
WorkingHours::Config.holiday_hours = { Date.new(2014, 4, 7) => { '10:00' => '12:00', '14:00' => '17:00' } }
|
833
|
+
end
|
834
|
+
|
835
|
+
context 'time is before the start of holiday hours' do
|
836
|
+
it 'does not count holiday hours as working time' do
|
837
|
+
expect(working_time_between(
|
838
|
+
Time.utc(2014, 4, 7, 8),
|
839
|
+
Time.utc(2014, 4, 7, 9)
|
840
|
+
)).to eq(0)
|
841
|
+
end
|
842
|
+
end
|
843
|
+
|
844
|
+
context 'time is between holiday hours' do
|
845
|
+
it 'does not count holiday hours as working time' do
|
846
|
+
expect(working_time_between(
|
847
|
+
Time.utc(2014, 4, 7, 13),
|
848
|
+
Time.utc(2014, 4, 7, 13, 30)
|
849
|
+
)).to eq(0)
|
850
|
+
end
|
851
|
+
end
|
852
|
+
|
853
|
+
context 'time is after the end of holiday hours' do
|
854
|
+
it 'does not count holiday hours as working time' do
|
855
|
+
expect(working_time_between(
|
856
|
+
Time.utc(2014, 4, 7, 19),
|
857
|
+
Time.utc(2014, 4, 7, 20)
|
858
|
+
)).to eq(0)
|
859
|
+
end
|
860
|
+
end
|
861
|
+
|
862
|
+
context 'time is before the start of the holiday hours' do
|
863
|
+
it 'does not count holiday hours as working time' do
|
864
|
+
expect(working_time_between(
|
865
|
+
Time.utc(2014, 4, 7, 9),
|
866
|
+
Time.utc(2014, 4, 7, 12)
|
867
|
+
)).to eq(7200)
|
868
|
+
end
|
869
|
+
end
|
870
|
+
|
871
|
+
context 'time crosses overridden holiday hours at midday' do
|
872
|
+
it 'does not count holiday hours as working time' do
|
873
|
+
expect(working_time_between(
|
874
|
+
Time.utc(2014, 4, 7, 9),
|
875
|
+
Time.utc(2014, 4, 7, 14)
|
876
|
+
)).to eq(7200)
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
880
|
+
context 'time crosses overridden holiday hours at midday' do
|
881
|
+
it 'does not count holiday hours as working time' do
|
882
|
+
expect(working_time_between(
|
883
|
+
Time.utc(2014, 4, 7, 12),
|
884
|
+
Time.utc(2014, 4, 7, 18)
|
885
|
+
)).to eq(10800)
|
886
|
+
end
|
887
|
+
end
|
888
|
+
end
|
709
889
|
end
|
710
890
|
end
|
@@ -3,7 +3,6 @@ require 'spec_helper'
|
|
3
3
|
describe WorkingHours::Config do
|
4
4
|
|
5
5
|
describe '.working_hours' do
|
6
|
-
|
7
6
|
let(:config) { WorkingHours::Config.working_hours }
|
8
7
|
let(:config2) { { :mon => { '08:00' => '14:00' } } }
|
9
8
|
let(:config3) { { :tue => { '10:00' => '16:00' } } }
|
@@ -155,7 +154,132 @@ describe WorkingHours::Config do
|
|
155
154
|
1.working.hour.ago
|
156
155
|
}.to raise_error(WorkingHours::InvalidConfiguration, 'Invalid type for `mon`: String - must be Hash')
|
157
156
|
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '.holiday_hours' do
|
161
|
+
let(:config) { WorkingHours::Config.holiday_hours }
|
162
|
+
let(:config2) { { Date.new(2019, 12, 1) => { '08:00' => '14:00' } } }
|
163
|
+
let(:config3) { { Date.new(2019, 12, 2) => { '10:00' => '16:00' } } }
|
164
|
+
|
165
|
+
it 'has a default config' do
|
166
|
+
expect(config).to be_kind_of(Hash)
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'is thread safe' do
|
170
|
+
expect(WorkingHours::Config.holiday_hours).to eq(config)
|
171
|
+
|
172
|
+
thread = Thread.new do
|
173
|
+
WorkingHours::Config.holiday_hours = config2
|
174
|
+
expect(WorkingHours::Config.holiday_hours).to eq(config2)
|
175
|
+
Thread.stop
|
176
|
+
expect(WorkingHours::Config.holiday_hours).to eq(config2)
|
177
|
+
end
|
178
|
+
|
179
|
+
expect {
|
180
|
+
sleep 0.1 # let the thread begin its execution
|
181
|
+
}.not_to change { WorkingHours::Config.holiday_hours }.from(config)
|
182
|
+
|
183
|
+
expect {
|
184
|
+
WorkingHours::Config.holiday_hours = config3
|
185
|
+
}.to change { WorkingHours::Config.holiday_hours }.from(config).to(config3)
|
186
|
+
|
187
|
+
expect {
|
188
|
+
thread.run
|
189
|
+
thread.join
|
190
|
+
}.not_to change { WorkingHours::Config.holiday_hours }.from(config3)
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'is fiber safe' do
|
194
|
+
expect(WorkingHours::Config.holiday_hours).to eq(config)
|
195
|
+
|
196
|
+
fiber = Fiber.new do
|
197
|
+
WorkingHours::Config.holiday_hours = config2
|
198
|
+
expect(WorkingHours::Config.holiday_hours).to eq(config2)
|
199
|
+
Fiber.yield
|
200
|
+
expect(WorkingHours::Config.holiday_hours).to eq(config2)
|
201
|
+
end
|
202
|
+
|
203
|
+
expect {
|
204
|
+
fiber.resume
|
205
|
+
}.not_to change { WorkingHours::Config.holiday_hours }.from(config)
|
206
|
+
|
207
|
+
expect {
|
208
|
+
WorkingHours::Config.holiday_hours = config3
|
209
|
+
}.to change { WorkingHours::Config.holiday_hours }.from(config).to(config3)
|
210
|
+
|
211
|
+
expect {
|
212
|
+
fiber.resume
|
213
|
+
}.not_to change { WorkingHours::Config.holiday_hours }.from(config3)
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'is initialized from last known global config' do
|
217
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => { '08:00' => '14:00' } }
|
218
|
+
Thread.new {
|
219
|
+
expect(WorkingHours::Config.holiday_hours).to match Date.new(2019, 12, 1) => {'08:00' => '14:00'}
|
220
|
+
}.join
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'should support multiple timespan per day' do
|
224
|
+
time_sheet = { Date.new(2019, 12, 1) => { '08:00' => '12:00', '14:00' => '18:00' } }
|
225
|
+
WorkingHours::Config.holiday_hours = time_sheet
|
226
|
+
expect(config).to eq(time_sheet)
|
227
|
+
end
|
228
|
+
|
229
|
+
describe 'validations' do
|
230
|
+
it 'rejects invalid day' do
|
231
|
+
expect {
|
232
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => 1, 'aaaaaa' => 2 }
|
233
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid day identifier(s): aaaaaa - must be a Date object")
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'rejects other type than hash' do
|
237
|
+
expect {
|
238
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => [] }
|
239
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid type for `2019-12-01`: Array - must be Hash")
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'rejects empty range' do
|
243
|
+
expect {
|
244
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => {} }
|
245
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "No working hours given for day `2019-12-01`")
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'rejects invalid time format' do
|
249
|
+
expect {
|
250
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => { '8:0' => '12:00' } }
|
251
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid time: 8:0 - must be 'HH:MM(:SS)'")
|
252
|
+
|
253
|
+
expect {
|
254
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => { '08:00' => '24:10' }}
|
255
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid time: 24:10 - outside of day")
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'rejects invalid range' do
|
259
|
+
expect {
|
260
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => { '12:30' => '12:00' } }
|
261
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid range: 12:30 => 12:00 - ends before it starts")
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'rejects overlapping range' do
|
265
|
+
expect {
|
266
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => { '08:00' => '13:00', '12:00' => '18:00' } }
|
267
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, "Invalid range: 12:00 => 18:00 - overlaps previous range")
|
268
|
+
end
|
158
269
|
|
270
|
+
it 'does not reject out-of-order, non-overlapping ranges' do
|
271
|
+
expect {
|
272
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => { '10:00' => '11:00', '08:00' => '09:00' } }
|
273
|
+
}.not_to raise_error
|
274
|
+
end
|
275
|
+
|
276
|
+
it 'raises an error when precompiling if working hours are invalid after assignment' do
|
277
|
+
WorkingHours::Config.holiday_hours = { Date.new(2019, 12, 1) => { '10:00' => '11:00', '08:00' => '09:00' } }
|
278
|
+
WorkingHours::Config.holiday_hours[Date.new(2019, 12, 1)] = 'Not correct'
|
279
|
+
expect {
|
280
|
+
1.working.hour.ago
|
281
|
+
}.to raise_error(WorkingHours::InvalidConfiguration, 'Invalid type for `2019-12-01`: String - must be Hash')
|
282
|
+
end
|
159
283
|
end
|
160
284
|
end
|
161
285
|
|
@@ -267,6 +391,7 @@ describe WorkingHours::Config do
|
|
267
391
|
it 'computes an optimized version' do
|
268
392
|
expect(subject).to eq({
|
269
393
|
:working_hours => [{}, {32400=>61200}, {32400=>61200}, {32400=>61200}, {32400=>61200}, {32400=>61200}, {}],
|
394
|
+
:holiday_hours => {},
|
270
395
|
:holidays => Set.new([]),
|
271
396
|
:time_zone => ActiveSupport::TimeZone['UTC']
|
272
397
|
})
|
@@ -283,6 +408,7 @@ describe WorkingHours::Config do
|
|
283
408
|
WorkingHours::Config.working_hours = {:mon => {'20:32:59' => '22:59:59'}}
|
284
409
|
expect(subject).to eq({
|
285
410
|
:working_hours => [{}, {73979 => 82799}, {}, {}, {}, {}, {}],
|
411
|
+
:holiday_hours => {},
|
286
412
|
:holidays => Set.new([]),
|
287
413
|
:time_zone => ActiveSupport::TimeZone['UTC']
|
288
414
|
})
|
@@ -292,6 +418,7 @@ describe WorkingHours::Config do
|
|
292
418
|
WorkingHours::Config.working_hours = {:mon => {'20:00' => '24:00'}}
|
293
419
|
expect(subject).to eq({
|
294
420
|
:working_hours => [{}, {72000 => 86399.999999}, {}, {}, {}, {}, {}],
|
421
|
+
:holiday_hours => {},
|
295
422
|
:holidays => Set.new([]),
|
296
423
|
:time_zone => ActiveSupport::TimeZone['UTC']
|
297
424
|
})
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: working_hours
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adrien Jarthon
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-07-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|