OSX Contacts, apply custom label to field using text from field value

In the Contacts app in OSX Yosemite it is currently impossible to import custom field labels when importing from a csv file. I use a 3rd party app to manage my contacts and I would like to import the custom labels that I use for postal addresses, phone numbers and email addresses for my contacts.

If I create a field that merges the custom label with field data, and separator character in between, would it be possible to use an apple script to extract the custom label, remove the separator character, and rename the field with the custom label?

For example I currently have a custom label for a phone field:
Field Custom Label: Dad Mobile
Field Value: 123 456 7890
Merged Field: Dad Mobile;123 456 7890

When importing the file into contacts via CSV I would map it to “phone other”. So the phone field for the contact would look like this when imported:

other: Dad Mobile;123 456 7890

Again, the script would extract the custom label, remove the separator character and rename the field with the custom label, resulting in:

Dad Mobile: 123 456 7890

Thanks for any and all help with this. Somewhat new to scripting.

Model: Macbook Pro Retina
AppleScript: 2.4
Browser: Safari 537.36
Operating System: Mac OS X (10.10)

Hi.

If you can get the information into Contacts correctly identified as ‘phone’, ‘email’, ‘address’, or whatever, it’s certainly possible to set custom labels where the numbers, addresses, etc. contain semicolons.

The script below contains several similar sections because different Contacts references are needed for each type of contact detail. It’s possible to telescope them together, but I think the code’s probably easier to edit and maintain as it is.


setCustomLabels()

on setCustomLabels()
	-- Get the required information from Contacts all at once for speed: the id and current value (or street) of every address, custom date, email, phone, related name, and url of every contact in the application, along with every contact's id. It's assumed that in the case of an address, any semicolon marker will be in the 'street' field.
	tell application "Contacts"
		set {addressInfo, cDateInfo, emailInfo, phoneInfo, rNameInfo, urlInfo, peopleIDs} to {{street, id} of addresses, {value, id} of custom dates, {value, id} of emails, {value, id} of phones, {value, id} of related names, {value, id} of urls, id} of people
		-- There's a problem referencing 'instant messages' with AppleScript in Contacts 9.0, so they're not included here. I don't know anything about 'social profiles'.
	end tell
	
	-- Now identify and effect any required label and value changes or deletions, starting with addresses.
	set theseChanges to getChanges(addressInfo, peopleIDs)
	repeat with thisChange in theseChanges
		set {newLabel, newValue, dataID, contactID} to thisChange
		if (newLabel > "") then
			tell application "Contacts" to tell address id dataID of person id contactID to set {its label, its street} to {newLabel, newValue}
		else
			-- Zero-length label returned for semicolon-only field value. Delete the object.
			tell application "Contacts" to delete address id dataID of person id contactID
		end if
	end repeat
	
	-- Custom dates.
	set theseChanges to getChanges(cDateInfo, peopleIDs)
	repeat with thisChange in theseChanges
		set {newLabel, newValue, dataID, contactID} to thisChange
		if (newLabel > "") then
			tell application "Contacts" to tell custom date id dataID of person id contactID to set {its label, its value} to {newLabel, newValue}
		else
			-- Simile.
			tell application "Contacts" to delete custom date id dataID of person id contactID
		end if
	end repeat
	
	-- Emails.
	set theseChanges to getChanges(emailInfo, peopleIDs)
	repeat with thisChange in theseChanges
		set {newLabel, newValue, dataID, contactID} to thisChange
		if (newLabel > "") then
			tell application "Contacts" to tell email id dataID of person id contactID to set {its label, its value} to {newLabel, newValue}
		else
			tell application "Contacts" to delete email id dataID of person id contactID
		end if
	end repeat
	
	-- Phones.
	set theseChanges to getChanges(phoneInfo, peopleIDs)
	repeat with thisChange in theseChanges
		set {newLabel, newValue, dataID, contactID} to thisChange
		if (newLabel > "") then
			tell application "Contacts" to tell phone id dataID of person id contactID to set {its label, its value} to {newLabel, newValue}
		else
			tell application "Contacts" to delete phone id dataID of person id contactID
		end if
	end repeat
	
	-- Related names.
	set theseChanges to getChanges(rNameInfo, peopleIDs)
	repeat with thisChange in theseChanges
		set {newLabel, newValue, dataID, contactID} to thisChange
		if (newLabel > "") then
			tell application "Contacts" to tell related name id dataID of person id contactID to set {its label, its value} to {newLabel, newValue}
		else
			tell application "Contacts" to delete related name id dataID of person id contactID
		end if
	end repeat
	
	-- URLs.
	set theseChanges to getChanges(urlInfo, peopleIDs)
	repeat with thisChange in theseChanges
		set {newLabel, newValue, dataID, contactID} to thisChange
		if (newLabel > "") then
			tell application "Contacts" to tell url id dataID of person id contactID to set {its label, its value} to {newLabel, newValue}
		else
			tell application "Contacts" to delete url id dataID of person id contactID
		end if
	end repeat
	
	-- Save the changes.
	tell application "Contacts" to save
end setCustomLabels

-- Find any semicolon markers in the info batch for a particular contact detail type, work out new label and value settings, and return a list of all the changes required for the batch along with the relevant IDs.
-- {{label, value, info id, contact id}, {label, value, info id, contact id}, . }
on getChanges(thisInfo, peopleIDs)
	-- thisInfo contains two lists, each of which contains as many lists as there are contacts. Each list in the first list corresponds to a contact and contains the value of every instance of the current contact detail type the contact may have. The matching lists in the second list contain the ids of the actual data objects in the application.
	-- peopleIDs contains the IDs of all the contacts in the application, the order matching that of the lists in thisInfo.
	script o
		property currentValues : beginning of thisInfo
		property dataIDs : end of thisInfo
		property contactIDs : peopleIDs
		property changes : {}
	end script
	
	set astid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to ";"
	
	-- For each contact .
	repeat with contact from 1 to (count peopleIDs)
		-- . and for each instance it may have of this type of contact detail .
		set theseValues to item contact of o's currentValues
		repeat with i from 1 to (count theseValues)
			-- . check the instance's value. If it contains a semicolon, append a list to this handler's output containing the text before the semicolon, the text after it, the ID of the object in the application, and the ID of the contact.
			set thisValue to item i of theseValues
			if (thisValue contains ";") then
				-- It's assumed there are no white spaces next to the semicolon(s). Where there's ONLY a semicolon in the field, the texts either side are zero-length.
				set end of o's changes to {text item 1 of thisValue, text item 2 of thisValue, item i of item contact of o's dataIDs, item contact of o's contactIDs}
			end if
		end repeat
	end repeat
	
	set AppleScript's text item delimiters to astid
	
	-- Return whatever changes are needed with this kind of contact detail.
	return o's changes
end getChanges

Edit: The script now deletes contact-detail objects whose values only contain a semicolon.

Nigel,

Many thanks for the solution you recommended. It works really well. There’s just one catch, some of the fields contain only the delimiter because they actually have no data for the contact item in question. The data that I’m importing is based off of a calculation field which inserts the semicolon delimiter between the two pieces of field data. If the data fields are empty the calculation field just ends up with the semicolon. Unfortunately, there’s no way to use a conditional in the 3rd party app to get around this.

Here’s an example of what I’m talking about:

Contact 1 (has email address)
Email field contains: label;value

Contact 2 (has no email address)
Email field contains: ;

Is there a way to find and clear the contents of fields that contain only the delimiter, and then run the script on the rest of the fields that contain the delimiter and the data?

When these fields with only delimiter are processed it halts the script because there is no data for it to work with. I was able to remove the fields containing only the delimiter manually on a small set of contacts and test the script”everything else seemed to work flawlessly.

Thanks again. :smiley:

Hi juskaiser.

OK. I’ve modified the script above. It now uses a different method to extract the new labels and values and this returns empty strings (“”) where the original values are just semicolons. The relabelling repeats for the individual communication media then delete the objects for which the returned labels are “”.

Nigel, again, thanks for the help here. The problem I’m running into now is a performance issue. This script is able to run fine if I have only 30 records in contacts, it stalls and sometimes goes through to completion with 50 records and is unable to process 100 or more records.

I have a MBP with 2.3Ghz i7 with 16 GB of ram, so it is a fairly robust laptop.

On previous test, I was working with a smaller subset of records so I wasn’t seeing this issue come up. Is there anything that can be done?

Hi juskaiser.

I’m not able to test with that many records, but I’d guess the problem’s actually Contacts itself not being able to digest all the changes at once. One thing you could try ” although I don’t know for certain that it would help ” would be to put this code .

-- Save the changes.
¨tell application "Contacts" to save

. after all six of the repeat with thisChange in theseChanges repeats instead of just after the last one as I have it above. The hope is that this will allow Contacts to consolidate the changes from each repeat before going on to the next batch. It’s worth a try, anyway.