Skip to content
Closed
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
13 changes: 8 additions & 5 deletions Sources/RemindCore/IDResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ public enum IDResolver {
}
for input in inputs {
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
if let index = Int(trimmed) {
guard index > 0 && index <= numericSorted.count else {
throw RemindCoreError.invalidIdentifier(trimmed)
let numericIndex = Int(trimmed)
if let index = numericIndex {
if index > 0 && index <= numericSorted.count {
appendUnique(numericSorted[index - 1])
continue
}
appendUnique(numericSorted[index - 1])
continue
}

if trimmed.count < minimumPrefixLength {
Expand All @@ -33,6 +33,9 @@ public enum IDResolver {

let matches = sorted.filter { $0.id.lowercased().hasPrefix(trimmed.lowercased()) }
if matches.isEmpty {
if numericIndex != nil {
throw RemindCoreError.invalidIdentifier(trimmed)
}
throw RemindCoreError.reminderNotFound(trimmed)
}
if matches.count > 1 {
Expand Down
49 changes: 49 additions & 0 deletions Tests/RemindCoreTests/IDResolverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,55 @@ struct IDResolverTests {
#expect(resolved.first?.title == "First")
}

@Test("Resolve an out-of-range numeric token as an ID prefix")
func resolveNumericPrefix() throws {
let reminder = ReminderItem(
id: "45534C82-FE4E-4C30-ABE1-2243ED31ACC8",
title: "Numeric prefix",
notes: nil,
isCompleted: false,
completionDate: nil,
priority: .none,
dueDate: nil,
listID: "list1",
listName: "Work"
)

let resolved = try IDResolver.resolve(["4553"], from: [reminder])
#expect(resolved.map(\.id) == [reminder.id])
}

@Test("Keep valid numeric index precedence over an ID prefix")
func validIndexPrecedesNumericPrefix() throws {
let reminders = [
ReminderItem(
id: "0001-prefix",
title: "Prefix match",
notes: nil,
isCompleted: false,
completionDate: nil,
priority: .none,
dueDate: nil,
listID: "list1",
listName: "Work"
),
ReminderItem(
id: "other",
title: "Numeric index",
notes: nil,
isCompleted: false,
completionDate: nil,
priority: .none,
dueDate: nil,
listID: "list1",
listName: "Work"
),
]

let resolved = try IDResolver.resolve(["0001"], from: reminders, numericFrom: [reminders[1]])
#expect(resolved.map(\.id) == [reminders[1].id])
}

@Test("Reject short prefix")
func rejectShortPrefix() {
#expect(throws: RemindCoreError.invalidIdentifier("ab")) {
Expand Down
4 changes: 2 additions & 2 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ remindctl edit 4A83 --list-id 7A12
remindctl edit 4A83 --no-repeat
```

`edit`, `complete`, and `delete` accept indexes from the current default listing or ID prefixes.
Numeric indexes must be positive and within the current view; out-of-range values return an error.
Commands that target reminders accept indexes from the current default listing or ID prefixes.
An in-range numeric value always selects that index. An out-of-range numeric value falls back to ID-prefix matching and returns an error when no reminder ID matches.
`complete` and `delete` process each reminder once, even when repeated indexes, IDs, or prefixes identify the same reminder. Previews and delete confirmation counts use these unique targets in first-input order; every input must still be valid and unambiguous.
Reminders sort by due date, then title, then stable ID so equal dates and titles do not make numeric targets depend on EventKit's fetch order. Use IDs when reminders may be added, removed, or edited between commands.

Expand Down
Loading