Timing is everything

Most people describe Shortcuts as being like Automator, but that misses the mark. Yes, there is a resemblance to Automator because of the block-like visual way you program them and I would never deny the connection, but Shortcuts’ deeper similarity is with AppleScript. Both Shortcuts and AppleScript have significant capabilities on their own, but their real power comes from exploiting hooks into apps and the underlying operating system. And, significantly, those hooks have to be programmed into the apps by the developers. For ages, Mac users have bemoaned the lack of AppleScript support in many of our apps; in the past few years, iOS users have had the same frustrations with apps and Shortcuts.

Recently, I’ve run across another connection between AppleScript and Shortcuts: they both have filtering operations that are really easy to use but which can be unbelievably slow. In AppleScript, there’s the whose construct, which provides a very compact and English-like way of filtering lists.

tell application "ThisApp"
  set aVariable to every appClass whose property is value
end tell

The property is value part can be changed to any legal AppleScript condition:

property is less than value
property contains value
property starts with value

And so on.

I use variations on this snippet quite often, both in one-off scripts to solve a single problem and in automations that run frequently. A couple of months ago, I wrote about how slow this kind of filtering can be with Calendar events, but it isn’t confined to that app. Another slowpoke is part of my system for following up on unpaid invoices at work.

I don’t want to get into the details of my invoicing system here. Suffice it to say that I have an Invoices list with an entry for every outstanding invoice. These entries have both a due date and a recurrence relation that keeps reminding me to send followup emails to the client until the invoice is paid. Whenever I send a followup email, I click or tap the completion button in Reminders, which

  1. Marks that entry as completed.
  2. Uses the recurrence relation to create an identical entry with a due date of (typically) two weeks later.

Note that marking an entry as complete does not delete it from the Invoices list. Over time, completed entries can build up unless they’re deleted.

The automation that goes along with this system runs at 5:00 am. It figures out which active invoice reminders will be due that day and generates the followup emails that will be sent to the appropriate clients. The emails are saved as drafts and are available on all my devices through iCloud. That way, whenever I get a notification to send a followup email, the email is already written and waiting for me.

The snippet of AppleScript that figures out which invoices need a followup is this:

set tonight to (current date)
set time of tonight to 18 * 60 * 60
tell application "Reminders"
  tell list "Invoices"
    set duns to name of every reminder whose (due date < tonight) and (completed is not true)
  end tell
end tell    

The filtering, which is based on both the due date and the completed status, can take a surprisingly long time to run. With just a couple hundred entries in Invoices (all but a dozen or so of which are completed), it can take over 10 seconds to run this little snippet of AppleScript (on my Late 2012 27″ iMac).1

Now, as a practical matter, I don’t care how long this automation takes, because it runs on my office computer while I’m still at home in bed. But it is weird that a list of only a couple hundred entries takes so long to filter. And I’ve used this same filtering construct in other AppleScripts that I run while I’m sitting at the computer, and it’s always frustrating to wait for something you know shouldn’t take that long.

I learned that Shortcuts can have this same problem after listening to Episode 49 of the Automators podcast. David and Rosemary’s guest was Scotty Jackson, and one of the shortcuts he discussed needs certain contact information for people he works with. He’s using Data Jar for storing this information, which struck me as odd, because surely all that information is already in Contacts. Couldn’t he just organize his coworkers in groups and use the group names to filter them according to context?

So after listening to the episode, I decided to see if a shortcut that used Contacts would work. I have a “Work” group in Contacts, so I wrote a shortcut with just this one step:

Contacts Filter

You can’t beat the simplicity, but on my 2018 iPad Pro, with about 950 entries in Contacts and 5 people in the Work group, this takes 13–15 seconds to run. That’s way too long and explains why Scotty J violated the DRY principle and duplicated some of his Contacts information in Data Jar. The time he spent duplicating that data will be made up for in faster run times and less frustration.

As I learned a couple of months ago, the slowdown in filtering Calendar events via AppleScript has to do with the interaction between the Open Scripting Architecture and the Calendar app. I suspect that basically the same interaction bottleneck happens with Reminders, as Reminders is at least partially built on Calendar. And maybe there’s a similar structure to Shortcuts’s interaction with Contacts that makes it so slow.

Whatever it is, it’s especially annoying when you know how little time it should take. To get a sense of this, I exported my contacts as a CSV file and ran this little Python script:

#!/usr/bin/env python

import pandas as pd
df = pd.read_csv('contacts.csv')
print(df[['First name', 'Last name']][df.Group=='Work'])

It took about 0.7 seconds to run on my 2012 iMac and I bet most of that was importing the Pandas library. There’s no excuse for Apple giving us tools that run slower than something an amateur like me can whip up in a couple of minutes.

  1. You might well ask, “Why don’t you clean up your completed reminders using that script you wrote a few years ago?” All I can say is we are all sinners.