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 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