biz 1.5.2 → 1.6.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
  SHA1:
3
- metadata.gz: 27809869df3ee991371e2258e892863d22b5ad2e
4
- data.tar.gz: 184a044cb47ccdb0437c5227da62bcbde65534aa
3
+ metadata.gz: e3d348e477f526c87601d6b59362fbb9e17b2dba
4
+ data.tar.gz: 83019f10ea87e74a41f317dd388dbe9d7ee204b6
5
5
  SHA512:
6
- metadata.gz: 5f9fef46fd079453c482963e9e89b8ef9368451275f4626567b923119bf97c68af7cda4d44f73dcae27cf7f458aeabb980a9d6130dcd5c093beade3c32b48df4
7
- data.tar.gz: f2f7dace5f2eb6b7baff6f6dc275afc7edb606e349ac7cddd7b32273ca3498434e137a1cb6b01ff3bc6d11a2b906baed602a81ac6eb003d1b2b3168b2da86c52
6
+ metadata.gz: c1c66c64493a052cc7d99aef3268cedb1691bbacde44f7cc731345a397e61627a7d589c789b071b7696add9585392ddbbe449cb61a185247e97a14bf6b8e055f
7
+ data.tar.gz: 523a4308dbda6e8e37d24d62474f70ed783b2d10ef65caabbb0047b0e21d3283d53d6b9678dff21ea46879968fd31d938aa74a272276c764b4dc6d2eb686421a
data/README.md CHANGED
@@ -11,14 +11,15 @@ Time calculations using business hours.
11
11
  ## Features
12
12
 
13
13
  * Support for:
14
- - Intervals spanning the entire day.
15
- - Interday intervals and holidays.
16
14
  - Multiple intervals per day.
17
15
  - Multiple schedule configurations.
18
- * Second-level precision on all calculations.
19
- * Accurate handling of Daylight Saving Time.
16
+ - Intervals spanning the entire day.
17
+ - Holidays.
18
+ - Breaks (time-segment holidays).
19
+ * Second-level calculation precision.
20
+ * Seamless Daylight Saving Time handling.
20
21
  * Schedule intersection.
21
- * Thread-safe.
22
+ * Thread safety.
22
23
 
23
24
  ## Anti-Features
24
25
 
@@ -51,12 +52,20 @@ Biz.configure do |config|
51
52
  sat: {'10:00' => '14:00'}
52
53
  }
53
54
 
55
+ config.breaks = {
56
+ Date.new(2006, 1, 2) => {'10:00' => '11:30'},
57
+ Date.new(2006, 1, 3) => {'14:15' => '14:30', '15:40' => '15:50'}
58
+ }
59
+
54
60
  config.holidays = [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]
55
61
 
56
62
  config.time_zone = 'America/Los_Angeles'
57
63
  end
58
64
  ```
59
65
 
66
+ Periods occurring on holidays are disregarded. Similarly, any segment of a
67
+ period that overlaps with a break is treated as inactive.
68
+
60
69
  If global configuration isn't your thing, configure an instance instead:
61
70
 
62
71
  ```ruby
@@ -86,11 +95,19 @@ Biz.within(Time.utc(2015, 3, 7), Time.utc(2015, 3, 14)).in_seconds
86
95
  # Determine if a time is in business hours
87
96
  Biz.in_hours?(Time.utc(2015, 1, 10, 9))
88
97
 
89
- # Determine if a time is on a holiday
98
+ # Determine if a time is during a break
99
+ Biz.on_break?(Time.utc(2016, 6, 3))
100
+
101
+ # Determine if a time is during a holiday
90
102
  Biz.on_holiday?(Time.utc(2014, 1, 1))
91
103
  ```
92
104
 
93
- Note that all returned times are in UTC.
105
+ All returned times are in UTC.
106
+
107
+ If a schedule will be configured with a large number of holidays and performance
108
+ is a particular concern, it's recommended that holidays are filtered down to
109
+ those relevant to the calculation(s) at hand before configuration to improve
110
+ performance.
94
111
 
95
112
  By dropping down a level, you can get access to the underlying time segments,
96
113
  which you can use to do your own custom calculations or just get a better idea
@@ -136,6 +153,11 @@ schedule_1 = Biz::Schedule.new do |config|
136
153
  sat: {'11:00' => '14:30'}
137
154
  }
138
155
 
156
+ config.breaks = {
157
+ Date.new(2016, 6, 2) => {'09:00' => '10:30', '16:00' => '16:30'},
158
+ Date.new(2016, 6, 3) => {'12:15' => '12:45'}
159
+ }
160
+
139
161
  config.holidays = [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]
140
162
 
141
163
  config.time_zone = 'Etc/UTC'
@@ -150,6 +172,11 @@ schedule_2 = Biz::Schedule.new do |config|
150
172
  thu: {'11:00' => '12:00', '13:00' => '14:00'}
151
173
  }
152
174
 
175
+ config.breaks = {
176
+ Date.new(2016, 6, 3) => {'13:30' => '14:00'},
177
+ Date.new(2016, 6, 4) => {'11:00' => '12:00'}
178
+ }
179
+
153
180
  config.holidays = [
154
181
  Date.new(2016, 1, 1),
155
182
  Date.new(2016, 7, 4),
@@ -163,8 +190,8 @@ schedule_1 & schedule_2
163
190
  ```
164
191
 
165
192
  The resulting schedule will be a combination of the two schedules: an
166
- intersection of the intervals, a union of the holidays, and the time zone of the
167
- first schedule.
193
+ intersection of the intervals, a union of the breaks and holidays, and the time
194
+ zone of the first schedule.
168
195
 
169
196
  For the above example, the resulting schedule would be equivalent to one with
170
197
  the following configuration:
@@ -178,6 +205,12 @@ Biz::Schedule.new do |config|
178
205
  thu: {'11:00' => '12:00', '13:00' => '14:00'}
179
206
  }
180
207
 
208
+ config.breaks = {
209
+ Date.new(2016, 6, 2) => {'09:00' => '10:30', '16:00' => '16:30'},
210
+ Date.new(2016, 6, 3) => {'12:15' => '12:45', '13:30' => '14:00'},
211
+ Date.new(2016, 6, 4) => {'11:00' => '12:00'}
212
+ }
213
+
181
214
  config.holidays = [
182
215
  Date.new(2016, 1, 1),
183
216
  Date.new(2016, 7, 4),
@@ -207,6 +240,8 @@ require 'biz/core_ext'
207
240
 
208
241
  Time.utc(2015, 8, 20, 9, 30).business_hours?
209
242
 
243
+ Time.utc(2016, 6, 3, 12).on_break?
244
+
210
245
  Time.utc(2014, 1, 1, 12).on_holiday?
211
246
 
212
247
  Date.new(2015, 12, 10).business_day?
data/lib/biz.rb CHANGED
@@ -25,6 +25,7 @@ module Biz
25
25
  within
26
26
  in_hours?
27
27
  business_hours?
28
+ on_break?
28
29
  on_holiday?
29
30
  ] => :schedule
30
31
 
@@ -6,4 +6,5 @@ end
6
6
  require 'biz/calculation/active'
7
7
  require 'biz/calculation/duration_within'
8
8
  require 'biz/calculation/for_duration'
9
+ require 'biz/calculation/on_break'
9
10
  require 'biz/calculation/on_holiday'
@@ -8,7 +8,8 @@ module Biz
8
8
  end
9
9
 
10
10
  def result
11
- schedule.intervals.any? { |interval| interval.contains?(time) } &&
11
+ schedule.intervals.any? { |interval| interval.contains?(time) } &&
12
+ schedule.breaks.none? { |break_| break_.contains?(time) } &&
12
13
  schedule.holidays.none? { |holiday| holiday.contains?(time) }
13
14
  end
14
15
 
@@ -0,0 +1,21 @@
1
+ module Biz
2
+ module Calculation
3
+ class OnBreak
4
+
5
+ def initialize(schedule, time)
6
+ @schedule = schedule
7
+ @time = time
8
+ end
9
+
10
+ def result
11
+ schedule.breaks.any? { |break_| break_.contains?(time) }
12
+ end
13
+
14
+ protected
15
+
16
+ attr_reader :schedule,
17
+ :time
18
+
19
+ end
20
+ end
21
+ end
@@ -21,6 +21,16 @@ module Biz
21
21
  end
22
22
  end
23
23
 
24
+ def breaks
25
+ @breaks ||= begin
26
+ raw
27
+ .breaks
28
+ .flat_map { |date, hours| break_periods(date, hours) }
29
+ .sort
30
+ .freeze
31
+ end
32
+ end
33
+
24
34
  def holidays
25
35
  @holidays ||= begin
26
36
  raw
@@ -41,12 +51,34 @@ module Biz
41
51
  @weekdays ||= raw.hours.keys.uniq.freeze
42
52
  end
43
53
 
54
+ def &(other)
55
+ self.class.new do |config|
56
+ config.hours = Interval.to_hours(intersected_intervals(other))
57
+ config.breaks = combined_breaks(other)
58
+ config.holidays = [*raw.holidays, *other.raw.holidays].map(&:to_date)
59
+ config.time_zone = raw.time_zone
60
+ end
61
+ end
62
+
44
63
  protected
45
64
 
46
65
  attr_reader :raw
47
66
 
48
67
  private
49
68
 
69
+ def to_proc
70
+ proc do |config|
71
+ config.hours = raw.hours
72
+ config.breaks = raw.breaks
73
+ config.holidays = raw.holidays
74
+ config.time_zone = raw.time_zone
75
+ end
76
+ end
77
+
78
+ def time
79
+ @time ||= Time.new(time_zone)
80
+ end
81
+
50
82
  def weekday_intervals(weekday, hours)
51
83
  hours.map { |start_timestamp, end_timestamp|
52
84
  Interval.new(
@@ -63,7 +95,33 @@ module Biz
63
95
  }
64
96
  end
65
97
 
66
- Raw = Struct.new(:hours, :holidays, :time_zone) do
98
+ def break_periods(date, hours)
99
+ hours.map { |start_timestamp, end_timestamp|
100
+ TimeSegment.new(
101
+ time.on_date(date, DayTime.from_timestamp(start_timestamp)),
102
+ time.on_date(date, DayTime.from_timestamp(end_timestamp))
103
+ )
104
+ }
105
+ end
106
+
107
+ def intersected_intervals(other)
108
+ intervals.flat_map { |interval|
109
+ other
110
+ .intervals
111
+ .map { |other_interval| interval & other_interval }
112
+ .reject(&:empty?)
113
+ }
114
+ end
115
+
116
+ def combined_breaks(other)
117
+ Hash.new do |config, date| config.store(date, {}) end.tap do |combined|
118
+ [raw.breaks, other.raw.breaks].each do |configured|
119
+ configured.each do |date, breaks| combined[date].merge!(breaks) end
120
+ end
121
+ end
122
+ end
123
+
124
+ Raw = Struct.new(:hours, :breaks, :holidays, :time_zone) do
67
125
  module Default
68
126
  HOURS = {
69
127
  mon: {'09:00' => '17:00'},
@@ -73,6 +131,7 @@ module Biz
73
131
  fri: {'09:00' => '17:00'}
74
132
  }.freeze
75
133
 
134
+ BREAKS = [].freeze
76
135
  HOLIDAYS = [].freeze
77
136
  TIME_ZONE = 'Etc/UTC'.freeze
78
137
  end
@@ -81,6 +140,7 @@ module Biz
81
140
  super
82
141
 
83
142
  self.hours ||= Default::HOURS
143
+ self.breaks ||= Default::BREAKS
84
144
  self.holidays ||= Default::HOLIDAYS
85
145
  self.time_zone ||= Default::TIME_ZONE
86
146
  end
@@ -5,6 +5,10 @@ module Biz
5
5
  Biz.in_hours?(self)
6
6
  end
7
7
 
8
+ def on_break?
9
+ Biz.on_break?(self)
10
+ end
11
+
8
12
  def on_holiday?
9
13
  Biz.on_holiday?(self)
10
14
  end
@@ -1,6 +1,8 @@
1
1
  module Biz
2
2
  class Holiday
3
3
 
4
+ extend Forwardable
5
+
4
6
  include Comparable
5
7
 
6
8
  def initialize(date, time_zone)
@@ -8,15 +10,15 @@ module Biz
8
10
  @time_zone = time_zone
9
11
  end
10
12
 
11
- def contains?(time)
12
- date == Time.new(time_zone).local(time).to_date
13
- end
13
+ delegate contains?: :to_time_segment
14
14
 
15
15
  def to_time_segment
16
- TimeSegment.new(
17
- Time.new(time_zone).on_date(date, DayTime.midnight),
18
- Time.new(time_zone).on_date(date, DayTime.endnight)
19
- )
16
+ @time_segment ||= begin
17
+ TimeSegment.new(
18
+ Time.new(time_zone).on_date(date, DayTime.midnight),
19
+ Time.new(time_zone).on_date(date, DayTime.endnight)
20
+ )
21
+ end
20
22
  end
21
23
 
22
24
  def <=>(other)
@@ -11,8 +11,6 @@ module Biz
11
11
  super(periods) do |yielder, period| yielder << period end
12
12
  end
13
13
 
14
- delegate time_zone: :schedule
15
-
16
14
  def timeline
17
15
  Timeline::Proxy.new(self)
18
16
  end
@@ -28,21 +26,27 @@ module Biz
28
26
  weeks
29
27
  .lazy
30
28
  .flat_map { |week| business_periods(week) }
31
- .select { |business_period| relevant?(business_period) }
32
- .map { |business_period| business_period & boundary }
33
- .reject { |business_period|
34
- schedule.holidays.any? { |holiday|
35
- holiday.contains?(business_period.start_time)
36
- }
37
- }
29
+ .select { |period| relevant?(period) }
30
+ .map { |period| period & boundary }
31
+ .flat_map { |period| active_periods(period) }
32
+ .reject { |period| on_holiday?(period) }
33
+ .reject(&:empty?)
38
34
  end
39
35
 
40
36
  def business_periods(week)
41
37
  intervals.lazy.map { |interval| interval.to_time_segment(week) }
42
38
  end
43
39
 
44
- def intervals
45
- schedule.intervals
40
+ def active_periods(period)
41
+ schedule.breaks.reduce([period]) { |periods, break_period|
42
+ periods.flat_map { |active_period| active_period / break_period }
43
+ }
44
+ end
45
+
46
+ def on_holiday?(period)
47
+ schedule.holidays.any? { |holiday|
48
+ holiday.contains?(period.start_time)
49
+ }
46
50
  end
47
51
 
48
52
  end
@@ -10,7 +10,7 @@ module Biz
10
10
 
11
11
  def weeks
12
12
  Range.new(
13
- Week.since_epoch(Time.new(time_zone).local(origin)),
13
+ Week.since_epoch(schedule.in_zone.local(origin)),
14
14
  Week.since_epoch(Time.heat_death)
15
15
  )
16
16
  end
@@ -20,7 +20,11 @@ module Biz
20
20
  end
21
21
 
22
22
  def boundary
23
- TimeSegment.after(origin)
23
+ @boundary ||= TimeSegment.after(origin)
24
+ end
25
+
26
+ def intervals
27
+ @intervals ||= schedule.intervals
24
28
  end
25
29
 
26
30
  end
@@ -10,7 +10,7 @@ module Biz
10
10
 
11
11
  def weeks
12
12
  Week
13
- .since_epoch(Time.new(time_zone).local(origin))
13
+ .since_epoch(schedule.in_zone.local(origin))
14
14
  .downto(Week.since_epoch(Time.big_bang))
15
15
  end
16
16
 
@@ -18,12 +18,16 @@ module Biz
18
18
  origin > period.start_time
19
19
  end
20
20
 
21
+ def active_periods(*)
22
+ super.reverse
23
+ end
24
+
21
25
  def boundary
22
- TimeSegment.before(origin)
26
+ @boundary ||= TimeSegment.before(origin)
23
27
  end
24
28
 
25
29
  def intervals
26
- super.reverse
30
+ @intervals ||= schedule.intervals.reverse
27
31
  end
28
32
 
29
33
  end
@@ -9,6 +9,7 @@ module Biz
9
9
 
10
10
  delegate %i[
11
11
  intervals
12
+ breaks
12
13
  holidays
13
14
  time_zone
14
15
  weekdays
@@ -36,38 +37,27 @@ module Biz
36
37
  Calculation::Active.new(self, time).result
37
38
  end
38
39
 
40
+ alias business_hours? in_hours?
41
+
42
+ def on_break?(time)
43
+ Calculation::OnBreak.new(self, time).result
44
+ end
45
+
39
46
  def on_holiday?(time)
40
47
  Calculation::OnHoliday.new(self, time).result
41
48
  end
42
49
 
43
- alias business_hours? in_hours?
44
-
45
50
  def in_zone
46
51
  Time.new(time_zone)
47
52
  end
48
53
 
49
54
  def &(other)
50
- self.class.new do |config|
51
- config.hours = Interval.to_hours(intersected_intervals(other))
52
- config.holidays = [*holidays, *other.holidays].map(&:to_date)
53
- config.time_zone = time_zone.name
54
- end
55
+ self.class.new(&(configuration & other.configuration))
55
56
  end
56
57
 
57
58
  protected
58
59
 
59
60
  attr_reader :configuration
60
61
 
61
- private
62
-
63
- def intersected_intervals(other)
64
- intervals.flat_map { |interval|
65
- other
66
- .intervals
67
- .map { |other_interval| interval & other_interval }
68
- .reject(&:empty?)
69
- }
70
- end
71
-
72
62
  end
73
63
  end
@@ -32,7 +32,7 @@ module Biz
32
32
  end
33
33
 
34
34
  def contains?(time)
35
- (start_time..end_time).cover?(time)
35
+ (start_time...end_time).cover?(time)
36
36
  end
37
37
 
38
38
  def &(other)
@@ -42,6 +42,13 @@ module Biz
42
42
  self.class.new(lower_bound, [lower_bound, upper_bound].max)
43
43
  end
44
44
 
45
+ def /(other)
46
+ [
47
+ self.class.new(start_time, other.start_time),
48
+ self.class.new(other.end_time, end_time)
49
+ ].reject(&:empty?).map { |potential| self & potential }
50
+ end
51
+
45
52
  def <=>(other)
46
53
  return unless other.is_a?(self.class)
47
54
 
@@ -1,3 +1,3 @@
1
1
  module Biz
2
- VERSION = '1.5.2'.freeze
2
+ VERSION = '1.6.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: biz
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Craig Little
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-04-12 00:00:00.000000000 Z
12
+ date: 2016-06-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: clavius
@@ -81,6 +81,7 @@ files:
81
81
  - lib/biz/calculation/active.rb
82
82
  - lib/biz/calculation/duration_within.rb
83
83
  - lib/biz/calculation/for_duration.rb
84
+ - lib/biz/calculation/on_break.rb
84
85
  - lib/biz/calculation/on_holiday.rb
85
86
  - lib/biz/configuration.rb
86
87
  - lib/biz/core_ext.rb