File: occurrence_enumerator.rb

package info (click to toggle)
mhc 1.1.1-2
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 2,320 kB
  • ctags: 3,529
  • sloc: ruby: 12,404; lisp: 7,448; makefile: 70; sh: 69
file content (265 lines) | stat: -rw-r--r-- 9,194 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
module RiCal
  #- ©2009 Rick DeNatale
  #- All rights reserved. Refer to the file README.txt for the license
  #
  # OccurrenceEnumerator provides common methods for CalendarComponents that support recurrence
  # i.e. Event, Journal, Todo, and TimezonePeriod
  module OccurrenceEnumerator

    include Enumerable

    def default_duration # :nodoc:
      dtend && dtstart.to_ri_cal_date_time_value.duration_until(dtend.to_ri_cal_date_time_value)
    end

    def default_start_time # :nodoc:
      dtstart && dtstart.to_ri_cal_date_time_value
    end

    class EmptyRulesEnumerator # :nodoc:
      def self.next_occurrence
        nil
      end

      def self.bounded?
        true
      end

      def self.empty?
        true
      end
    end

    # OccurrenceMerger takes multiple recurrence rules and enumerates the combination in sequence.
    class OccurrenceMerger # :nodoc:
      def self.for(component, rules)
        if rules.nil? || rules.empty?
          EmptyRulesEnumerator
        elsif rules.length == 1
          rules.first.enumerator(component)
        else
          new(component, rules)
        end
      end

      attr_accessor :enumerators, :nexts

      def initialize(component, rules)
        self.enumerators = rules.map {|rrule| rrule.enumerator(component)}
        @bounded = enumerators.all? {|enumerator| enumerator.bounded?}
        @empty = enumerators.all? {|enumerator| enumerator.empty?}
        self.nexts = @enumerators.map {|enumerator| enumerator.next_occurrence}
      end

      def empty?
        @empty
      end

      # return the earliest of each of the enumerators next occurrences
      def next_occurrence
        result = nexts.compact.sort.first
        if result
          nexts.each_with_index { |datetimevalue, i| @nexts[i] = @enumerators[i].next_occurrence if result == datetimevalue }
        end
        result
      end

      def bounded?
        @bounded
      end
    end

    # EnumerationInstance holds the values needed during the enumeration of occurrences for a component.
    class EnumerationInstance # :nodoc:
      include Enumerable

      def initialize(component)
        @component = component
        @rrules = OccurrenceMerger.for(@component, [@component.rrule_property, @component.rdate_property].flatten.compact)
        @exrules = OccurrenceMerger.for(@component, [@component.exrule_property, @component.exdate_property].flatten.compact)
        @yielded = 0
      end

      # return the next exclusion which starts at the same time or after the start time of the occurrence
      # return nil if this exhausts the exclusion rules
      def exclusion_for(occurrence)
        while (@next_exclusion && @next_exclusion.dtstart < occurrence.dtstart)
          @next_exclusion = @exrules.next_occurrence
        end
        @next_exclusion
      end

      # TODO: Need to research this, I beleive that this should also take the end time into account,
      #       but I need to research
      def exclusion_match?(occurrence, exclusion)
        exclusion && (occurrence.dtstart == exclusion.dtstart)
      end

      # Also exclude occurrences before the :starting date_time
      def before_start?(occurrence)
        (@start && occurrence.dtstart.to_datetime < @start) ||
        @overlap_range && occurrence.before_range?(@overlap_range)
      end

      def next_occurrence
        @next_exclusion ||= @exrules.next_occurrence
        occurrence = nil

        until occurrence
          if (occurrence = @rrules.next_occurrence)
            if exclusion_match?(occurrence, exclusion_for(occurrence))
              occurrence = nil # Look for the next one
            end
          else
            break
          end
        end
        occurrence
      end

      def options_stop(occurrence)
        occurrence != :excluded &&
        (@cutoff && occurrence.dtstart.to_datetime >= @cutoff) || 
        (@count && @yielded >= @count) ||
        (@overlap_range && occurrence.after_range?(@overlap_range))
      end


      # yield each occurrence to a block
      # some components may be open-ended, e.g. have no COUNT or DTEND
      def each(options = nil)
        process_options(options) if options
        if @rrules.empty?
          unless before_start?(@component)
            yield @component unless options_stop(@component)
          end
        else
          occurrence = next_occurrence
          while (occurrence)
            candidate = @component.recurrence(occurrence)
            if options_stop(candidate)
              occurrence = nil
            else
              unless before_start?(candidate)
                @yielded += 1
                yield candidate
              end
              occurrence = next_occurrence
            end
          end
        end
      end
      
      def bounded?
        @rrules.bounded? || @count || @cutoff || @overlap_range
      end
      
      def process_overlap_range(overlap_range)
        if overlap_range
          @overlap_range = [overlap_range.first.to_overlap_range_start, overlap_range.last.to_overlap_range_end]
        end
      end

      def process_options(options)
        @start = options[:starting] && options[:starting].to_datetime
        @cutoff = options[:before] && options[:before].to_datetime
        @overlap_range = process_overlap_range(options[:overlapping])
        @count = options[:count]
      end

      def to_a(options = {})
        process_options(options)
        raise ArgumentError.new("This component is unbounded, cannot produce an array of occurrences!") unless bounded?
        super()
      end

      alias_method :entries, :to_a
    end

    # return an array of occurrences according to the options parameter.  If a component is not bounded, and
    # the number of occurrences to be returned is not constrained by either the :before, or :count options
    # an ArgumentError will be raised.
    #
    # The components returned will be the same type as the receiver, but will have any recurrence properties
    # (rrule, rdate, exrule, exdate) removed since they are single occurrences, and will have the recurrence-id
    # property set to the occurrences dtstart value. (see RFC 2445 sec 4.8.4.4 pp 107-109)
    #
    # parameter options:
    # * :starting:: a Date, Time, or DateTime, no occurrences starting before this argument will be returned
    # * :before:: a Date, Time, or DateTime, no occurrences starting on or after this argument will be returned.
    # * :count:: an integer which limits the number of occurrences returned.
    # * :overlapping:: a two element array of Dates, Times, or DateTimes, assumed to be in chronological order. Only occurrences which are either totally or partially within the range will be returned.
    def occurrences(options={})
      enumeration_instance.to_a(options)
    end

    # TODO: Thread safe?
    def enumeration_instance #:nodoc:
      EnumerationInstance.new(self)
    end
    
    def before_range?(overlap_range)
      finish = finish_time
      !finish_time || finish_time < overlap_range.first
    end

    def after_range?(overlap_range)
      start = start_time
      !start || start > overlap_range.last
    end
    
    # execute the block for each occurrence
    def each(&block) # :yields: Component
      enumeration_instance.each(&block)
    end

    # A predicate which determines whether the component has a bounded set of occurrences
    def bounded?
      enumeration_instance.bounded?
    end

    # Return a array whose first element is a UTC DateTime representing the start of the first
    # occurrence, and whose second element is a UTC DateTime representing the end of the last
    # occurrence.
    # If the receiver is not bounded then the second element will be nil.
    #
    # The purpose of this method is to provide values which may be used as database attributes so
    # that a query can find all occurence enumerating components which may have occurrences within
    # a range of times.
    def zulu_occurrence_range
      if bounded?
        all = occurrences
        first, last = all.first, all.last
      else
        first = occurrences(:count => 1).first
        last = nil
      end
      [first.zulu_occurrence_range_start_time, last ? last.zulu_occurrence_range_finish_time : nil]
    end

    def set_occurrence_properties!(occurrence) # :nodoc:
      occurrence_end = occurrence.dtend
      occurrence_start = occurrence.dtstart
      @rrule_property = nil
      @exrule_property = nil
      @rdate_property = nil
      @exdate_property = nil
      @recurrence_id_property = occurrence_start
      if @dtend_property && !occurrence_end
         occurrence_end = occurrence_start + (@dtend_property - @dtstart_property)
      end
      @dtstart_property = @dtstart_property.for_occurrence(occurrence_start)
      @dtend_property = (@dtend_property || @dtstart_property).for_occurrence(occurrence_end) if occurrence_end
      self
    end

    def recurrence(occurrence) # :nodoc:
      result = self.dup.set_occurrence_properties!(occurrence)
    end
    
    def recurs?
      @rrule_property && @rrule_property.length > 0 || @rdate_property && @rdate_property.length > 0
    end

  end
end