class Tilia::VObject::Recur::EventIterator
This class is used to determine new for a recurring event, when the next events occur.
This iterator may loop infinitely in the future, therefore it is important that if you use this class, you set hard limits for the amount of iterations you want to handle.
Note that currently there is not full support for the entire iCalendar specification, as it's very complex and contains a lot of permutations that's not yet used very often in software.
For the focus has been on features as they actually appear in Calendaring software, but this may well get expanded as needed / on demand
The following RRULE properties are supported
* UNTIL * INTERVAL * COUNT * FREQ=DAILY * BYDAY * BYHOUR * BYMONTH * FREQ=WEEKLY * BYDAY * BYHOUR * WKST * FREQ=MONTHLY * BYMONTHDAY * BYDAY * BYSETPOS * FREQ=YEARLY * BYMONTH * BYMONTHDAY (only if BYMONTH is also set) * BYDAY (only if BYMONTH is also set)
Anything beyond this is 'undefined', which means that it may get ignored, or you may get unexpected results. The effect is that in some applications the specified recurrence may look incorrect, or is missing.
The recurrence iterator also does not yet support THISANDFUTURE.
Public Class Methods
Creates the iterator.
There's three ways to set up the iterator.
-
You can pass a VCALENDAR component and a UID.
-
You can pass an array of VEVENTs (all UIDS should match).
-
You can pass a single VEVENT component.
Only the second method is recomended. The other 1 and 3 will be removed at some point in the future.
The uid parameter is only required for the first method.
@param [Component|array] input @param [String, nil] uid @param [ActiveSupport::TimeZone] time_zone Reference timezone for floating dates and
times.
# File lib/tilia/v_object/recur/event_iterator.rb, line 72 def initialize(input, uid = nil, time_zone = nil) @overridden_events = [] @exceptions = {} @all_day = false time_zone = ActiveSupport::TimeZone.new('UTC') if time_zone.nil? @time_zone = time_zone if input.is_a?(Array) events = input elsif input.is_a?(Component::VEvent) # Single instance mode. events = [input] else # Calendar + UID mode. uid = uid.to_s if uid.blank? fail ArgumentError, 'The UID argument is required when a VCALENDAR is passed to this constructor' end unless input.key?('VEVENT') fail ArgumentError, 'No events found in this calendar' end events = input.by_uid(uid) end events.each do |vevent| if !vevent.key?('RECURRENCE-ID') @master_event = vevent else @exceptions[vevent['RECURRENCE-ID'].date_time(@time_zone).to_i] = true @overridden_events << vevent end end unless @master_event # No base event was found. CalDAV does allow cases where only # overridden instances are stored. # # In this particular case, we're just going to grab the first # event and use that instead. This may not always give the # desired result. if @overridden_events.size == 0 fail ArgumentError, "This VCALENDAR did not have an event with UID: #{uid}" end @master_event = @overridden_events.shift end @start_date = @master_event['DTSTART'].date_time(@time_zone) @all_day = !@master_event['DTSTART'].time? if @master_event.key?('EXDATE') @master_event['EXDATE'].each do |ex_date| ex_date.date_times(@time_zone).each do |dt| @exceptions[dt.to_i] = true end end end if @master_event.key?('DTEND') @event_duration = (@master_event['DTEND'].date_time(@time_zone).to_i - @start_date.to_i).seconds elsif @master_event.key?('DURATION') @event_duration = @master_event['DURATION'].date_interval elsif @all_day @event_duration = 1.day else @event_duration = 0.seconds end if @master_event.key?('RDATE') @recur_iterator = Recur::RDateIterator.new( @master_event['RDATE'].parts, @start_date ) elsif @master_event.key?('RRULE') @recur_iterator = Recur::RRuleIterator.new( @master_event['RRULE'].parts, @start_date ) else @recur_iterator = Recur::RRuleIterator.new( { 'FREQ' => 'DAILY', 'COUNT' => 1 }, @start_date ) end rewind fail Recur::NoInstancesException, 'This recurrence rule does not generate any valid instances' unless valid end
Public Instance Methods
Returns the date for the current position of the iterator.
@return [Time]
# File lib/tilia/v_object/recur/event_iterator.rb, line 170 def current @current_date.clone if @current_date end
This method returns the end date for the current iteration of the event.
@return [Time]
# File lib/tilia/v_object/recur/event_iterator.rb, line 186 def dt_end return nil unless valid @current_date + @event_duration end
This method returns the start date for the current iteration of the event.
@return [Time]
# File lib/tilia/v_object/recur/event_iterator.rb, line 178 def dt_start @current_date.clone if @current_date end
# File lib/tilia/v_object/recur/event_iterator.rb, line 403 def each to_enum.each { |i| yield(i) } end
Returns a VEVENT for the current iterations of the event.
This VEVENT will have a recurrence id, and it's DTSTART and DTEND altered.
@return [VEvent]
# File lib/tilia/v_object/recur/event_iterator.rb, line 198 def event_object return @current_overridden_event if @current_overridden_event event = @master_event.clone event.delete('RRULE') event.delete('EXDATE') event.delete('RDATE') event.delete('EXRULE') event.delete('RECURRENCE-ID') floating = event['DTSTART'].floating? event['DTSTART'].date_time = dt_start event['DTSTART'].floating = floating if event.key?('DTEND') floating = event['DTEND'].floating? event['DTEND'].date_time = dt_end event['DTEND'].floating = floating end # Including a RECURRENCE-ID to the object, unless this is the first # object. # # The inner recurIterator is always one step ahead, this is why we're # checking for the key being higher than 1. if @recur_iterator.key > 1 recurid = event['DTSTART'].clone recurid.name = 'RECURRENCE-ID' event.add(recurid) end event end
Quickly jump to a date in the future.
@param [Time] date_time
# File lib/tilia/v_object/recur/event_iterator.rb, line 325 def fast_forward(date_time) self.next while valid && dt_end < date_time end
Returns true if this recurring event never ends.
@return [Boolean]
# File lib/tilia/v_object/recur/event_iterator.rb, line 332 def infinite? @recur_iterator.infinite? end
Returns the current position of the iterator.
This is for us simply a 0-based index.
@return [Fixnum]
# File lib/tilia/v_object/recur/event_iterator.rb, line 236 def key # The counter is always 1 ahead. @counter - 1 end
Advances the iterator with one step.
@return [void]
# File lib/tilia/v_object/recur/event_iterator.rb, line 277 def next @current_overridden_event = nil @counter += 1 if @next_date # We had a stored value. next_date = @next_date @next_date = nil else # We need to ask rruleparser for the next date. # We need to do this until we find a date that's not in the # exception list. loop do unless @recur_iterator.valid next_date = nil break end next_date = @recur_iterator.current @recur_iterator.next break unless @exceptions.key?(next_date.to_i) end end # next_date now contains what rrule thinks is the next one, but an # overridden event may cut ahead. if @overridden_events_index.any? timestamp = @overridden_events_index.keys[-1] offset = @overridden_events_index[timestamp] if !next_date || timestamp < next_date.to_i # Overridden event comes first. @current_overridden_event = @overridden_events[offset] # Putting the rrule next date aside. @next_date = next_date @current_date = @current_overridden_event['DTSTART'].date_time(@time_zone) # Ensuring that this item will only be used once. @overridden_events_index.delete(timestamp) # Exit point! return nil end end @current_date = next_date end
Sets the iterator back to the starting point.
# File lib/tilia/v_object/recur/event_iterator.rb, line 255 def rewind @recur_iterator.rewind # re-creating overridden event index. index = {} @overridden_events.each_with_index do |event, key| stamp = event['DTSTART'].date_time(@time_zone).to_i index[stamp] = key end index = index.to_a.sort { |a, b| b[0] <=> a[0] }.to_h @counter = 0 @overridden_events_index = index @current_overridden_event = nil @next_date = nil @current_date = @start_date.clone self.next end
The next date from the rrule parser.
Sometimes we need to temporary store the next date, because an overridden event came before.
@return [Time] RUBY: attr_accessor :next_date
# File lib/tilia/v_object/recur/event_iterator.rb, line 393 def to_a fail 'Can not convert infinite event to array!' if infinite? list = [] to_enum.each do |date| list << date end list end
# File lib/tilia/v_object/recur/event_iterator.rb, line 407 def to_enum copy = clone copy.rewind Enumerator.new do |yielder| while copy.valid yielder << copy.dt_start copy.next end end end
This is called after next, to see if the iterator is still at a valid position, or if it's at the end.
@return [Boolean]
# File lib/tilia/v_object/recur/event_iterator.rb, line 245 def valid if @counter > Settings.max_recurrences && Settings.max_recurrences != -1 fail MaxInstancesExceededException, "Recurring events are only allowed to generate #{Settings.max_recurrences}" end !@current_date.nil? end