typesapplescripttypeerrorreminders

How to complete reminders with no due date (or due today) in 1 line without creating a dummy reminder?


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?


Solution

  • Updated Answer

    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:

    https://drive.google.com/open?id=1S0ToJcMK7o5wOspwOG_eIVIJ1xowA6fg&authuser=hepcat72%40gmail.com&usp=drive_fs

    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:

    https://drive.google.com/open?id=1S2WEyhUiL9CsyWcfFCvJ0CV7OqLqtAO2&authuser=hepcat72%40gmail.com&usp=drive_fs

    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
    

    Old Answer

    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).

    https://drive.google.com/open?id=187J4PV0qM9fB8ycdVoZBqjtBqAmYDBh2&authuser=hepcat72%40gmail.com&usp=drive_fs

    Note, if there's a timeout error on the slow portion of the code, the error string is returned.