Skip to content
Merged
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
22 changes: 5 additions & 17 deletions Sources/ICalGuyKit/Services/DateGrouper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,12 @@ public struct DateGrouper: Sendable {

for event in events {
let eventStart = calendar.startOfDay(for: event.startDate)
let eventEnd = calendar.startOfDay(for: event.endDate)

let effectiveEnd: Date
if event.isAllDay {
// All-day events: EventKit sets endDate to midnight of the *next* day.
// Subtract 1 day so a 1-day event stays in 1 bucket, a 3-day event spans 3 days.
effectiveEnd = calendar.date(byAdding: .day, value: -1, to: eventEnd) ?? eventEnd
} else {
// Timed events ending exactly at midnight: don't spill into the next day
let endComponents = calendar.dateComponents([.hour, .minute, .second], from: event.endDate)
if endComponents.hour == 0 && endComponents.minute == 0 && endComponents.second == 0
&& eventEnd > eventStart
{
effectiveEnd = calendar.date(byAdding: .day, value: -1, to: eventEnd) ?? eventEnd
} else {
effectiveEnd = eventEnd
}
}
// Bucket by the last instant the event occupies. EventKit ends all-day events at
// 23:59:59 of the last day (not midnight of the next), and timed events ending exactly
// at midnight shouldn't spill into the next day. Zero-duration events stay on their start day.
let lastInstant = max(event.startDate, event.endDate.addingTimeInterval(-1))
let effectiveEnd = calendar.startOfDay(for: lastInstant)

var day = eventStart
// Clip to query range boundaries
Expand Down
45 changes: 45 additions & 0 deletions Tests/ICalGuyKitTests/DateGrouperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,51 @@ final class DateGrouperTests: XCTestCase {
XCTAssertEqual(groups[0].events.count, 1)
}

// Real EventKit ends all-day events at 23:59:59 of the last day, not midnight of the next.

func testSingleDayAllDayEventWithEventKitEndDate() {
let events = [
makeEvent(
id: "1", title: "Holiday",
startDate: date(2024, 3, 15), endDate: date(2024, 3, 16).addingTimeInterval(-1),
isAllDay: true)
]

let groups = grouper.groupByDate(events, from: date(2024, 3, 1), to: date(2024, 3, 31))

XCTAssertEqual(groups.count, 1)
XCTAssertEqual(groups[0].date, "2024-03-15")
XCTAssertEqual(groups[0].events.count, 1)
}

func testMultiDayAllDayEventWithEventKitEndDateIncludesLastDay() {
// Mar 15-17 inclusive
let events = [
makeEvent(
id: "1", title: "Conference",
startDate: date(2024, 3, 15), endDate: date(2024, 3, 18).addingTimeInterval(-1),
isAllDay: true)
]

let groups = grouper.groupByDate(events)

XCTAssertEqual(groups.map(\.date), ["2024-03-15", "2024-03-16", "2024-03-17"])
}

func testAllDayEventWithEventKitEndDateClippedToRange() {
// Mar 14-17 inclusive, queried Mar 15-16
let events = [
makeEvent(
id: "1", title: "Conference",
startDate: date(2024, 3, 14), endDate: date(2024, 3, 18).addingTimeInterval(-1),
isAllDay: true)
]

let groups = grouper.groupByDate(events, from: date(2024, 3, 15), to: date(2024, 3, 16))

XCTAssertEqual(groups.map(\.date), ["2024-03-15", "2024-03-16"])
}

func testTimedOvernightEventSpansTwoDays() {
// Timed event from 10pm Mar 15 to 2am Mar 16
let events = [
Expand Down