From 88bb48ae7efde0e2e0304f33465c0fde792bb0d4 Mon Sep 17 00:00:00 2001 From: John Crepezzi Date: Sun, 1 Jan 2012 12:22:17 -0500 Subject: [PATCH] Added support for conflicts_with? Closes #41 --- lib/ice_cube/schedule.rb | 47 ++++++++++++++-- spec/examples/schedule_spec.rb | 97 ++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/lib/ice_cube/schedule.rb b/lib/ice_cube/schedule.rb index b519f07b..56f6e993 100644 --- a/lib/ice_cube/schedule.rb +++ b/lib/ice_cube/schedule.rb @@ -125,9 +125,7 @@ def occurrences(closing_time) # All of the occurrences def all_occurrences - unless end_time || recurrence_rules.all?(&:terminating?) - raise ArgumentError.new('Rule must specify either an until date or a count to use #all_occurrences') - end + raise ArgumentError.new('Rule must specify either an until date or a count to use #all_occurrences') unless terminating? find_occurrences(start_time) end @@ -180,6 +178,43 @@ def occurring_at?(time) end end + # Determine if this schedule conflicts with another schedule + # @param [IceCube::Schedule] other_schedule - The schedule to compare to + # @param [Time] closing_time - the last time to consider + # @return [Boolean] whether or not the schedules conflict at all + def conflicts_with?(other_schedule, closing_time = nil) + unless terminating? || other_schedule.terminating? || closing_time + raise ArgumentError.new 'At least one schedule must be terminating to use #conflicts_with?' + end + # Pick the terminating schedule, and other schedule + # No need to reverse if terminating? or there is a closing time + terminating_schedule = self + unless terminating? || closing_time + terminating_schedule, other_schedule = other_schedule, terminating_schedule + end + # Go through each occurrence of the terminating schedule and determine + # if the other occurs at that time + last_time = nil + terminating_schedule.each_occurrence do |time| + if closing_time && time > closing_time + last_time = closing_time + break + end + last_time = time + return true if other_schedule.occurring_at?(time) + end + # Due to durations, we need to walk up to the end time, and verify in the + # other direction + if last_time + other_schedule.each_occurrence do |time| + break if time > last_time + return true if terminating_schedule.occurring_at?(time) + end + end + # No conflict, return false + false + end + # Determine if the schedule occurs at a specific time def occurs_at?(time) occurs_between?(time, time) @@ -261,6 +296,12 @@ def self.from_hash(data, options = {}) schedule end + # Determine if the schedule will end + # @return [Boolean] true if ending, false if repeating forever + def terminating? + end_time || recurrence_rules.all?(&:terminating?) + end + private # Reset all rules for another run diff --git a/spec/examples/schedule_spec.rb b/spec/examples/schedule_spec.rb index 743427c7..2404f162 100644 --- a/spec/examples/schedule_spec.rb +++ b/spec/examples/schedule_spec.rb @@ -4,6 +4,103 @@ include IceCube + describe :conflicts_with? do + + it 'should raise an error if both are not terminating' do + schedules = 2.times.map do + schedule = IceCube::Schedule.new(Time.now) + schedule.rrule IceCube::Rule.daily + schedule + end + lambda do + schedules.first.conflicts_with?(schedules.last) + end.should raise_error ArgumentError + end + + it 'should not raise error if both are non-terminating closing time present' do + schedule1 = IceCube::Schedule.new Time.now + schedule1.rrule IceCube::Rule.weekly + schedule2 = IceCube::Schedule.new Time.now + schedule2.rrule IceCube::Rule.weekly + lambda do + schedule1.conflicts_with?(schedule2, Time.now + IceCube::ONE_DAY) + end.should_not raise_error + end + + it 'should not raise an error if one is non-terminating' do + schedule1 = IceCube::Schedule.new Time.now + schedule1.rrule IceCube::Rule.weekly + schedule2 = IceCube::Schedule.new Time.now + schedule2.rrule IceCube::Rule.weekly.until(Time.now) + lambda do + schedule1.conflicts_with?(schedule2) + end.should_not raise_error + end + + it 'should not raise an error if the other is non-terminating' do + schedule1 = IceCube::Schedule.new Time.now + schedule1.rrule IceCube::Rule.weekly.until(Time.now) + schedule2 = IceCube::Schedule.new Time.now + schedule2.rrule IceCube::Rule.weekly + lambda do + schedule1.conflicts_with?(schedule2) + end.should_not raise_error + end + + it 'should return true if conflict is present' do + start_time = Time.now + schedule1 = IceCube::Schedule.new(start_time) + schedule1.rrule IceCube::Rule.daily + schedule2 = IceCube::Schedule.new(start_time) + schedule2.rrule IceCube::Rule.daily + conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_DAY) + conflict.should be_true + end + + it 'should return false if conflict is not present' do + start_time = Time.now + schedule1 = IceCube::Schedule.new(start_time) + schedule1.rrule IceCube::Rule.weekly.day(:tuesday) + schedule2 = IceCube::Schedule.new(start_time) + schedule2.rrule IceCube::Rule.weekly.day(:monday) + conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_DAY) + conflict.should be_false + end + + it 'should return true if conflict is present based on duration' do + start_time = Time.now + schedule1 = IceCube::Schedule.new(start_time, :duration => IceCube::ONE_DAY + 1) + schedule1.rrule IceCube::Rule.weekly.day(:monday) + schedule2 = IceCube::Schedule.new(start_time) + schedule2.rrule IceCube::Rule.weekly.day(:tuesday) + conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_WEEK) + conflict.should be_true + end + + it 'should return true if conflict is present based on duration - other way' do + start_time = Time.now + schedule1 = IceCube::Schedule.new(start_time) + schedule1.rrule IceCube::Rule.weekly.day(:tuesday) + schedule2 = IceCube::Schedule.new(start_time, :duration => IceCube::ONE_DAY + 1) + schedule2.rrule IceCube::Rule.weekly.day(:monday) + conflict = schedule1.conflicts_with?(schedule2, start_time + IceCube::ONE_WEEK) + conflict.should be_true + end + + it 'should return false if conflict is past closing_time' do + start_time = Time.local(2011, 1, 1, 12) # Sunday + schedule1 = IceCube::Schedule.new(start_time) + schedule1.rrule IceCube::Rule.weekly.day(:friday) + schedule2 = IceCube::Schedule.new(start_time) + schedule2.rrule IceCube::Rule.weekly.day(:friday) + schedule2.conflicts_with?(schedule1, start_time + IceCube::ONE_WEEK). + should be_true + schedule2.conflicts_with?(schedule1, start_time + IceCube::ONE_DAY). + should be_false + end + + end + describe :each do it 'should be able to yield occurrences for a schedule' do