class IceCube::Schedule
Attributes
Get the end time
Get the start time
Public Class Methods
# File lib/ice_cube/schedule.rb, line 387 def self.dump(schedule) return schedule if schedule.nil? || schedule == "" schedule.to_yaml end
Load the schedule from a hash
# File lib/ice_cube/schedule.rb, line 374 def self.from_hash(original_hash, options = {}) HashParser.new(original_hash).to_schedule do |schedule| Deprecated.schedule_options(schedule, options) yield schedule if block_given? end end
Load the schedule from ical
# File lib/ice_cube/schedule.rb, line 337 def self.from_ical(ical, options = {}) IcalParser.schedule_from_ical(ical, options) end
Load the schedule from yaml
# File lib/ice_cube/schedule.rb, line 347 def self.from_yaml(yaml, options = {}) YamlParser.new(yaml).to_schedule do |schedule| Deprecated.schedule_options(schedule, options) yield schedule if block_given? end end
# File lib/ice_cube/schedule.rb, line 392 def self.load(yaml) return yaml if yaml.nil? || yaml == "" from_yaml(yaml) end
Create a new schedule
# File lib/ice_cube/schedule.rb, line 18 def initialize(start_time = nil, options = {}) self.start_time = start_time || TimeUtil.now self.end_time = self.start_time + options[:duration] if options[:duration] self.end_time = options[:end_time] if options[:end_time] @all_recurrence_rules = [] @all_exception_rules = [] yield self if block_given? end
Public Instance Methods
Add an exception rule to the schedule
# File lib/ice_cube/schedule.rb, line 82 def add_exception_rule(rule) @all_exception_rules << rule unless @all_exception_rules.include?(rule) end
Add an exception time to the schedule
# File lib/ice_cube/schedule.rb, line 59 def add_exception_time(time) return nil if time.nil? rule = SingleOccurrenceRule.new(time) add_exception_rule rule time end
Add a recurrence rule to the schedule
# File lib/ice_cube/schedule.rb, line 70 def add_recurrence_rule(rule) @all_recurrence_rules << rule unless @all_recurrence_rules.include?(rule) end
Add a recurrence time to the schedule
# File lib/ice_cube/schedule.rb, line 48 def add_recurrence_time(time) return nil if time.nil? rule = SingleOccurrenceRule.new(time) add_recurrence_rule rule time end
All of the occurrences
# File lib/ice_cube/schedule.rb, line 152 def all_occurrences require_terminating_rules enumerate_occurrences(start_time).to_a end
Emit an enumerator based on the start time
# File lib/ice_cube/schedule.rb, line 158 def all_occurrences_enumerator enumerate_occurrences(start_time) 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
# File lib/ice_cube/schedule.rb, line 255 def conflicts_with?(other_schedule, closing_time = nil) closing_time = TimeUtil.ensure_time(closing_time) unless terminating? || other_schedule.terminating? || closing_time raise ArgumentError, "One or both schedules 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 last_time += terminating_schedule.duration 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
# File lib/ice_cube/schedule.rb, line 39 def duration end_time ? end_time - start_time : 0 end
# File lib/ice_cube/schedule.rb, line 43 def duration=(seconds) @end_time = start_time + seconds end
Iterate forever
# File lib/ice_cube/schedule.rb, line 163 def each_occurrence(&block) enumerate_occurrences(start_time, &block).to_a self end
Set #end_time
# File lib/ice_cube/schedule.rb, line 34 def end_time=(end_time) @end_time = TimeUtil.ensure_time end_time end
Get the exception rules
# File lib/ice_cube/schedule.rb, line 100 def exception_rules @all_exception_rules.reject { |r| r.is_a?(SingleOccurrenceRule) } end
Get the exception times that are on the schedule
# File lib/ice_cube/schedule.rb, line 126 def exception_times @all_exception_rules.select { |r| r.is_a?(SingleOccurrenceRule) }.map(&:time) end
Get the first n occurrences, or the first occurrence if n is skipped
# File lib/ice_cube/schedule.rb, line 297 def first(n = nil) occurrences = enumerate_occurrences(start_time).take(n || 1) n.nil? ? occurrences.first : occurrences end
Get the final n occurrences of a terminating schedule or the final one if no n is given
# File lib/ice_cube/schedule.rb, line 304 def last(n = nil) require_terminating_rules occurrences = enumerate_occurrences(start_time).to_a n.nil? ? occurrences.last : occurrences[-n..-1] end
The next occurrence after now (overridable)
# File lib/ice_cube/schedule.rb, line 175 def next_occurrence(from = nil, options = {}) from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time) enumerate_occurrences(from + 1, nil, options).next rescue StopIteration nil end
The next n occurrences after now
# File lib/ice_cube/schedule.rb, line 169 def next_occurrences(num, from = nil, options = {}) from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time) enumerate_occurrences(from + 1, nil, options).take(num) end
Get all of the occurrences from the #start_time up until a given Time
# File lib/ice_cube/schedule.rb, line 147 def occurrences(closing_time) enumerate_occurrences(start_time, closing_time).to_a end
Occurrences between two times
# File lib/ice_cube/schedule.rb, line 211 def occurrences_between(begin_time, closing_time, options = {}) enumerate_occurrences(begin_time, closing_time, options).to_a end
Determine if the schedule is occurring at a given time
# File lib/ice_cube/schedule.rb, line 241 def occurring_at?(time) time = TimeUtil.match_zone(time, start_time) or raise ArgumentError, "Time required, got #{time.inspect}" if duration > 0 return false if exception_time?(time) occurs_between?(time - duration + 1, time) else occurs_at?(time) end end
Return a boolean indicating if an occurrence is occurring between two times, inclusive of its duration. This counts zero-length occurrences that intersect the start of the range and within the range, but not occurrences at the end of the range since none of their duration intersects the range.
# File lib/ice_cube/schedule.rb, line 228 def occurring_between?(opening_time, closing_time) occurs_between?(opening_time, closing_time, :spans => true) end
Determine if the schedule occurs at a specific time
# File lib/ice_cube/schedule.rb, line 292 def occurs_at?(time) occurs_between?(time, time) end
Return a boolean indicating if an occurrence falls between two times
# File lib/ice_cube/schedule.rb, line 216 def occurs_between?(begin_time, closing_time, options = {}) enumerate_occurrences(begin_time, closing_time, options).next true rescue StopIteration false end
Return a boolean indicating if an occurrence falls on a certain date
# File lib/ice_cube/schedule.rb, line 233 def occurs_on?(date) date = TimeUtil.ensure_date(date) begin_time = TimeUtil.beginning_of_date(date, start_time) closing_time = TimeUtil.end_of_date(date, start_time) occurs_between?(begin_time, closing_time) end
The previous occurrence from a given time
# File lib/ice_cube/schedule.rb, line 183 def previous_occurrence(from) from = TimeUtil.match_zone(from, start_time) or raise ArgumentError, "Time required, got #{time.inspect}" return nil if from <= start_time enumerate_occurrences(start_time, from - 1).to_a.last end
The previous n occurrences before a given time
# File lib/ice_cube/schedule.rb, line 190 def previous_occurrences(num, from) from = TimeUtil.match_zone(from, start_time) or raise ArgumentError, "Time required, got #{time.inspect}" return [] if from <= start_time a = enumerate_occurrences(start_time, from - 1).to_a a.size > num ? a[-1*num,a.size] : a end
Get the recurrence rules
# File lib/ice_cube/schedule.rb, line 94 def recurrence_rules @all_recurrence_rules.reject { |r| r.is_a?(SingleOccurrenceRule) } end
Get the recurrence times that are on the schedule
# File lib/ice_cube/schedule.rb, line 106 def recurrence_times @all_recurrence_rules.select { |r| r.is_a?(SingleOccurrenceRule) }.map(&:time) end
The remaining occurrences (same requirements as #all_occurrences)
# File lib/ice_cube/schedule.rb, line 198 def remaining_occurrences(from = nil, options = {}) require_terminating_rules from ||= TimeUtil.now(@start_time) enumerate_occurrences(from, nil, options).to_a end
Returns an enumerator for all remaining occurrences
# File lib/ice_cube/schedule.rb, line 205 def remaining_occurrences_enumerator(from = nil, options = {}) from ||= TimeUtil.now(@start_time) enumerate_occurrences(from, nil, options) end
Remove an exception rule
# File lib/ice_cube/schedule.rb, line 88 def remove_exception_rule(rule) res = @all_exception_rules.delete(rule) res.nil? ? [] : [res] end
Remove an exception time
# File lib/ice_cube/schedule.rb, line 134 def remove_exception_time(time) found = false @all_exception_rules.delete_if do |rule| found = true if rule.is_a?(SingleOccurrenceRule) && rule.time == time end time if found end
Remove a recurrence rule
# File lib/ice_cube/schedule.rb, line 76 def remove_recurrence_rule(rule) res = @all_recurrence_rules.delete(rule) res.nil? ? [] : [res] end
Remove a recurrence time
# File lib/ice_cube/schedule.rb, line 114 def remove_recurrence_time(time) found = false @all_recurrence_rules.delete_if do |rule| found = true if rule.is_a?(SingleOccurrenceRule) && rule.time == time end time if found end
Set #start_time
# File lib/ice_cube/schedule.rb, line 28 def start_time=(start_time) @start_time = TimeUtil.ensure_time start_time end
Determine if the schedule will end @return [Boolean] true if ending, false if repeating forever
# File lib/ice_cube/schedule.rb, line 383 def terminating? recurrence_rules.empty? || recurrence_rules.all?(&:terminating?) end
Convert the schedule to a hash
# File lib/ice_cube/schedule.rb, line 355 def to_hash data = {} data[:start_time] = TimeUtil.serialize_time(start_time) data[:start_date] = data[:start_time] if IceCube.compatibility <= 11 data[:end_time] = TimeUtil.serialize_time(end_time) if end_time data[:rrules] = recurrence_rules.map(&:to_hash) if IceCube.compatibility <= 11 && exception_rules.any? data[:exrules] = exception_rules.map(&:to_hash) end data[:rtimes] = recurrence_times.map do |rt| TimeUtil.serialize_time(rt) end data[:extimes] = exception_times.map do |et| TimeUtil.serialize_time(et) end data end
Serialize this schedule #to_ical
# File lib/ice_cube/schedule.rb, line 325 def to_ical(force_utc = false) pieces = [] pieces << "DTSTART#{IcalBuilder.ical_format(start_time, force_utc)}" pieces.concat recurrence_rules.map { |r| "RRULE:#{r.to_ical}" } pieces.concat exception_rules.map { |r| "EXRULE:#{r.to_ical}" } pieces.concat recurrence_times_without_start_time.map { |t| "RDATE#{IcalBuilder.ical_format(t, force_utc)}" } pieces.concat exception_times.map { |t| "EXDATE#{IcalBuilder.ical_format(t, force_utc)}" } pieces << "DTEND#{IcalBuilder.ical_format(end_time, force_utc)}" if end_time pieces.join("\n") end
String serialization
# File lib/ice_cube/schedule.rb, line 311 def to_s pieces = [] rd = recurrence_times_with_start_time - extimes pieces.concat rd.sort.map { |t| IceCube::I18n.l(t, format: IceCube.to_s_time_format) } pieces.concat rrules.map { |t| t.to_s } pieces.concat exrules.map { |t| IceCube::I18n.t('ice_cube.not', target: t.to_s) } pieces.concat extimes.sort.map { |t| target = IceCube::I18n.l(t, format: IceCube.to_s_time_format) IceCube::I18n.t('ice_cube.not_on', target: target) } pieces.join(IceCube::I18n.t('ice_cube.pieces_connector')) end
Convert the schedule to yaml
# File lib/ice_cube/schedule.rb, line 342 def to_yaml(*args) YAML::dump(to_hash, *args) end
Private Instance Methods
Find all of the occurrences for the schedule between opening_time and closing_time Iteration is unrolled in pairs to skip duplicate times in end of DST
# File lib/ice_cube/schedule.rb, line 408 def enumerate_occurrences(opening_time, closing_time = nil, options = {}, &block) opening_time = TimeUtil.match_zone(opening_time, start_time) closing_time = TimeUtil.match_zone(closing_time, start_time) opening_time += start_time.subsec - opening_time.subsec rescue 0 opening_time = start_time if opening_time < start_time spans = options[:spans] == true && duration != 0 Enumerator.new do |yielder| reset t1 = full_required? ? start_time : realign((spans ? opening_time - duration : opening_time)) loop do break unless (t0 = next_time(t1, closing_time)) break if closing_time && t0 > closing_time if (spans ? (t0.end_time > opening_time) : (t0 >= opening_time)) yielder << (block_given? ? block.call(t0) : t0) end break unless (t1 = next_time(t0 + 1, closing_time)) break if closing_time && t1 > closing_time if TimeUtil.same_clock?(t0, t1) && recurrence_rules.any?(&:dst_adjust?) wind_back_dst next (t1 += 1) end if (spans ? (t1.end_time > opening_time) : (t1 >= opening_time)) yielder << (block_given? ? block.call(t1) : t1) end next (t1 += 1) end end end
Return a boolean indicating whether or not a specific time is excluded from the schedule
# File lib/ice_cube/schedule.rb, line 463 def exception_time?(time) @all_exception_rules.any? do |rule| rule.on?(time, self) end end
Indicate if any rule needs to be run from the start of time If we have rules with counts, we need to walk from the beginning of time
# File lib/ice_cube/schedule.rb, line 456 def full_required? @all_recurrence_rules.any?(&:full_required?) || @all_exception_rules.any?(&:full_required?) end
# File lib/ice_cube/schedule.rb, line 475 def implicit_start_occurrence_rule SingleOccurrenceRule.new(start_time) end
Get the next time after (or including) a specific time
# File lib/ice_cube/schedule.rb, line 438 def next_time(time, closing_time) loop do min_time = recurrence_rules_with_implicit_start_occurrence.reduce(nil) do |min_time, rule| begin new_time = rule.next_time(time, self, min_time || closing_time) [min_time, new_time].compact.min rescue StopIteration min_time end end break nil unless min_time next (time = min_time + 1) if exception_time?(min_time) break Occurrence.new(min_time, min_time + duration) end end
If any rule has validations for values within the period, (overriding the interval from start time, e.g. `day`), and the opening time is offset from the interval multiplier such that it might miss the first correct occurrence (e.g. repeat is every N weeks, but selecting from end of week N-1, the first jump would go to end of week N and miss any earlier validations in the week). This realigns the opening time to the start of the interval's correct period (e.g. move to start of week N) TODO: check if this is needed for validations other than `:wday`
# File lib/ice_cube/schedule.rb, line 514 def realign(opening_time) time = TimeUtil::TimeWrapper.new(opening_time) recurrence_rules.each do |rule| wday_validations = rule.other_interval_validations.select { |v| v.type == :wday } or next interval = rule.base_interval_validation.validate(opening_time, self).to_i offset = wday_validations .map { |v| v.validate(opening_time, self).to_i } .reduce(0) { |least, i| i > 0 && i <= interval && (i < least || least == 0) ? i : least } time.add(rule.base_interval_type, 7 - time.to_time.wday) if offset > 0 end time.to_time end
# File lib/ice_cube/schedule.rb, line 491 def recurrence_rules_with_implicit_start_occurrence if recurrence_rules.empty? [implicit_start_occurrence_rule].concat @all_recurrence_rules else @all_recurrence_rules end end
# File lib/ice_cube/schedule.rb, line 483 def recurrence_times_with_start_time if recurrence_rules.empty? [start_time].concat recurrence_times_without_start_time else recurrence_times end end
# File lib/ice_cube/schedule.rb, line 479 def recurrence_times_without_start_time recurrence_times.reject { |t| t == start_time } end
# File lib/ice_cube/schedule.rb, line 469 def require_terminating_rules return true if terminating? method_name = caller[0].split(' ').last raise ArgumentError, "All recurrence rules must specify .until or .count to use #{method_name}" end
Reset all rules for another run
# File lib/ice_cube/schedule.rb, line 400 def reset @all_recurrence_rules.each(&:reset) @all_exception_rules.each(&:reset) end
# File lib/ice_cube/schedule.rb, line 499 def wind_back_dst recurrence_rules.each do |rule| rule.skipped_for_dst end end