AppleSloth
March 14, 2020 at 3:33 PM by Dr. Drang
At the end of this post from a few days ago, I thought about changing my Southwest calendar fixer system from a Python script that operates on .ics
files before they get imported into Calendar to an AppleScript or JavaScript for Automation (JXA) that operates directly on Calendar events after they’ve been imported:
I’m beginning to think
swicsfix
on the Mac could be replaced with a fairly short JXA script (or AppleScript) that follows the logic of SWA Calendar Fix.
Translating the Shortcuts logic into AppleScript was fairly easy, but the solution turned out to be unacceptable because AppleScript (and JXA) are unbearable slow at the necessary operations.
Here’s a simple AppleScript that collects all the future calendar events that match the default format that comes from Southwest. This is basically what the first step of what my shortcut does.
applescript:
1: tell application "Calendar"
2: tell calendar "home"
3: set swHomeEvents to every event whose ¬
4: start date is greater than or equal to (current date) ¬
5: and ¬
6: summary starts with "Southwest Airlines Confirmation"
7: end tell
8:
9: tell calendar "work"
10: set swWorkEvents to every event whose ¬
11: start date is greater than or equal to (current date) ¬
12: and ¬
13: summary starts with "Southwest Airlines Confirmation"
14: end tell
15:
16: set swEvents to swHomeEvents & swWorkEvents
17:
18: end tell
I have to look at two calendars, “home” and “work,” because my reservation could be for vacation or business travel, and I want to make sure the script will fix either type. If I were going to continue down this development path, I’d get rid of the repeated code by having it loop through the two calendars; but as we’ll see, that’s not going help.
When I first tested this code, I thought I’d done something horribly wrong, because it just kept running and running. After long while, though, it came to a stop and returned the two events that it was supposed to.
How long is a “long while”? Well, that depends on how the code was run:
- In Script Editor, it finished in about 61 seconds, as timed by me with a stopwatch.
- In Script Debugger, it finished in 112 seconds, as recorded by Script Debugger’s own timer.
In the Terminal, executed via
time osascript sw-calendar-as.scpt
it finished in 85 seconds, as reported by
time
.- When saved as an app and run via double-clicking, it finished in 85 seconds, just like running it from
osascript
.
All of these runs were performed on my 2012 iMac at home. Based on some other tests I’ve run, I think the run times on my 2017 iMac at work would be about 40% of these. In contrast:
- The Python script I was hoping to replace is essentially instantaneous on both iMacs.
- The Shortcut described in the post a few days ago takes less than two seconds on my oldest iOS device, a 2016 9.7″ iPad Pro.
Note that this AppleScript hasn’t actually done anything to these calendar events, it’s just collected the ones that need fixing. Even without making changes,1 it is unacceptably (and almost unimaginably) slow.
This is not the fault of AppleScript per se. The equivalent JXA script,
javascript:
1: var now = new(Date)
2:
3: var Calendar = Application("Calendar")
4: var homeCal = Calendar.calendars.whose({name: "home"})[0]
5: var workCal = Calendar.calendars.whose({name: "work"})[0]
6:
7: var swHomeEvents = homeCal.events.whose({
8: _and: [
9: {startDate: {_greaterThan: now}},
10: {summary: {_beginsWith: "Southwest Airlines Confirmation"}}
11: ]
12: })
13: var swWorkEvents = workCal.events.whose({
14: _and: [
15: {startDate: {_greaterThan: now}},
16: {summary: {_beginsWith: "Southwest Airlines Confirmation"}}
17: ]
18: })
19:
20: var swEvents = swHomeEvents().concat(swWorkEvents())
21: swEvents
takes just as long.2
The problem is with the underlying Open Scripting Architecture (OSA) and its interaction with Calendar. These ungodly run times are, I have learned over the past couple of days, a well-known problem when scripting Calendar. As I see it, I have three options:
Instead of putting my flight events into the “home” and “work” calendars, which have about 3,000 events between them, start a new calendar called “flights” that holds only these types of event. A script written to edit events in this new calendar will run quickly because there are few events to collect and filter through.
I tested this out by creating a fake “flights” calendar with 100 events in it (I’ll be long retired before I get to 100 flights). The above scripts ran in a reasonable 2–3 seconds.
What I don’t like about this is that I’ve been categorizing my travel according to its purpose for years, and I really don’t want to change just to accommodate Apple’s poorly written code.
Install and learn how to use Shane Stanley’s CalendarLib and BridgePlus AppleScript libraries, which were specifically written to get around the Calendar bottleneck. What I don’t like about this solution is that these libraries can be made ineffective at any time by changes from Apple. BridgePlus is already dicey. Here’s the caveat at the top of its description:
Important: Scripts using BridgePlus cannot be edited in Script Editor in Mojave and later because of new security settings. You need to use Script Debugger. Also, if you are running Catalina, you may receive a message saying the library is damaged, when it is not. This is the result of new Gatekeeper checks. Unfortunately there is no equivalent to control-click-and-open for script libraries, which resolves this issue for applications. If you face this issue, you will need to remove the quarantine attributes of the library before you can use it. A longer-term solution is still in development.
- Forget the whole thing and stick with my tried and true Python solution. This is what I’m doing.
I don’t regret the time I spent fiddling with this. I knew early on that the AppleScript/JXA solution wouldn’t be acceptable, but I kept experimenting to learn new things. For example:
- I had no idea run times could vary so much based on how the script was invoked.
- I learned that JXA definitions like that
swHomeEvents
andswWorkEvents
return functions instead of arrays. This is different from AppleScript, where similar definitions return lists. In JXA, you have to run the function to get the array, which is why you seeswHomeEvents()
andswWorkEvents()
in Line 20 of the JXA script. According to the JXA release notes that came out with Yosemite, running the function in JavaScript is like usingget
in AppleScript. - I learned that the JXA equivalent to AppleScript’s
starts with
construction is_beginsWith
, not_startsWith
. Is this because JavaScript already has astartsWith
method? Damned if I know, but it’s an important exception to the usual convention for translating AppleScript terms. This is also in Yosemite’s JXA release notes.
-
Which I doubt would add much to the run time. ↩
-
In this post, I’m going to ignore the awful JXA syntax Apple has inflicted on us and focus on the run times. I may come back to the syntax in a later post. Suffice it to say that people who think JXA will save us from the horrible syntax of AppleScript haven’t spent enough time with JXA. ↩