Sort a contact card's emails display order with AppleScript?

Hi,
I’ve been teaching myself AppleScript by trying to manipulate contacts data. Below is my first ever script (I have some vba and old C++ background).

I have a few questions:

  1. This works fine for a small selection of contacts, but seems to crash after a while for about 2000 contacts. I run it from the editor. Any ideas to make it more stable?
  2. How can I create a contact group from inside a tell person / end tell statement? (cf “Unknown labels”)
  3. I’d like to recyle this code to do the same thing with phone numbers, dates, and relationship fields (I use those to store string data, like category, where we met, referred by, …) I don’t know how to create some sort of call function like sort_fields(field_type, label 1, label 2, label 3, …, label n) any ideas?

Thanks!

tell application "Contacts"
 
-- set thePeople to selection
set thePeople to people
repeat with aPerson in thePeople
tell aPerson
 
set work_labels to {}
set home_labels to {}
set other_labels to {}
set unknown_labels to {}
 
set home_values to {}
set work_values to {}
set other_values to {}
set unknown_values to {}
 
-- Sort emails into lists based on labels
repeat with anEmail in emails
tell anEmail
set theLabel to label
if theLabel is "work" then
set work_labels to work_labels & "work"
set work_values to work_values & value
else if theLabel is "home" then
set home_labels to home_labels & "home"
set home_values to home_values & value
else if theLabel is "other" then
set other_labels to other_labels & "other"
set other_values to other_values & value
else
set unknown_labels to unknown_labels & theLabel
set unknown_values to unknown_values & value
end if
end tell
end repeat
 
-- if unknown_labels is not equal to {} then
-- add aPerson to group "Unknown labels" of application "Contacts"
-- end if
 
-- This part will place "work" emails first, then "home, ... etc.
set all_labels to work_labels & home_labels & other_labels & unknown_labels
set all_values to work_values & home_values & other_values & unknown_values
 
set i to 1
repeat with anEmail in emails
set label of anEmail to item i of all_labels
set value of anEmail to item i of all_values
set i to i + 1
end repeat
 
save
 
end tell
end repeat
 
end tell

Hello.

Applescript can’t reference more than 2^14 which is 16384 elements I gueess. When you are accessing 2000 records I guess you implicitly pass that boundary.

You may try to increase the boundary, or make more room if you like, by adding property parent : AppleScript at the top of your script.

In the end, when you meet the wall, you’ll have to filter out subsets of your records, as AppleScript adheres to the boundary mentioned earlier. (As long as you want to use AppleScript, I think may be able to access address book records by sqlLite.)

Hi. Welcome to MacScripter.

You don’t say what you mean by “crash”. (Spinning beachball? Error message? Smell of burning?) But one of the problems when sending a lot of commands to an application ” as when individually scripting every email of every person in Contacts ” is the sheer amount of event passing which takes place. For each command, the script broadcasts a command event; the application recognises the event as intended for it, carries out the command, and sends an acknowledgement when it’s done; the script then goes on to the next command.

On top of this, the application has no idea of the purpose or progress of the script. It just receives individual commands out of the blue ” say, to do something with the label of email x of person y. It then internally looks up person y, person y’s email x, and the email’s label property, and carries out the action. Once it’s done that and sent an acknowledgement, it’s finished. If the very next command is for the very same property of the same email of the same person, the internal look-up happens all over again. (There are probably technical inaccuracies in this description. I’m just giving a way of thinking about it which pays off when scripting.)

There’s not much you can do about this in your script when it comes to setting the email property values, but you can get the original values in bulk. This requires only one command (possibly two events) and the application moves internally between the emails instead of starting again every time. So your sorting loop for each person could be:


-- Sort emails into lists based on labels
-- First get the values of all this person's email labels and values in one go.
set {email_labels, email_values} to {label, value} of emails
-- Then do the sorting without talking to the application.
repeat with i from 1 to (count email_labels)
	set theLabel to item i of email_labels
	if theLabel is "work" then
		set work_labels to work_labels & "work"
		set work_values to work_values & item i of email_values
	else if theLabel is "home" then
		set home_labels to home_labels & "home"
		set home_values to home_values & item i of email_values
	else if theLabel is "other" then
		set other_labels to other_labels & "other"
		set other_values to other_values & item i of email_values
	else
		set unknown_labels to unknown_labels & theLabel
		set unknown_values to unknown_values & item i of email_values
	end if
end repeat

Even better would be to get the label and value values for every email of every person at once, although the vanilla parsing code would have to be slightly more complex.

This approach also goes a little way towards answering your third question. For instance, the repeat loop in the version of the code above can easily be adapted and put into its own handler. You pass the handler a list of label values and a list of value values and it returns eight sorted lists or, preferably, the two resulting from the concatentations of the two groups of four.

While on the subject of concatenation, when appending single items to lists, it’s more efficient to append the items to the existing lists than to create new lists by concatentation:


if theLabel is "work" then
	set end of work_labels to "work"
	set end of work_values to item i of email_values
else if theLabel is "home" then
		set end of home_labels to "home"
		set end of home_values to item i of email_values
-- etc.

I haven’t actually run this code, but it looks OK:

tell application "Contacts"
	
	-- set thePeople to selection
	set thePeople to people
	repeat with aPerson in thePeople
		tell aPerson
			
			-- Get the values of all this person's email labels and values in one go.
			set {email_labels, email_values} to {label, value} of emails
			
			-- Sort labels and values into lists based on labels
			set {all_labels, all_values} to my groupByLabel(email_labels, email_values) -- 'my' because the handler call's in a 'tell' statement.
			
			repeat with i from 1 to (count all_labels)
				set anEmail to email i
				set label of anEmail to item i of all_labels
				set value of anEmail to item i of all_values
			end repeat
			
			save
			
		end tell
	end repeat
end tell

on groupByLabel(label_list, value_list)
	set work_labels to {}
	set home_labels to {}
	set other_labels to {}
	set unknown_labels to {}
	
	set home_values to {}
	set work_values to {}
	set other_values to {}
	set unknown_values to {}
	
	repeat with i from 1 to (count label_list)
		set theLabel to item i of label_list
		set theValue to item i of value_list
		if theLabel is "work" then
			set end of work_labels to "work"
			set end of work_values to theValue
		else if theLabel is "home" then
			set end of home_labels to "home"
			set end of home_values to theValue
		else if theLabel is "other" then
			set end of other_labels to "other"
			set end of other_values to theValue
		else
			set end of unknown_labels to theLabel
			set end of unknown_values to theValue
		end if
	end repeat
	
	-- This part will place "work" emails first, then "home, ... etc.
	set all_labels to work_labels & home_labels & other_labels & unknown_labels
	set all_values to work_values & home_values & other_values & unknown_values
	
	return {all_labels, all_values}
end groupByLabel

Thanks for all the tips!

I did read a forum post afterwards about appending to an existing list:
http://macscripter.net/viewtopic.php?id=12562
Good to know.

By crash I meant the spinning beach ball for hours, literally, when running from the AppleScript Editor. However, if I compile the script and run it as an application, then it runs fine in about 2 minutes. The editor probably gets overloaded with all the calls to the Contacts app.

In any case I still need to read up on the language and scripting best practices… thanks again.

Hello.

It is not only concentrating the number of events into one or two that makes up for the huge saving of time one can experience when using “bulk operations”.

The other thing here, is that every property we can see from a script is key value observed (bound). So, at least any property we write, and probably also those we fetch will induce some overhead of key value observing on a per property basis. Whereas good written software, will only issue one key-value operation for a block operation.

This will reduce a great deal of overhead and time when scripting.

I just wrote this, since it explains a lot about the time differences between singular versus block operations.