I am trying to write an AppleScript that completes all reminders that match a name and list, but if they're either without a due date OR overdue (due date < today).
I know that I can loop over reminders to solve this, BUT looping over reminders is intractably slow when there are lots of reminders, so I would like to know how to do this in 1 line, which is multiple orders of magnitude faster in my use case.
When I run this script:
set curDate to current date
tell application "Reminders"
set myList to "Reminders"
set myTitle to "Test 1"
set completed of every reminder in list myList whose completed is false and name is myTitle and (due date is missing value or due date is less than curDate) to true
end tell
I get this error:
error "Reminders got an error: Can’t make missing value into type date." number -1700 from missing value to date
that highlights this line:
set completed of every reminder in list myList whose completed is false and name is myTitle and (due date is missing value or due date is less than curDate) to true
I cannot seem to get the type
of due date
and the type of missing value
to match without creating a dummy reminder... observe: the following code works:
set curDate to current date
tell application "Reminders"
set myList to "Reminders"
set myTitle to "Test 1"
-- Create a dummy reminder whose due date is "missing value"
set newremin to make new reminder
set name of newremin to "Debugging delete me"
set completed of every reminder in list myList whose completed is false and name is myTitle and (due date is (due date of newremin) or due date is less than curDate) to true
end tell
I created 2 reminders to test this, both named "Test 1". One is due today and the other has no due date. The working example with the dummy reminder succeeds in completing both reminders, and runs relatively fast.
Is there a way to modify the 1 set completed...
line to work without having to create a dummy reminder?
There was a problem with my first answer which I worked out. Neither my script nor the script in the other answer would complete a due reminder shown in the Reminders app as not completed. I think that this is due to either a reminder synch issue (between multiple devices) or due to the fact that I completed then "un-completed" the reminder, either of which causes duplicate reminders.
Regardless of the cause, in this situation, the every reminder...
statement only compares the search criteria to one (and it's not the one the Reminders app was displaying). Here's a screen recording showing that the AppleScript never loops on the currently due reminder shown in the reminders app:
Oddly, the "future-due" reminders in that recurrence do get evaluated & returned. I discovered that if I return every reminder and search through the results, I find a recurring reminder whose due date matches the incomplete reminder shown in the Reminders app, but the properties of the returned reminder shows it to be complete.
Also, the script, given whose due date is greater than curDate
only returns future-due recurring reminders if a reminder in the recurrence has been completed then "un-completed".
Another interesting/odd behavior is that if I do not include whose due date is...
, one of the incomplete reminders returned shows as due nearly a year from now and I can find no such reminder in the actual sqlite3 database.
Regardless, the only solution I have found that is both fast and robust to these reminder database inconsistencies is to obtain the reminder ID's from the sqlite3 database and loop in the AppleScript on those reminder IDs.
I have a perl script I wrote to find reminders in the sqlite3 database, though I have not published it publicly yet. However, if you write your own script, here is a screen recording proof of concept of what to have it return and how you can use that reminder ID to complete the reminder:
I will eventually publish my perl script and I will try to update this answer when I do, but I will say that my resulting AppleScript/PerlScript combo always (so far) seems to complete every intended reminder thrown at it, though I will add a caveat that if another device's sqlite3 database has a different set of IDs in it's sqlite3 database, this script cannot complete those unless it is run on that device.
In the meantime, here is the content of my AppleScript (edited for brevity), which shows the calls to my perl script:
set status to my completeReminder("Feed the cat", "Reminders")
on completeReminder(myTitle, myList)
set curDate to current date
set fcmd to "perl getReminder.pl -c incomplete -t '" & myTitle & "' -l '" & myList & "' -d future-due --ids"
set ccmd to "perl getReminder.pl -c incomplete -t '" & myTitle & "' -l '" & myList & "' -d past-or-no-due --ids"
try
set future_due to paragraphs of (do shell script fcmd)
set to_complete to paragraphs of (do shell script ccmd)
on error errmsg
return {-1, errmsg}
end try
if (count of to_complete) is equal to 0 then
return {0, "No matching reminders"}
end if
if (count of future_due) is greater than 0 then
-- Version to avoid completing future-due reminders
try
with timeout of 600 seconds
tell application "Reminders"
repeat with remid in to_complete
set rems to (every reminder whose id is remid)
repeat with rem in rems
set rem to item 1 of rems
set completed of rem to true
end repeat
end repeat
end tell
end timeout
on error errmsg
return {-1, errmsg}
end try
else
-- This may or may not actually be faster than the loop using the IDs above - I did not make that comparison, but it was much faster in my previous versions
tell application "Reminders"
set completed of every reminder in list myList whose completed is false and name is myTitle to true
end tell
end if
return {(count of to_complete), (count of to_complete) & " reminder(s) completed"}
end completeReminder
While I have not figured out a way to do this in 1 line, I did note that speed was the reason I was looking to do this in 1 line, and combined with the other answer, I have found my way to a solution that runs in 2 times the fastest speed possible that you get from using a 1-line every reminder
statement without the need to create a dummy reminder.
Even with my dummy reminder 1-line solution, I tested to discover that it was the dummy reminder creation that was taking over a minute to complete. The 1-line statement was going very fast once it had the dummy reminder to compare a "missing value due date" to any due dates of the reminders.
I also tried searching for an existing dummy reminder, but still, it ran in over a minute's time. I suspected it was the requirement of a return from the every reminder
statement that is the time-limiting bottleneck. However, with more testing, I found that I can get a quick (roughly 7-second) execution of a first reminder
statement that does a return.
I also realized that all I'm trying to do is avoid completing reminders due in the future, so I was able to combine that with a first reminder
statement to determine if there actually existed matching reminders that I do not want to complete based on the due date. The result is the following script that completes the 2 reminders (no due date and a past-due due date) in roughly 13 seconds (when there are no reminders that explicitly should not be completed). Worst case, if there are reminders to avoid completing, it runs in the time of @Robert Kniazidis's solution (between 1m30s and 2m30s) plus 7 seconds:
set curDate to current date
set myList to "Reminders"
set myTitle to "Test 1"
tell application "Reminders"
try
-- If no reminders with due dates to avoid exist, this errors out with an index error on the first line and we can complete every matching reminder regardless of due date in the catch. Otherwise, we continue on with a slow solution...
first reminder in list myList whose completed is false and name is myTitle and due date is greater than curDate
-- If there are reminders to avoid completing, do the slow roll...
try
with timeout of 600 seconds
set theReminders to reminders of list myList whose completed is false and name is myTitle
repeat with theReminder in theReminders
try
set dueDate to (due date of theReminder) as date
if dueDate is less than curDate then set completed of theReminder to true
on error
set completed of theReminder to true
end try
end repeat
end timeout
on error errmsg
return errmsg
end try
on error
-- Complete every matching reminder regardless of due date (very fast)
set completed of every reminder in list myList whose completed is false and name is myTitle to true
end try
end tell
And here's a screen recording showing that it runs in about 13s (compared to the other solution, which takes between 1m30s and 2m30s).
Note, if there's a timeout error on the slow portion of the code, the error string is returned.