Creating Union of List of People in Contacts

I’m trying to get a list of people in Contacts that meet certain criteria. I’m trying to find a better way than iterating through all my (6K) Contacts. Once I have the list, I want to loop over it using repeat with.

For example, I’d like a set of all the Contacts that have either:

  • a birthday, or
  • a custom date with the label “anniversary”

I think that this gives me a list of all the contacts with birthdays:
set peopleWithBirthdays to every person whose birth date is not equal to missing value

Unfortunately, I can’t figure out how to get second one, namely, how to specify that the label of any of a person’s custom dates has a particular value.

Any finally, how do I get the union of these people with either a birthday or an anniversary (or both)?

Think of whose expressions as a database query. If you have experience with SQL, its very similar to a SQL SELECT statement where you can join sub collections.

Anyway, here’s one approach:

tell application "Contacts"
	set peopleWithBirthdays to every person where (its birth date is not missing value) or (its first custom date's label is "anniversary")
end tell

This expression presumes there will only be one custom date. If there are multiple dates, then it gets more tricky.

These sorts of queries are always faster than iterating. In fact, if you then want to alter all of the people that match this expression in some way, you can use a set statement:

set property of every person where (its birth date is not missing value) or (its first custom date's label is "anniversary") to someValue

Rather than testing its first custom date's label is "anniversary", I want to test whether any custom date label contains ‘anniversary’.

Sorry, my SQL skills are pretty rudimentary. :pleading_face:

Apparently, in SQL one tests if some record exists by using the clause WHERE EXISTS(sub_query).

What is the corresponding clause in AppleScript?

Then you can move to this:

every person where (label of its custom dates contains "anniversary")

This works because the label of its custom dates sub-expression produces a list of strings (the label property value of each custom date associated with a person). The contains operator tests if the list contains an item matching “anniversary”.

Excellent!

Currently, I’m iterating through the list of people for some additional conditions that I think maybe I should add to the selection query:

  • Exclude anyone who is dead
  • Exclude anyone who doesn’t have an email address with the label of either: home, work, or school

So, putting it all together:

tell application "Contacts"
	set peopleToCheck to every person where ((its birth date is not missing value) or (label of its custom dates contains "anniversary")) and ((label of its custom dates does not contain "death") and ((label of its email contains "home") or (label of its email contains "work") or (label of its email contains "school")))
end tell

Right?

Looks good. Keep adjusting the search sub-expressions until you obtain the collection of records you are interested in. You have all the building blocks.

Something is wrong in my clause for selecting people with certain labels on any of their emails.

set peopleToCheck to every person where ((label of its first email contains strContactEmailLabelHome))

Returns a set of one person.

But,

set peopleToCheck to every person where ((label of its email contains strContactEmailLabelHome))

Returns a set that is empty. :thinking:

Make sure you pluralize email as emails. The singular term is probably colliding with a property name somewhere in the Contacts scripting interface.

Yes, I tried that already:

set peopleToCheck to every person where ((label of its emails contains strContactEmailLabelHome))

Returns an empty set.

At least it runs really fast! Actually, it seems too fast to have actually done a query. This speed got me thinking and I tried a lot of simple hacks before hitting on this one:

set peopleToCheck to every person where (label of its emails contains "email")

Returns a set of three.

Looking at the returned set, this query is matching when the entire label is exactly “email”; the query disregards contains and is matching on is.

Turns out that custom labels are in the format:
_$!<theLabel>!$_

Yes, theLabel is prefixed by _$!< and suffixed by >!$_; for some reason, contains is not disregarding the prefix and suffix before doing its comparison.

Emails without a custom label do have a label and it is Email; note the capital letter. Fortunately, I had my query surrounded by ignoring case so it matched my lowercase search criteria.

Of the three people who matched, one had an email with the label _$!<work>!$_

So, putting this all together, I tried:

set peopleToCheck to every person where (label of its email contains "_$!<work>!$_")

It runs for almost three seconds but returns an empty set. How can this be; there are hundreds—perhaps thousands—of email addresses labeled “work” in my Contacts.

I’m out of ideas.

What do you suggest trying to query people based on the custom labels of their email addresses?

UPDATE Wednesday, June 1, 2022 7:57 PM

By the way, this query also returns an empty record set:

set peopleToCheck to every person where (label of its custom dates contains "anniversary")

So the problem with custom labels is not restricted to emails.

Well, here’s the answer to my question:

Unfortunately, I don’t know how to recast the Swift solution into AppleScript.

UPDATE Wednesday, June 1, 2022 8:53 PM

Apparently, a custom label is created/modified by referencing a property,:

make new email at end of emails of thePerson with properties {label:"Work", value:"john@example.com"}

I’ve tried a lot of different approaches, but I can NOT figure out the syntax for referring to a person’s email’s property within a query. :weary:

UPDATE Wednesday, June 1, 2022 10:03 PM
Here’s an example of using CNLabeledValue in an AppleScript:

on makeEmail(label, emailAddress)
   if (emailAddress is "") then return false
   
   return |⌘|'s class "CNLabeledValue"'s labeledValueWithLabel:(label) value:(emailAddress)
end makeEmail

But I couldn’t figure out how to apply it to solve my query problem.

I know this is an Applescript forum, but sometimes I am reminded that “if the only tool you have is a hammer, everything looks like a nail.”

This problem would seem to be better solved using a Smart Folder in Contacts - then you can access that smart folder from Applescript to iterate the results?

On a related subject - I had to remove Contacts automation from my Applescript project as it was triggering the “Application Overflow” error (or something like that). I take it nobody else is having that problem?

1 Like

What a brilliant idea! And since a Smart Group can refer to other Smart Groups, I can get almost exactly the query I want:

This gives me a group that has either a birthday or anniversary either today or tomorrow:

And this reduces the group to only those who have one or more email addresses:

For today, the two together leave me with only six people who must be further inspected to remove people who either:

  • Have a Custom Date labeled “Death”
  • Have their birthday or anniversary tomorrow (as opposed to today)

Excellent!

PS: Too bad that the Smart Group option Send Email to “<theGroupName>” doesn’t work with my mailer choice of Gmail. It apparently opens this single URL, which is completely useless:

mailto:Bday+or+Ann+w/+Email

Whereas, what I need is n separate URLs, each in the form of:

mailto:<emailSmartGroupMember for person i>

I wonder how this command works if there are multiple email addresses for a Card?

PPS: Wow! It takes only .07 seconds to do the selection with my SmartGroup!