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
2 changes: 1 addition & 1 deletion App/Services/Calendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ final class CalendarService: Service {
),
"end": .string(
description:
"End date/time (defaults to one week from start; one day if start is date-only). If timezone is omitted, local time is assumed.",
"End date/time (defaults to one week from start; one day if start is date-only). If timezone is omitted, local time is assumed. A date-only value includes that whole day.",
format: .dateTime
),
"calendars": .array(
Expand Down
57 changes: 36 additions & 21 deletions App/Services/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ final class MessageService: NSObject, Service, NSOpenSavePanelDelegate {
),
"end": .string(
description:
"End of the date range (exclusive). If timezone is omitted, local time is assumed. Date-only uses local midnight.",
"End of the date range (exclusive). If timezone is omitted, local time is assumed. A date-only value includes that whole day.",
format: .dateTime
),
"query": .string(
Expand Down Expand Up @@ -151,27 +151,31 @@ final class MessageService: NSObject, Service, NSOpenSavePanelDelegate {
$0.stringValue
}) ?? []

// Either bound may be given alone; the other stays open.
// chat.db stores dates as Int64 nanoseconds since 2001, so the open ends
// use dates that fit, rather than Date.distantPast and Date.distantFuture.
let calendar = Calendar.current
func parsedDate(_ name: String) throws -> (date: Date, isDateOnly: Bool)? {
guard let value = arguments[name], !value.isNull else { return nil }
guard let string = value.stringValue else {
throw ArgumentError.invalid("\(name) has the wrong type")
}
guard let parsed = ISO8601DateFormatter.parsedLenientISO8601Date(fromISO8601String: string)
else {
throw ArgumentError.invalid("\(name) must be an ISO 8601 date")
}
return parsed
}
let start = try parsedDate("start")
.map { calendar.normalizedStartDate(from: $0.date, isDateOnly: $0.isDateOnly) }
let end = try parsedDate("end")
.map { calendar.normalizedEndDate(from: $0.date, isDateOnly: $0.isDateOnly) }
var dateRange: Range<Date>?
if let startDateStr = arguments["start"]?.stringValue,
let endDateStr = arguments["end"]?.stringValue,
let parsedStart = ISO8601DateFormatter.parsedLenientISO8601Date(
fromISO8601String: startDateStr
),
let parsedEnd = ISO8601DateFormatter.parsedLenientISO8601Date(
fromISO8601String: endDateStr
)
{
let calendar = Calendar.current
let normalizedStart = calendar.normalizedStartDate(
from: parsedStart.date,
isDateOnly: parsedStart.isDateOnly
)
let normalizedEnd = calendar.normalizedEndDate(
from: parsedEnd.date,
isDateOnly: parsedEnd.isDateOnly
)

dateRange = normalizedStart ..< normalizedEnd
if start != nil || end != nil {
let lowerBound = start ?? Date(timeIntervalSinceReferenceDate: -9_000_000_000)
let upperBound = end ?? Date(timeIntervalSinceReferenceDate: 9_000_000_000)
// An end before the start matches nothing.
dateRange = lowerBound ..< max(lowerBound, upperBound)
}

let searchTerm = arguments["query"]?.stringValue
Expand Down Expand Up @@ -425,6 +429,17 @@ final class MessageService: NSObject, Service, NSOpenSavePanelDelegate {
return FileManager.default.isReadableFile(atPath: messagesDatabasePath)
}

private enum ArgumentError: LocalizedError {
case invalid(String)

var errorDescription: String? {
switch self {
case .invalid(let message):
return "Invalid argument: \(message)"
}
}
}

private enum DatabaseAccessError: LocalizedError {
case noBookmarkFound
case securityScopeAccessFailed
Expand Down
14 changes: 10 additions & 4 deletions App/Services/Phone.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ final class PhoneService: NSObject, Service, NSOpenSavePanelDelegate {
),
"start": .string(
description:
"Start of the date range (inclusive). ISO 8601 format. If timezone is omitted, local time is assumed.",
"Start of the date range (inclusive). ISO 8601 format. If timezone is omitted, local time is assumed. Date-only uses local midnight.",
format: .dateTime
),
"end": .string(
description:
"End of the date range (exclusive). ISO 8601 format. If timezone is omitted, local time is assumed.",
"End of the date range (exclusive). ISO 8601 format. If timezone is omitted, local time is assumed. A date-only value includes that whole day.",
format: .dateTime
),
"call_type": .string(
Expand Down Expand Up @@ -76,7 +76,10 @@ final class PhoneService: NSObject, Service, NSOpenSavePanelDelegate {
else {
throw ArgumentError.invalid("start must be an ISO 8601 date")
}
request.startDate = parsed.date
request.startDate = Calendar.current.normalizedStartDate(
from: parsed.date,
isDateOnly: parsed.isDateOnly
)
}
if let end = try self.argument("end", in: arguments, as: \.stringValue) {
guard
Expand All @@ -86,7 +89,10 @@ final class PhoneService: NSObject, Service, NSOpenSavePanelDelegate {
else {
throw ArgumentError.invalid("end must be an ISO 8601 date")
}
request.endDate = parsed.date
request.endDate = Calendar.current.normalizedEndDate(
from: parsed.date,
isDateOnly: parsed.isDateOnly
)
}

try await self.requestDatabaseAccess()
Expand Down
46 changes: 25 additions & 21 deletions App/Services/Reminders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ final class RemindersService: Service {
),
"end": .string(
description:
"End date/time range for fetching reminders. If timezone is omitted, local time is assumed. Date-only uses local midnight.",
"End date/time range for fetching reminders. If timezone is omitted, local time is assumed. A date-only value includes that whole day.",
format: .dateTime
),
"lists": .array(
Expand Down Expand Up @@ -148,31 +148,35 @@ final class RemindersService: Service {
endDate = calendar.normalizedEndDate(from: endDateValue, isDateOnly: endIsDateOnly)
}

// Create predicate based on completion status
let predicate: NSPredicate
// Choose predicates based on completion status: completed reminders match by
// completion date and incomplete ones by due date.
let completedPredicate = self.eventStore.predicateForCompletedReminders(
withCompletionDateStarting: startDate,
ending: endDate,
calendars: reminderLists
)
let incompletePredicate = self.eventStore.predicateForIncompleteReminders(
withDueDateStarting: startDate,
ending: endDate,
calendars: reminderLists
)
let predicates: [NSPredicate]
if case .bool(let completed) = arguments["completed"] {
if completed {
predicate = self.eventStore.predicateForCompletedReminders(
withCompletionDateStarting: startDate,
ending: endDate,
calendars: reminderLists
)
} else {
predicate = self.eventStore.predicateForIncompleteReminders(
withDueDateStarting: startDate,
ending: endDate,
calendars: reminderLists
)
}
predicates = [completed ? completedPredicate : incompletePredicate]
} else if startDate != nil || endDate != nil {
// Without a completion status, a date range applies to both kinds.
predicates = [incompletePredicate, completedPredicate]
} else {
// If completion status not specified, use incomplete predicate as default
predicate = self.eventStore.predicateForReminders(in: reminderLists)
predicates = [self.eventStore.predicateForReminders(in: reminderLists)]
}

// Fetch reminders
let reminders = try await withCheckedThrowingContinuation { continuation in
self.eventStore.fetchReminders(matching: predicate) { fetchedReminders in
continuation.resume(returning: fetchedReminders ?? [])
var reminders: [EKReminder] = []
for predicate in predicates {
reminders += try await withCheckedThrowingContinuation { continuation in
self.eventStore.fetchReminders(matching: predicate) { fetchedReminders in
continuation.resume(returning: fetchedReminders ?? [])
}
}
}

Expand Down
Loading