Skip to content

Commit

Permalink
Merge pull request #71 from zendesk/craig/break-intersection
Browse files Browse the repository at this point in the history
Include breaks when intersecting schedules
  • Loading branch information
craiglittle authored Jun 13, 2016
2 parents bde74a0 + c471b79 commit bc6efff
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 41 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Metrics/AbcSize:
Max: 20

Metrics/ClassLength:
Max: 120

Metrics/MethodLength:
Max: 20

Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ schedule_1 = Biz::Schedule.new do |config|
sat: {'11:00' => '14:30'}
}

config.breaks = {
Date.new(2016, 6, 2) => {'09:00' => '10:30', '16:00' => '16:30'},
Date.new(2016, 6, 3) => {'12:15' => '12:45'}
}

config.holidays = [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]

config.time_zone = 'Etc/UTC'
Expand All @@ -164,6 +169,11 @@ schedule_2 = Biz::Schedule.new do |config|
thu: {'11:00' => '12:00', '13:00' => '14:00'}
}

config.breaks = {
Date.new(2016, 6, 3) => {'13:30' => '14:00'},
Date.new(2016, 6, 4) => {'11:00' => '12:00'}
}

config.holidays = [
Date.new(2016, 1, 1),
Date.new(2016, 7, 4),
Expand All @@ -177,8 +187,8 @@ schedule_1 & schedule_2
```

The resulting schedule will be a combination of the two schedules: an
intersection of the intervals, a union of the holidays, and the time zone of the
first schedule.
intersection of the intervals, a union of the breaks and holidays, and the time
zone of the first schedule.

For the above example, the resulting schedule would be equivalent to one with
the following configuration:
Expand All @@ -192,6 +202,12 @@ Biz::Schedule.new do |config|
thu: {'11:00' => '12:00', '13:00' => '14:00'}
}

config.breaks = {
Date.new(2016, 6, 2) => {'09:00' => '10:30', '16:00' => '16:30'},
Date.new(2016, 6, 3) => {'12:15' => '12:45', '13:30' => '14:00'},
Date.new(2016, 6, 4) => {'11:00' => '12:00'}
}

config.holidays = [
Date.new(2016, 1, 1),
Date.new(2016, 7, 4),
Expand Down
35 changes: 35 additions & 0 deletions lib/biz/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,30 @@ def weekdays
@weekdays ||= raw.hours.keys.uniq.freeze
end

def &(other)
self.class.new do |config|
config.hours = Interval.to_hours(intersected_intervals(other))
config.breaks = combined_breaks(other)
config.holidays = [*raw.holidays, *other.raw.holidays].map(&:to_date)
config.time_zone = raw.time_zone
end
end

protected

attr_reader :raw

private

def to_proc
proc do |config|
config.hours = raw.hours
config.breaks = raw.breaks
config.holidays = raw.holidays
config.time_zone = raw.time_zone
end
end

def time
@time ||= Time.new(time_zone)
end
Expand Down Expand Up @@ -86,6 +104,23 @@ def break_periods(date, hours)
}
end

def intersected_intervals(other)
intervals.flat_map { |interval|
other
.intervals
.map { |other_interval| interval & other_interval }
.reject(&:empty?)
}
end

def combined_breaks(other)
Hash.new do |config, date| config.store(date, {}) end.tap do |combined|
[raw.breaks, other.raw.breaks].each do |configured|
configured.each do |date, breaks| combined[date].merge!(breaks) end
end
end
end

Raw = Struct.new(:hours, :breaks, :holidays, :time_zone) do
module Default
HOURS = {
Expand Down
17 changes: 1 addition & 16 deletions lib/biz/schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,12 @@ def in_zone
end

def &(other)
self.class.new do |config|
config.hours = Interval.to_hours(intersected_intervals(other))
config.holidays = [*holidays, *other.holidays].map(&:to_date)
config.time_zone = time_zone.name
end
self.class.new(&(configuration & other.configuration))
end

protected

attr_reader :configuration

private

def intersected_intervals(other)
intervals.flat_map { |interval|
other
.intervals
.map { |other_interval| interval & other_interval }
.reject(&:empty?)
}
end

end
end
87 changes: 87 additions & 0 deletions spec/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@
end
end

context 'when converted to a proc for configuration' do
let(:proc_configuration) { described_class.new(&configuration) }

it 'configures the intervals' do
expect(proc_configuration.intervals).to eq configuration.intervals
end

it 'configures the holidays' do
expect(proc_configuration.holidays).to eq configuration.holidays
end

it 'configures the time zone' do
expect(proc_configuration.time_zone).to eq configuration.time_zone
end
end

describe '#intervals' do
context 'when unconfigured' do
subject(:configuration) {
Expand Down Expand Up @@ -239,4 +255,75 @@ def to_a
expect(configuration.weekdays).to eq %i[mon tue wed thu fri sat]
end
end

describe '#&' do
let(:other) {
described_class.new do |config|
config.hours = {
sun: {'10:00' => '12:00'},
mon: {'08:00' => '10:00'},
tue: {'11:00' => '15:00'},
wed: {'16:00' => '18:00'},
thu: {'11:00' => '12:00', '13:00' => '14:00'}
}

config.breaks = {Date.new(2006, 1, 3) => {'11:15' => '11:45'}}

config.holidays = [
Date.new(2006, 1, 1),
Date.new(2006, 7, 4),
Date.new(2006, 11, 24)
]

config.time_zone = 'Etc/UTC'
end
}

it 'returns a new configuration' do
expect(configuration & other).to be_a Biz::Configuration
end

it 'intersects the intervals' do
expect(Biz::Interval.to_hours((configuration & other).intervals)).to eq(
mon: {'09:00' => '10:00'},
tue: {'11:00' => '15:00'},
wed: {'16:00' => '17:00'},
thu: {'11:00' => '12:00', '13:00' => '14:00'}
)
end

it 'unions the breaks' do
expect((configuration & other).breaks).to eq [
Biz::TimeSegment.new(
in_zone('America/New_York') { Time.new(2006, 1, 2, 10) },
in_zone('America/New_York') { Time.new(2006, 1, 2, 11, 30) }
),
Biz::TimeSegment.new(
in_zone('America/New_York') { Time.new(2006, 1, 3, 11, 15) },
in_zone('America/New_York') { Time.new(2006, 1, 3, 11, 45) }
),
Biz::TimeSegment.new(
in_zone('America/New_York') { Time.new(2006, 1, 3, 14, 15) },
in_zone('America/New_York') { Time.new(2006, 1, 3, 14, 30) }
),
Biz::TimeSegment.new(
in_zone('America/New_York') { Time.new(2006, 1, 3, 15, 40) },
in_zone('America/New_York') { Time.new(2006, 1, 3, 15, 50) }
)
]
end

it 'unions the holidays' do
expect((configuration & other).holidays.map(&:to_date)).to eq [
Date.new(2006, 1, 1),
Date.new(2006, 7, 4),
Date.new(2006, 11, 24),
Date.new(2006, 12, 25)
]
end

it 'uses the original time zone' do
expect((configuration & other).time_zone).to eq configuration.time_zone
end
end
end
41 changes: 18 additions & 23 deletions spec/schedule_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@
thu: {'11:00' => '12:00', '13:00' => '14:00'}
}

config.breaks = {Date.new(2006, 1, 3) => {'11:15' => '11:45'}}

config.holidays = [
Date.new(2006, 1, 1),
Date.new(2006, 7, 4),
Expand All @@ -179,30 +181,23 @@
end
}

it 'returns a new schedule' do
expect(schedule & other).to be_a Biz::Schedule
end

it 'configures the schedule with the intersection of intervals' do
expect(Biz::Interval.to_hours((schedule & other).intervals)).to eq(
mon: {'09:00' => '10:00'},
tue: {'11:00' => '15:00'},
wed: {'16:00' => '17:00'},
thu: {'11:00' => '12:00', '13:00' => '14:00'}
)
end

it 'configures the schedule with the union of holidays' do
expect((schedule & other).holidays.map(&:to_date)).to eq [
Date.new(2006, 1, 1),
Date.new(2006, 7, 4),
Date.new(2006, 11, 24),
Date.new(2006, 12, 25)
it 'returns an intersected schedule' do
expect(
(schedule & other).periods.after(Time.utc(2006, 1, 1)).take(3).to_a
).to eq [
Biz::TimeSegment.new(
Time.utc(2006, 1, 2, 9),
Time.utc(2006, 1, 2, 10)
),
Biz::TimeSegment.new(
Time.utc(2006, 1, 3, 11),
Time.utc(2006, 1, 3, 11, 15)
),
Biz::TimeSegment.new(
Time.utc(2006, 1, 3, 11, 45),
Time.utc(2006, 1, 3, 14, 15)
)
]
end

it 'configures the schedule with the original time zone' do
expect((schedule & other).time_zone).to eq schedule.time_zone
end
end
end

0 comments on commit bc6efff

Please sign in to comment.