class Tilia::VObject::Recur::RRuleIterator
RRuleParser.
This class receives an RRULE string, and allows you to iterate to get a list of dates in that recurrence.
For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain 5 items, one for each day.
Public Class Methods
Creates the Iterator.
@param [String|array] rrule @param [Time] start
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 16 def initialize(rrule, start) @week_start = 'MO' @counter = 0 @interval = 1 @day_map = { 'SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6 } @day_names = { 0 => 'Sunday', 1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday' } @start_date = start parse_r_rule(rrule) @current_date = @start_date.clone end
Public Instance Methods
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 44 def current return nil unless valid @current_date.clone end
This method allows you to quickly go to the next occurrence after the specified date.
@param [Time] dt
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 109 def fast_forward(dt) self.next while valid && @current_date < dt end
Returns true if this recurring event never ends.
@return [Boolean]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 99 def infinite? !@count && !@until end
Returns the current item number.
@return [Fixnum]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 52 def key @counter end
Goes on to the next iteration.
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 77 def next # Otherwise, we find the next event in the normal RRULE # sequence. case @frequency when 'hourly' next_hourly when 'daily' next_daily when 'weekly' next_weekly when 'monthly' next_monthly when 'yearly' next_yearly end @counter += 1 end
Resets the iterator.
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 69 def rewind @current_date = @start_date.clone @counter = 0 end
Returns whether the current item is a valid item for the recurrence iterator. This will return false if we've gone beyond the UNTIL or COUNT statements.
@return [Boolean]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 61 def valid return @counter < @count if @count @until.nil? || @current_date <= @until end
Protected Instance Methods
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 563 def days recurrence_days = [] @by_day.each do |by_day| # The day may be preceeded with a positive (+n) or # negative (-n) integer. However, this does not make # sense in 'weekly' so we ignore it here. recurrence_days << @day_map[by_day[0...2]] end recurrence_days end
Simple mapping from iCalendar day names to day numbers.
@return [array] RUBY: attr_accessor :day_map
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 555 def hours recurrence_hours = [] @by_hour.each do |by_hour| recurrence_hours << by_hour.to_i end recurrence_hours end
Returns all the occurrences for a monthly frequency with a 'byDay' or 'byMonthDay' expansion for the current month.
The returned list is an array of integers with the day of month (1-31).
@return [array]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 451 def monthly_occurrences start_date = @current_date.clone by_day_results = [] # Our strategy is to simply go through the byDays, advance the date to # that point and add it to the results. if @by_day @by_day.each do |day| day_index = @day_map[day[-2..-1]] # Dayname will be something like 'wednesday'. Now we need to find # all wednesdays in this month. day_hits = [] check_date = start_date - (start_date.day - 1).days if check_date.wday != day_index if day_index < check_date.wday check_date += (7 - check_date.wday + day_index).days else check_date += (day_index - check_date.wday).days end end loop do day_hits << check_date.day check_date += 1.week break unless check_date.month == start_date.month end # So now we have 'all wednesdays' for month. It is however # possible that the user only really wanted the 1st, 2nd or last # wednesday. if day.length > 2 offset = day[0..-3].to_i if offset > 0 # It is possible that the day does not exist, such as a # 5th or 6th wednesday of the month. by_day_results << day_hits[offset - 1] if day_hits[offset - 1] else # if it was negative we count from the end of the array # might not exist, fx. -5th tuesday by_day_results << day_hits[offset] if day_hits[offset] end else # There was no counter (first, second, last wednesdays), so we # just need to add the all to the list). by_day_results.concat(day_hits) end end end by_month_day_results = [] if @by_month_day @by_month_day.each do |month_day| days_in_month = Time.days_in_month(start_date.month, start_date.year) # Removing values that are out of range for this month if month_day.to_i > days_in_month || month_day.to_i < 0 - days_in_month next end if month_day.to_i > 0 by_month_day_results << month_day.to_i else # Negative values by_month_day_results << days_in_month + 1 + month_day.to_i end end end # If there was just byDay or just byMonthDay, they just specify our # (almost) final list. If both were provided, then byDay limits the # list. if @by_month_day && @by_day result = by_month_day_results & by_day_results elsif @by_month_day result = by_month_day_results else result = by_day_results end result = result.uniq result = result.sort # The last thing that needs checking is the BYSETPOS. If it's set, it # means only certain items in the set survive the filter. return result unless @by_set_pos filtered_result = [] @by_set_pos.each do |set_pos| set_pos = set_pos.to_i set_pos += 1 if set_pos < 0 filtered_result << result[set_pos - 1] if result[set_pos - 1] end filtered_result = filtered_result.sort filtered_result end
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 574 def months recurrence_months = [] @by_month.each do |by_month| recurrence_months << by_month.to_i end recurrence_months end
Does the processing for advancing the iterator for daily frequency.
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 125 def next_daily unless @by_hour || @by_day @current_date += @interval.days return nil end recurrence_hours = hours if @by_hour recurrence_days = days if @by_day recurrence_months = months if @by_month loop do if @by_hour if @current_date.hour == 23 # to obey the interval rule @current_date += (@interval - 1).days end @current_date += 1.hour else @current_date += @interval.days end # Current month of the year current_month = @current_date.month # Current day of the week current_day = @current_date.wday # Current hour of the day current_hour = @current_date.hour break unless (@by_day && !recurrence_days.include?(current_day)) || (@by_hour && !recurrence_hours.include?(current_hour)) || (@by_month && !recurrence_months.include?(current_month)) end end
Does the processing for advancing the iterator for hourly frequency.
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 118 def next_hourly @current_date += @interval.hours end
Does the processing for advancing the iterator for monthly frequency.
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 210 def next_monthly current_day_of_month = @current_date.day unless @by_month_day || @by_day # If the current day is higher than the 28th, rollover can # occur to the next month. We Must skip these invalid # entries. if current_day_of_month < 29 @current_date += @interval.months else increase = 0 temp_date = nil loop do increase += 1 temp_date = @current_date + (@interval * increase).months break unless temp_date.day != current_day_of_month end @current_date = temp_date end return nil end occurrence = nil loop do occurrences = monthly_occurrences occurrence = nil stop = false occurrences.each do |this_occurrence| # The first occurrence thats higher than the current # day of the month wins. next unless this_occurrence > current_day_of_month occurrence = this_occurrence stop = true break end break if stop occurrence = occurrences.last unless occurrence # If we made it all the way here, it means there were no # valid occurrences, and we need to advance to the next # month. @current_date -= (@current_date.day - 1).days @current_date += @interval.months # This goes to 0 because we need to start counting at the # beginning. current_day_of_month = 0 end @current_date += (occurrence.to_i - @current_date.day).days end
Does the processing for advancing the iterator for weekly frequency.
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 166 def next_weekly if !@by_hour && !@by_day @current_date += @interval.weeks return nil end recurrence_hours = hours if @by_hour recurrence_days = days if @by_day # First day of the week: first_day = @day_map[@week_start] loop do if @by_hour @current_date += 1.hour else @current_date += 1.day end # Current day of the week current_day = @current_date.wday # Current hour of the day current_hour = @current_date.hour # We need to roll over to the next week if current_day == first_day && (!@by_hour || current_hour == 0) @current_date += (@interval - 1).weeks # We need to go to the first day of this week, but only if we # are not already on this first day of this week. if @current_date.wday != first_day @current_date -= (@current_date.wday - first_day).days end end # We have a match break unless (@by_day && !recurrence_days.include?(current_day)) || (@by_hour && !recurrence_hours.include?(current_hour)) end end
Does the processing for advancing the iterator for yearly frequency.
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 265 def next_yearly current_month = @current_date.month current_year = @current_date.year current_day_of_month = @current_date.day # No sub-rules, so we just advance by year unless @by_month # Unless it was a leap day! if current_month == 2 && current_day_of_month == 29 counter = 0 next_date = nil loop do counter += 1 # Here we increase the year count by the interval, until # we hit a date that's also in a leap year. # # We could just find the next interval that's dividable by # 4, but that would ignore the rule that there's no leap # year every year that's dividable by a 100, but not by # 400. (1800, 1900, 2100). So we just rely on the datetime # functions instead. next_date = @current_date + (@interval * counter).years break if next_date.to_date.leap? end @current_date = next_date return nil end # The easiest form @current_date += @interval.years return nil end current_month = @current_date.month current_year = @current_date.year current_day_of_month = @current_date.day advanced_to_new_month = false occurrence = nil # If we got a byDay or getMonthDay filter, we must first expand # further. if @by_day || @by_month_day loop do occurrences = monthly_occurrences stop = false occurrences.each do |this_occurrence| # The first occurrence that's higher than the current # day of the month wins. # If we advanced to the next month or year, the first # occurrence is always correct. next unless this_occurrence > current_day_of_month || advanced_to_new_month occurrence = this_occurrence stop = true break end occurrence = occurrences.last unless occurrence break if stop # If we made it here, it means we need to advance to # the next month or year. current_day_of_month = 1 advanced_to_new_month = true loop do current_month += 1 if current_month > 12 current_year += @interval current_month = 1 end break if @by_month.include?(current_month.to_s) end @current_date = @current_date + (current_year - @current_date.year).years + (current_month - @current_date.month).months + (current_day_of_month - @current_date.day).days end # If we made it here, it means we got a valid occurrence @current_date = @current_date + (current_year - @current_date.year).years + (current_month - @current_date.month).months + (occurrence - @current_date.day).days return nil else # These are the 'byMonth' rules, if there are no byDay or # byMonthDay sub-rules. loop do current_month += 1 if current_month > 12 current_year += @interval current_month = 1 end break if @by_month.include?(current_month.to_s) end @current_date = @current_date + (current_year - @current_date.year).years + (current_month - @current_date.month).months + (current_day_of_month - @current_date.day).days return nil end end
This method receives a string from an RRULE property, and populates this class with all the values.
@param [String|array] rrule
@return [void]
# File lib/tilia/v_object/recur/r_rule_iterator.rb, line 380 def parse_r_rule(rrule) if rrule.is_a?(String) rrule = Property::ICalendar::Recur.string_to_array(rrule) end rrule.each do |key, value| key = key.upcase case key when 'FREQ' value = value.downcase unless ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'].include?(value) fail InvalidDataException, "Unknown value for FREQ=#{value.upcase}" end @frequency = value when 'UNTIL' @until = DateTimeParser.parse(value, @start_date.time_zone) # In some cases events are generated with an UNTIL= # parameter before the actual start of the event. # # Not sure why this is happening. We assume that the # intention was that the event only recurs once. # # So we are modifying the parameter so our code doesn't # break. @until = @start_date if @until < @start_date when 'INTERVAL', 'COUNT' val = value.to_i if val < 1 fail InvalidDataException, "#{key.upcase} in RRULE must be a positive integer!" end key = key.downcase key == 'interval' ? @interval = val : @count = val when 'BYSECOND' @by_second = value.is_a?(Array) ? value : [value] when 'BYMINUTE' @by_minute = value.is_a?(Array) ? value : [value] when 'BYHOUR' @by_hour = value.is_a?(Array) ? value : [value] when 'BYDAY' value = value.is_a?(Array) ? value : [value] value.each do |part| unless part =~ /^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $/xi fail InvalidDataException, "Invalid part in BYDAY clause: #{part}" end end @by_day = value when 'BYMONTHDAY' @by_month_day = value.is_a?(Array) ? value : [value] when 'BYYEARDAY' @by_year_day = value.is_a?(Array) ? value : [value] when 'BYWEEKNO' @by_week_no = value.is_a?(Array) ? value : [value] when 'BYMONTH' @by_month = value.is_a?(Array) ? value : [value] when 'BYSETPOS' @by_set_pos = value.is_a?(Array) ? value : [value] when 'WKST' @week_start = value.upcase else fail InvalidDataException, "Not supported: #{key.upcase}" end end end