working_hours 1.3.2 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 102b64fd640e32b4164e32d0323d3575bfd720064d02045ff65f7e320b7dd305
4
- data.tar.gz: dea4493b517eda97438e35f3857da9b456ead0e992b6278b33d108f1c38ef7ca
3
+ metadata.gz: d2f3a760854612e389ff263f670c2ddbd3206c5895f13a753267e44a92ca12f1
4
+ data.tar.gz: f9f30c0aaf3c7004a68b7f18ef39767e30c497bedc14361c2ad450894272997e
5
5
  SHA512:
6
- metadata.gz: 88e9102b1a8111aab7b0c5017c93f2eda8c99821499dfe9aa91e89572925b090a95840059a91de0e1114340ae566973a4d67844ebabee4a8f93dd6f11ae94786
7
- data.tar.gz: 5dcdc183274c968c8cf61880870642ec1045092f9f579fb0f14c9ad1e45ffd232e37aa544c0a1c2f4c58dca4a676d1c6856fdd7cf95171e8702201ec44b550bc
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.1](https://github.com/intrepidd/working_hours/compare/v1.3.1...master)
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[:working_hours][time.wday].each do |from, to|
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
- config[:working_hours][time.wday].reverse_each do |from, to|
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
- config[:working_hours][time.wday].each do |from, to|
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[:working_hours][time.wday].each do |from, to|
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[:working_hours][time.wday].reverse_each do |from, to|
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
- config[:working_hours][time.wday].present? and not config[:holidays].include?(time.to_date)
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[:working_hours][time.wday].each do |from, to|
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[:working_hours][from.wday].each do |begins, ends|
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
@@ -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 = [config[:working_hours], config[:holidays], config[:time_zone]].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 validate_working_hours! week
124
- if week.empty?
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
@@ -1,3 +1,3 @@
1
1
  module WorkingHours
2
- VERSION = "1.3.2"
2
+ VERSION = "1.4.0"
3
3
  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 'Calls precompiled only once' do
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.3.2
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-02-20 00:00:00.000000000 Z
12
+ date: 2021-07-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport