Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 48 additions & 19 deletions caldav/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ func match(filter CompFilter, comp *ical.Component) (bool, error) {
return filter.IsNotDefined, nil
}

var zeroDate time.Time
if filter.Start != zeroDate {
if !filter.Start.IsZero() || !filter.End.IsZero() {
match, err := matchCompTimeRange(filter.Start, filter.End, comp)
if err != nil {
return false, err
Expand Down Expand Up @@ -126,46 +125,76 @@ func matchPropFilter(filter PropFilter, comp *ical.Component) (bool, error) {

func matchCompTimeRange(start, end time.Time, comp *ical.Component) (bool, error) {
// See https://datatracker.ietf.org/doc/html/rfc4791#section-9.9
// The "start" attribute specifies the inclusive start of the time range,
// and the "end" attribute specifies the non-inclusive end of the time range.
// Both attributes MUST be specified as "date with UTC time" value.

// evaluate recurring components
rset, err := comp.RecurrenceSet(start.Location())
rset, err := comp.RecurrenceSet(time.UTC)
if err != nil {
return false, err
}
if rset != nil {
// TODO we can only set inclusive to true or false, but really the
// start time is inclusive while the end time is not :/
return len(rset.Between(start, end, true)) > 0, nil
// return len(rset.Between(start, end, true)) > 0, nil
// if start is zero then rset.After(zero) should work

// TODO: first_after_start only looks at DTSTART yielding wrong behaviour;
// an event can start before interval [start,end) but still intersect the interval;
// in this case it should be matched by according to RFC 4791.
//
// "The CALDAV:time-range XML element specifies that for a
// given calendaring REPORT request, the server MUST only return the
// calendar object resources that, depending on the context, have a
// component or property whose value intersects a specified time
// range."
//
// OPTIMIZATION: would make slightly more efficient code,
// i.e., fewer passes over rset iterator,
// if rset.Iterator's next() function was exported as Next()
// and the following code block was rewritten
if first_after_start := rset.After(start, true); first_after_start.IsZero() {
return false, nil
} else if end.IsZero() || first_after_start.Before(end) {
return true, nil
} else {
return false, nil
}
}

// TODO handle more than just events
if comp.Name != ical.CompEvent {
return false, nil
}
event := ical.Event{comp}
event := ical.Event{Component: comp}

eventStart, err := event.DateTimeStart(start.Location())
eventStart, err := event.DateTimeStart(time.UTC)
if err != nil {
return false, err
}
eventEnd, err := event.DateTimeEnd(end.Location())
eventEnd, err := event.DateTimeEnd(time.UTC)
if err != nil {
return false, err
}

// Event starts in time range
if eventStart.After(start) && (end.IsZero() || eventStart.Before(end)) {
duration_zero := eventStart.Equal(eventEnd)

// test if [eventStart, eventEnd) intersects [start, end)
// special handling if duration_zero;
// in that case check if eventStart is contained in [start,end)
//
// S_E compare event start versus filter end
// E_S compare event end versus filter start
//
// refer to table https://datatracker.ietf.org/doc/html/rfc4791#section-9.9
//
if S_E := eventStart.Compare(end); start.IsZero() && S_E < 0 {
return true, nil
}
// Event ends in time range
if eventEnd.After(start) && (end.IsZero() || eventEnd.Before(end)) {
} else if E_S := eventEnd.Compare(start); end.IsZero() && (E_S > 0 || (duration_zero && E_S >= 0)) {
return true, nil
}
// Event covers entire time range plus some
if eventStart.Before(start) && (!end.IsZero() && eventEnd.After(end)) {
} else if (S_E < 0 && E_S > 0) || (duration_zero && E_S >= 0 && S_E < 0) {
return true, nil
} else {
return false, nil
}
return false, nil
}

func matchPropTimeRange(start, end time.Time, field *ical.Prop) (bool, error) {
Expand Down
133 changes: 125 additions & 8 deletions caldav/match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,87 @@ UID:DC6C50A017428C5216A2F1CD@example.com
END:VEVENT
END:VCALENDAR`)

event4 := newCO(`
BEGIN:VCALENDAR
PRODID:DAVx5/4.4.5-ose ical4j/3.2.19 (org.fossify.calendar)
VERSION:2.0
BEGIN:VEVENT
CREATED:20250111T232306Z
DTEND;TZID=Europe/Paris:20250114T200000
DTSTAMP:20250111T235047Z
DTSTART;TZID=Europe/Paris:20250114T190000
RRULE:FREQ=DAILY;COUNT=2;INTERVAL=1
SEQUENCE:5
STATUS:TENTATIVE
SUMMARY:event
UID:FA4733E2-EDE6-454A-BAE1-0AC82E6384AB
X-APPLE-CREATOR-IDENTITY:com.apple.mobilecal
X-APPLE-CREATOR-TEAM-IDENTITY:0000000000
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION: description
TRIGGER:-PT30M
END:VALARM
END:VEVENT
BEGIN:VTIMEZONE
TZID:Europe/Paris
BEGIN:STANDARD
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
END:VTIMEZONE
END:VCALENDAR
`)

event5 := newCO(`
BEGIN:VCALENDAR
PRODID:DAVx5/4.4.5-ose ical4j/3.2.19 (org.fossify.calendar)
VERSION:2.0
BEGIN:VEVENT
CREATED:20250111T232306Z
DTSTAMP:20250111T235047Z
DTSTART;TZID=Europe/Paris:20250101T120000
STATUS:CONFIRMED
SUMMARY:event
UID:FA4733E2-EDE6-454A-BAE1-0AC82E6384AB
X-APPLE-CREATOR-IDENTITY:com.apple.mobilecal
X-APPLE-CREATOR-TEAM-IDENTITY:0000000000
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION: description
TRIGGER:-PT30M
END:VALARM
END:VEVENT
BEGIN:VTIMEZONE
TZID:Europe/Paris
BEGIN:STANDARD
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
END:DAYLIGHT
END:VTIMEZONE
END:VCALENDAR
`)

todo1 := newCO(`BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
Expand All @@ -172,8 +253,8 @@ END:VCALENDAR`)
{
name: "nil-query",
query: nil,
addrs: []CalendarObject{event1, event2, event3, todo1},
want: []CalendarObject{event1, event2, event3, todo1},
addrs: []CalendarObject{event1, event2, event3, event4, todo1},
want: []CalendarObject{event1, event2, event3, event4, todo1},
},
{
// https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.8
Expand All @@ -188,8 +269,8 @@ END:VCALENDAR`)
},
},
},
addrs: []CalendarObject{event1, event2, event3, todo1},
want: []CalendarObject{event1, event2, event3},
addrs: []CalendarObject{event1, event2, event3, event4, todo1},
want: []CalendarObject{event1, event2, event3, event4},
},
{
// https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.1
Expand All @@ -206,7 +287,7 @@ END:VCALENDAR`)
},
},
},
addrs: []CalendarObject{event1, event2, event3, todo1},
addrs: []CalendarObject{event1, event2, event3, event4, todo1},
want: []CalendarObject{event2, event3},
},
{
Expand Down Expand Up @@ -245,7 +326,7 @@ END:VCALENDAR`)
},
},
},
addrs: []CalendarObject{event1, event2, event3, todo1},
addrs: []CalendarObject{event1, event2, event3, event4, todo1},
want: []CalendarObject{event3},
},
{
Expand All @@ -267,7 +348,7 @@ END:VCALENDAR`)
},
},
},
addrs: []CalendarObject{event1, event2, event3, todo1},
addrs: []CalendarObject{event1, event2, event3, event4, todo1},
want: []CalendarObject{event1},
},
{
Expand All @@ -285,9 +366,45 @@ END:VCALENDAR`)
},
},
},
addrs: []CalendarObject{event1, event2, event3, todo1},
addrs: []CalendarObject{event1, event2, event3, event4, todo1},
want: []CalendarObject{event2},
},
{
// only end tag
name: "recurring events in time range",
query: &CalendarQuery{
CompFilter: CompFilter{
Name: "VCALENDAR",
Comps: []CompFilter{
CompFilter{
Name: "VEVENT",
End: toDate(t, "20250114T180000Z"),
},
},
},
},
addrs: []CalendarObject{event1, event2, event3, event4, todo1},
want: []CalendarObject{event1, event2, event3},
},
{
// event with no DTEND or DURATION
name: "No DTEND or DURATION",
query: &CalendarQuery{
CompFilter: CompFilter{
Name: "VCALENDAR",
Comps: []CompFilter{
CompFilter{
Name: "VEVENT",
Start: toDate(t, "20250101T110000Z"),
End: toDate(t, "20250114T110001Z"),
},
},
},
},
addrs: []CalendarObject{event4, event5},
want: []CalendarObject{event5},
},

// TODO add more examples
} {
t.Run(tc.name, func(t *testing.T) {
Expand Down