biz 1.5.2 → 1.6.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/README.md +44 -9
- data/lib/biz.rb +1 -0
- data/lib/biz/calculation.rb +1 -0
- data/lib/biz/calculation/active.rb +2 -1
- data/lib/biz/calculation/on_break.rb +21 -0
- data/lib/biz/configuration.rb +61 -1
- data/lib/biz/core_ext/time.rb +4 -0
- data/lib/biz/holiday.rb +9 -7
- data/lib/biz/periods/abstract.rb +15 -11
- data/lib/biz/periods/after.rb +6 -2
- data/lib/biz/periods/before.rb +7 -3
- data/lib/biz/schedule.rb +8 -18
- data/lib/biz/time_segment.rb +8 -1
- data/lib/biz/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e3d348e477f526c87601d6b59362fbb9e17b2dba
|
4
|
+
data.tar.gz: 83019f10ea87e74a41f317dd388dbe9d7ee204b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
19
|
-
|
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
|
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
|
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
|
-
|
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
|
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
data/lib/biz/calculation.rb
CHANGED
@@ -8,7 +8,8 @@ module Biz
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def result
|
11
|
-
schedule.intervals.any?
|
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
|
data/lib/biz/configuration.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/biz/core_ext/time.rb
CHANGED
data/lib/biz/holiday.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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)
|
data/lib/biz/periods/abstract.rb
CHANGED
@@ -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 { |
|
32
|
-
.map { |
|
33
|
-
.
|
34
|
-
|
35
|
-
|
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
|
45
|
-
schedule.
|
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
|
data/lib/biz/periods/after.rb
CHANGED
@@ -10,7 +10,7 @@ module Biz
|
|
10
10
|
|
11
11
|
def weeks
|
12
12
|
Range.new(
|
13
|
-
Week.since_epoch(
|
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
|
data/lib/biz/periods/before.rb
CHANGED
@@ -10,7 +10,7 @@ module Biz
|
|
10
10
|
|
11
11
|
def weeks
|
12
12
|
Week
|
13
|
-
.since_epoch(
|
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
|
-
|
30
|
+
@intervals ||= schedule.intervals.reverse
|
27
31
|
end
|
28
32
|
|
29
33
|
end
|
data/lib/biz/schedule.rb
CHANGED
@@ -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
|
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
|
data/lib/biz/time_segment.rb
CHANGED
@@ -32,7 +32,7 @@ module Biz
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def contains?(time)
|
35
|
-
(start_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
|
|
data/lib/biz/version.rb
CHANGED
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.
|
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-
|
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
|