Module: Protocol::Caldav::Ical::Expand

Defined in:
lib/protocol/caldav/ical/expand.rb

Class Method Summary collapse

Class Method Details

.expand(component, range_start:, range_end:, max_occurrences: 10000) ⇒ Object

Expand a recurring VCALENDAR into individual instances. Returns a serialized VCALENDAR string with RRULE removed and each occurrence as a separate VEVENT with RECURRENCE-ID.



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
# File 'lib/protocol/caldav/ical/expand.rb', line 17

def expand(component, range_start:, range_end:, max_occurrences: 10000)
  return serialize_component(component) unless component.name.casecmp?('VCALENDAR')

  # Find the base VEVENT with RRULE
  base = component.find_components('VEVENT').find { |v| v.find_property('RRULE') }
  return serialize_component(component) unless base

  # Collect override VEVENTs (same UID, has RECURRENCE-ID)
  uid = base.find_property('UID')&.value
  overrides = {}
  component.find_components('VEVENT').each do |v|
    rid = v.find_property('RECURRENCE-ID')
    if rid && v.find_property('UID')&.value == uid
      rid_time = Filter::Match.send(:parse_datetime_string, rid.value.strip)
      overrides[rid_time] = v if rid_time
    end
  end

  dtstart_prop = base.find_property('DTSTART')
  dtstart = Filter::Match.send(:parse_datetime_string, dtstart_prop.value.strip)
  return serialize_component(component) unless dtstart

  dtend = Filter::Match.send(:parse_ical_datetime, base, 'DTEND')
  duration = dtend ? (dtend - dtstart).to_i : 3600

  rrule = base.find_property('RRULE').value.strip
  exdates = base.find_all_properties('EXDATE').filter_map do |ex|
    Filter::Match.send(:parse_datetime_string, ex.value.strip)
  end

  occurrences = Rrule.expand(
    dtstart: dtstart,
    rrule_value: rrule,
    range_start: range_start,
    range_end: range_end,
    exdates: exdates,
    max_count: max_occurrences
  )

  instances = occurrences.map do |occ_start|
    override = overrides[occ_start]
    if override
      serialize_vevent_instance(override, occ_start)
    else
      serialize_expanded_instance(base, occ_start, duration)
    end
  end

  # Also include overrides that aren't in the base RRULE expansion
  overrides.each do |rid_time, override_vevent|
    next if occurrences.any? { |o| Rrule.send(:times_equal?, o, rid_time) }
    odt = Filter::Match.send(:parse_ical_datetime, override_vevent, 'DTSTART')
    next unless odt && odt >= range_start && odt < range_end
    instances << serialize_vevent_instance(override_vevent, rid_time)
  end

  "BEGIN:VCALENDAR\r\n#{instances.join}END:VCALENDAR\r\n"
end