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
56 changes: 36 additions & 20 deletions Sources/RemindCore/DateParsing.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import Foundation

public struct ParsedDate: Sendable, Equatable {
public let date: Date
public let isDateOnly: Bool

public init(date: Date, isDateOnly: Bool) {
self.date = date
self.isDateOnly = isDateOnly
}
}

public enum DateParsing {
public static func parseUserDate(
_ input: String,
now: Date = Date(),
calendar: Calendar = .current
) -> Date? {
) -> ParsedDate? {
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
let lower = trimmed.lowercased()

Expand All @@ -17,37 +27,43 @@ public enum DateParsing {
isoFormatter(withFraction: true).date(from: trimmed)
?? isoFormatter(withFraction: false).date(from: trimmed)
if let iso {
return iso
return ParsedDate(date: iso, isDateOnly: false)
}

for formatter in dateFormatters() {
for (formatter, dateOnly) in dateFormattersWithContext() {
if let date = formatter.date(from: trimmed) {
return date
return ParsedDate(date: date, isDateOnly: dateOnly)
}
}

return nil
}

public static func formatDisplay(_ date: Date, calendar: Calendar = .current) -> String {
public static func formatDisplay(
_ date: Date, isDateOnly: Bool = false, calendar: Calendar = .current
) -> String {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.timeZone = calendar.timeZone
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.timeStyle = isDateOnly ? .none : .short
return formatter.string(from: date)
}

private static func parseRelativeDate(_ input: String, now: Date, calendar: Calendar) -> Date? {
private static func parseRelativeDate(
_ input: String, now: Date, calendar: Calendar
) -> ParsedDate? {
switch input {
case "today":
return calendar.startOfDay(for: now)
return ParsedDate(date: calendar.startOfDay(for: now), isDateOnly: true)
case "tomorrow":
return calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: now))
.map { ParsedDate(date: $0, isDateOnly: true) }
case "yesterday":
return calendar.date(byAdding: .day, value: -1, to: calendar.startOfDay(for: now))
.map { ParsedDate(date: $0, isDateOnly: true) }
case "now":
return now
return ParsedDate(date: now, isDateOnly: false)
default:
return nil
}
Expand All @@ -62,22 +78,22 @@ public enum DateParsing {
return formatter
}

private static func dateFormatters() -> [DateFormatter] {
let formats = [
"yyyy-MM-dd",
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd HH:mm:ss",
"MM/dd/yyyy",
"MM/dd/yyyy HH:mm",
"dd-MM-yy",
"dd-MM-yyyy",
private static func dateFormattersWithContext() -> [(DateFormatter, Bool)] {
let formats: [(String, Bool)] = [
("yyyy-MM-dd", true),
("yyyy-MM-dd HH:mm", false),
("yyyy-MM-dd HH:mm:ss", false),
("MM/dd/yyyy", true),
("MM/dd/yyyy HH:mm", false),
("dd-MM-yy", true),
("dd-MM-yyyy", true),
]
return formats.map { format in
return formats.map { format, dateOnly in
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone.current
formatter.dateFormat = format
return formatter
return (formatter, dateOnly)
}
}
}
70 changes: 25 additions & 45 deletions Sources/RemindCore/EventKitStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,11 @@ public actor RemindersStore {
reminder.notes = draft.notes
reminder.calendar = calendar
reminder.priority = draft.priority.eventKitValue
if let dueDate = draft.dueDate {
reminder.dueDateComponents = calendarComponents(from: dueDate)
if let parsed = draft.dueDate {
reminder.dueDateComponents = calendarComponents(from: parsed.date, dateOnly: parsed.isDateOnly)
}
try eventStore.save(reminder, commit: true)
return ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
return item(from: reminder)
}

public func updateReminder(id: String, update: ReminderUpdate) async throws -> ReminderItem {
Expand All @@ -127,8 +117,8 @@ public actor RemindersStore {
reminder.notes = notes
}
if let dueDateUpdate = update.dueDate {
if let dueDate = dueDateUpdate {
reminder.dueDateComponents = calendarComponents(from: dueDate)
if let parsed = dueDateUpdate {
reminder.dueDateComponents = calendarComponents(from: parsed.date, dateOnly: parsed.isDateOnly)
} else {
reminder.dueDateComponents = nil
}
Expand All @@ -145,17 +135,7 @@ public actor RemindersStore {

try eventStore.save(reminder, commit: true)

return ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
return item(from: reminder)
}

public func completeReminders(ids: [String]) async throws -> [ReminderItem] {
Expand All @@ -164,19 +144,7 @@ public actor RemindersStore {
let reminder = try reminder(withID: id)
reminder.isCompleted = true
try eventStore.save(reminder, commit: true)
updated.append(
ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
)
updated.append(item(from: reminder))
}
return updated
}
Expand Down Expand Up @@ -212,6 +180,7 @@ public actor RemindersStore {
let completionDate: Date?
let priority: Int
let dueDateComponents: DateComponents?
let isDateOnly: Bool
let listID: String
let listName: String
}
Expand All @@ -220,14 +189,17 @@ public actor RemindersStore {
let predicate = eventStore.predicateForReminders(in: calendars)
eventStore.fetchReminders(matching: predicate) { reminders in
let data = (reminders ?? []).map { reminder in
ReminderData(
let components = reminder.dueDateComponents
let dateOnly = components != nil && components?.hour == nil && components?.minute == nil
return ReminderData(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: Int(reminder.priority),
dueDateComponents: reminder.dueDateComponents,
dueDateComponents: components,
isDateOnly: dateOnly,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
Expand All @@ -245,6 +217,7 @@ public actor RemindersStore {
completionDate: data.completionDate,
priority: ReminderPriority(eventKitValue: data.priority),
dueDate: date(from: data.dueDateComponents),
isDateOnly: data.isDateOnly,
listID: data.listID,
listName: data.listName
)
Expand All @@ -266,8 +239,12 @@ public actor RemindersStore {
return calendar
}

private func calendarComponents(from date: Date) -> DateComponents {
calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
private func calendarComponents(from date: Date, dateOnly: Bool) -> DateComponents {
let components: Set<Calendar.Component> =
dateOnly
? [.year, .month, .day]
: [.year, .month, .day, .hour, .minute]
return calendar.dateComponents(components, from: date)
}

private func date(from components: DateComponents?) -> Date? {
Expand All @@ -276,14 +253,17 @@ public actor RemindersStore {
}

private func item(from reminder: EKReminder) -> ReminderItem {
ReminderItem(
let components = reminder.dueDateComponents
let dateOnly = components != nil && components?.hour == nil && components?.minute == nil
return ReminderItem(
id: reminder.calendarItemIdentifier,
title: reminder.title ?? "",
notes: reminder.notes,
isCompleted: reminder.isCompleted,
completionDate: reminder.completionDate,
priority: ReminderPriority(eventKitValue: Int(reminder.priority)),
dueDate: date(from: reminder.dueDateComponents),
dueDate: date(from: components),
isDateOnly: dateOnly,
listID: reminder.calendar.calendarIdentifier,
listName: reminder.calendar.title
)
Expand Down
11 changes: 7 additions & 4 deletions Sources/RemindCore/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
public let completionDate: Date?
public let priority: ReminderPriority
public let dueDate: Date?
public let isDateOnly: Bool
public let listID: String
public let listName: String

Expand All @@ -62,6 +63,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
completionDate: Date?,
priority: ReminderPriority,
dueDate: Date?,
isDateOnly: Bool = false,
listID: String,
listName: String
) {
Expand All @@ -72,6 +74,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
self.completionDate = completionDate
self.priority = priority
self.dueDate = dueDate
self.isDateOnly = isDateOnly
self.listID = listID
self.listName = listName
}
Expand All @@ -80,10 +83,10 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
public struct ReminderDraft: Sendable {
public let title: String
public let notes: String?
public let dueDate: Date?
public let dueDate: ParsedDate?
public let priority: ReminderPriority

public init(title: String, notes: String?, dueDate: Date?, priority: ReminderPriority) {
public init(title: String, notes: String?, dueDate: ParsedDate?, priority: ReminderPriority) {
self.title = title
self.notes = notes
self.dueDate = dueDate
Expand All @@ -94,15 +97,15 @@ public struct ReminderDraft: Sendable {
public struct ReminderUpdate: Sendable {
public let title: String?
public let notes: String?
public let dueDate: Date??
public let dueDate: ParsedDate??
public let priority: ReminderPriority?
public let listName: String?
public let isCompleted: Bool?

public init(
title: String? = nil,
notes: String? = nil,
dueDate: Date?? = nil,
dueDate: ParsedDate?? = nil,
priority: ReminderPriority? = nil,
listName: String? = nil,
isCompleted: Bool? = nil
Expand Down
4 changes: 2 additions & 2 deletions Sources/RemindCore/ReminderFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public enum ReminderFiltering {
case "all", "a":
return .all
default:
if let date = DateParsing.parseUserDate(token, now: now, calendar: calendar) {
return .date(date)
if let parsed = DateParsing.parseUserDate(token, now: now, calendar: calendar) {
return .date(parsed.date)
}
return nil
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/remindctl/CommandHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ enum CommandHelpers {
}
}

static func parseDueDate(_ value: String) throws -> Date {
guard let date = DateParsing.parseUserDate(value) else {
static func parseDueDate(_ value: String) throws -> ParsedDate {
guard let parsed = DateParsing.parseUserDate(value) else {
throw RemindCoreError.invalidDate(value)
}
return date
return parsed
}
}
2 changes: 1 addition & 1 deletion Sources/remindctl/Commands/EditCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ enum EditCommand {
let listName = values.option("list")
let notes = values.option("notes")

var dueUpdate: Date??
var dueUpdate: ParsedDate??
if let dueValue = values.option("due") {
dueUpdate = try CommandHelpers.parseDueDate(dueValue)
}
Expand Down
29 changes: 26 additions & 3 deletions Sources/remindctl/OutputFormatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ enum OutputRenderer {
static func printReminder(_ reminder: ReminderItem, format: OutputFormat) {
switch format {
case .standard:
let due = reminder.dueDate.map { DateParsing.formatDisplay($0) } ?? "no due date"
let due =
reminder.dueDate.map {
DateParsing.formatDisplay($0, isDateOnly: reminder.isDateOnly)
} ?? "no due date"
Swift.print("✓ \(reminder.title) [\(reminder.listName)] — \(due)")
case .plain:
Swift.print(plainLine(for: reminder))
Expand Down Expand Up @@ -96,7 +99,10 @@ enum OutputRenderer {
}
for (index, reminder) in sorted.enumerated() {
let status = reminder.isCompleted ? "x" : " "
let due = reminder.dueDate.map { DateParsing.formatDisplay($0) } ?? "no due date"
let due =
reminder.dueDate.map {
DateParsing.formatDisplay($0, isDateOnly: reminder.isDateOnly)
} ?? "no due date"
let priority = reminder.priority == .none ? "" : " priority=\(reminder.priority.rawValue)"
Swift.print("[\(index + 1)] [\(status)] \(reminder.title) [\(reminder.listName)] — \(due)\(priority)")
}
Expand All @@ -110,7 +116,16 @@ enum OutputRenderer {
}

private static func plainLine(for reminder: ReminderItem) -> String {
let due = reminder.dueDate.map { isoFormatter().string(from: $0) } ?? ""
let due: String
if let dueDate = reminder.dueDate {
if reminder.isDateOnly {
due = dateOnlyFormatter().string(from: dueDate)
} else {
due = isoFormatter().string(from: dueDate)
}
} else {
due = ""
}
return [
reminder.id,
reminder.listName,
Expand Down Expand Up @@ -157,4 +172,12 @@ enum OutputRenderer {
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}

private static func dateOnlyFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone.current
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}
}
Loading