Removing numerous ‘homepage’ entries from Contacts.app with AppleScript extremely slow
That's bold to assert AppleScript being inefficient (vague term) as a cause. Of course, it very well could be a factor, but it feels a little awkward to say that your script is inefficient, and woefully so. I don't know if that is the only reason the script runs slowly, but it's a good place to start making fixes, which I'll outline by extracting problem lines in your code:
⚠️ set myPeople to people
Redundant. There is little point in assigning a value to a variable that you don't intend to use in a meaningful way (e.g. for manipulating data without changing the source, or, if you really need to, for making scripts easier to read or debug). Nowhere else in your script do you make a reference to myPeople
, except for one other line that is also redundant. Therefore, don't waste an operation (and potentially memory, but not really in this particular case) creating a variable you don't need.
⭕️ set numPeople to (count of myPeople)
Redundant in principle (I note that you do log the value of numPeople
, though it's only used to give you an index reference, which you don't need to know; see next comment).
⚠️ repeat with i from 1 to numPeople
set myGuy to item i of myPeople
Ignoring for a moment the log call that references numPeople
, then the entire purpose of the declaring numPeople
is to allow iteration through a list by way of a counter variable (i
in your case) that is used to access each item through its index (position), i.e. item i of myPeople
. There are many instances where this would be very appropriate, but it is slower than letting AppleScript worry about how it accesses list items, which it can take off your hands using this syntax: repeat with myGuy in people
⚠️ set urlNum to count of personUrls
Redundant, for the same reason as above. As an additional note, I would personally choose to evaluate the size of a list using the length
property. This doesn't apply to nested lists of lists for which you want to include deeply-nested items in the final number, but that's not the case here.
As soon as a script has evaluated (retrieved) an object, its properties will have been retrieved as part of that evaluation. length
is a property of a list object, and its a simple, unary value (an integer
), so accessing that value will always be quick. count
is a command. It performs some undisclosed operation(s) and returns a value. I don't know what those operations are, and they will be performed at the C-language level, so probably (almost certainly) aren't slowing this script down at all. But, in principle, it's something to bear in mind as there are other situations† a command and a property seemingly do the same thing, but the property is demonstrably faster.
†Can't recall them right now.
⭕️ if urlNum > 0 then
Redundant, in principle. There is an else
clause that you might insist on keeping, but the only thing it does it to log the fact that nothing was done. If someone asked me how to intentionally slow a script down because it's just too efficient, this might be one of my answers.
⚠️ repeat with j from urlNum to 1 by -1
This is flagged for both the use of a counter variable, j
, and for its position within the if
block. If urlNum
were set to 0, the repeat loop hear would never be entered, and the script would continue executing the code that follows it. But, as will become clear, the entire repeat
block is redunant.
⚠️ log ... (the label of item j of personUrls))
delete item j of personUrls
I'm questioning the necessity of this log
command as a whole. It's certainly not as self-defeating as the one I mentioned earlier, but it does perform a current date
command call, and a lookup in the personUrls
list object.
- In situations where you do require a counter variable to iterate through a list, do as you did above, and declare a variable to which can assign the current list item's value, i.e.
set hisURL to item j of personalUrls
. In crude terms, each time you ask AppleScript for [the value of]item j of...
, it must access the list object and perform a look up, which is a relatively expensive operation to perform. Declaring a variable means the lookup is only performed once, then the value (a copy of the original) is stored in memory, for which retrieval is quick and easy in computational terms.
Returning to the value being logged, its worth seems negated by the immediate deletion of the URL data. I'm wondering if you might have just wanted a means of tracking where your script had reached in its run, which needn't be so involved. Using your counter variables, you could simply: log [j, i]
(logging their upper bounds once is sufficient, since those values don't change during a loop).
The delete
command, when viewed in the context of the repeat loop in which it is called, is going to be slowing things down a lot. You are iterating through every item in a collection in order to delete it...
⚠️ save
...then you save your changes. How would I intentionally make my script run as slowly as possible ? I would perform a save operation on the entire address book a number of times equal to numPeople * urlNum
. This value is at least 177, but it's actually multiples of this. The total number of times you need to perform the save
operation, I imagine, would be 1.
The Knock-On Effects:
Now we know that iterating over PersonUrls
was not necessary, the entire repeat
block can be replaced with the line: delete every url of myGuy whose...
.
- As something to be mindful of, any script that features nested
repeat
loops are going to be inefficient: the number of operations performed is a product of each list's size.
I actually note that you do mention attempting to delete a contact's URLs en masse, which didn't work for you, forcing you to do it iteratively. However, as you didn't supply any code showing the methods you tried to do mass-deletion, it's not possible to offer insight into why it failed for you.
Removing the repeat
block has a cumulative benefit of negating the parent if
block, irrespective of my earlier comments on it.
- Conditionals can be expensive expressions to evaluate, particularly performing 177 of them that were never needed.
The Refactored Code:
Continuing back up through the script, the preceding variable declarations all become redundant, which leads to the eventual conclusion that your entire script is functionally equivalent to:
use application "Contacts"
tell (a reference to every person)
delete (its urls where the value contains "outlook")
set its note to missing value
end tell
save
System info: AppleScript version: 2.7 System version: 10.13.6
What Now ?
Was your ultimate goal to remove all URLs containing "outlook", or did you plan on retaining one "outlook" URL for contacts that have them ?
@CJK’s answer didn’t work for me, but the response given in the Events tab of Script Editor gave me the basis of an alternative that appears to work as intended:
tell application "Contacts"
delete (every url of every person whose value contains "outlook")
set note of every person to missing value
save
end tell
Added by @CJK on 2019-04-03:
Rather than forcing AppleScript to enumerate every person
in your address book twice, which is, in general terms, a costly operation, store a reference to the collection in a variable, which you can then use repeatedly with a lot less overhead:
tell application "Contacts"
set _everyone to a reference to every person
set _homepages to a reference to _everyone's urls
delete the _homepages where the value contains "outlook"
set _everyone's note to missing value
save
end tell
System info: AppleScript version: 2.7 System version: 10.13.6
This is what is returned by the Events or Replies tab when running the original refactored code:
tell application "Contacts"
delete every url of every person whose value contains "outlook"
set note of every person to missing value
end tell
tell application "Script Editor"
save current application
end tell
I’m guessing that nothing appears to happen because (I think) changes made to the contacts only work once saved. @CJK’s new version above works as intended, however.
Now, is it as easy to whittle down a bunch of duplicate items to one as it clearly is to delete all of them?