Simplified key-value access of associative AppleScript lists

(Note: My apologies if the solution presented below has been described previously. If so, I didn’t find it with a MacScripter search.)

In contrast with array searching in Cocoa, AppleScript does not offer a simple command for searching a list and retrieving the index of a matching item. Specifically for the purposes of the current discussion, this limitation extends to an inability to search a list of text string keys for a matching key. Common ways to get around this missing functionality include querying the key list in a repeat loop until the matching key is found (often in a separate handler if this task must be performed repeatedly), or converting the list to a Cocoa array and using any of several array methods to retrieve the index of a matching key. The current post describes a vanilla AppleScript solution that requires only one or two lines of code to find the index of a matching key. The first form of the technique to be described is a more rigorous version that can handle the special case of a key list containing a key consisting of the empty string; it’s drawback is that it is a bit more cumbersome to implement. A second, simplified form is then described that is applicable to all cases that the first form can handle with the exception of a key list with an empty string key. Since it is easier to implement, it is the recommended form whenever feasible.

With either approach, one requirement is that all key list keys must be text strings. Another constraint is that if multiple identical keys are present in the key list, the index of only the first matching key will be retrieved.

For illustrative purposes, consider the following list of color names serving as the key list, and a corresponding associated list of color descriptions that are to be retrieved by key value (i.e., by color name):


set colorNames to {"red", "green", "blue-green", "", "blue"}
set colorDescriptions to {"This is the color red.", "This is the color green.", "This is the color blue-green.", "This is no color.", "This is the color blue."}

To find the index of a matching key, perform the following steps:

 (1) Calculate the number of characters in the longest key, add 2 to that value, and call the resulting value nReps. In the above example, "blue-green" with 10 characters is the longest key, and thus:
      nReps = 10 + 2 = 12

 (2) Choose two separate special characters that do not appear in any of the keys. We'll call them special-character-1 and special-character-2. Examples include opening and closing parentheses "(" and ")", square brackets "[" and "]", angled brackets "<" and ">", or, more obscurely, double angle quotation marks "«" and "»" (Unicode code points = 171 and 187, keyboard shortcuts = Option-\ and Shift-Option-\), or angle quotation marks "‹" and "›" (Unicode code points = 8249 and 8250, keyboard shortcuts = Shift-Option-3 and Shift-Option-4). Any characters that do not appear in any of the keys will do.

 (3) Create a pattern string that starts with nReps repetitions of special-character-1 (without any special-character-2's.) For the current example, I will use left and right angled brackets "<" and ">" as the special characters; therefore, the pattern string will start with 12 repetitions of the left angled bracket character. Then, for each key in the order it appears in the key list, append to the end of the pattern string: one special-character-1 + the key + a sufficient number of repetitions of special-character-2 such that the entire substring length = nReps. For instance, for the "red" key in the current example, the appended substring would consist of 1 left angled bracket & "red" & 8 right angled brackets for a total substring length = 12. The final pattern string would thus be:
      pattern string = "<<<<<<<<<<<<" & "<red>>>>>>>>" & "<green>>>>>>" & "<blue-green>" & "<>>>>>>>>>>>" & "<blue>>>>>>>"
      -or equivalently-
      pattern string = "<<<<<<<<<<<<<red>>>>>>>><green>>>>>><blue-green><>>>>>>>>>>><blue>>>>>>>"

 (4) The index of a key in the key list may then be retrieved with the following simple command:
      matching index = (offset of <special-character-1><key value><special-character-2> in <pattern string>) div nReps

Here are examples of key-value retrievals for the color lists:


set colorNames to {"red", "green", "blue-green", "", "blue"} -- the key list
set colorDescriptions to {"This is the color red.", "This is the color green.", "This is the color blue-green.", "This is no color.", "This is the color blue."} -- a list of associated values

set {patternString, nReps} to {"<<<<<<<<<<<<<red>>>>>>>><green>>>>>><blue-green><>>>>>>>>>>><blue>>>>>>>", 12}

-- For the green color description:
set ix to (offset of "<green>" in patternString) div nReps --> 2
colorDescriptions's item ix --> "This is the color green."
-- For the blue-green color description:
set ix to (offset of "<blue-green>" in patternString) div nReps --> 3
colorDescriptions's item ix --> "This is the color blue-green."
-- For the no-color description:
set ix to (offset of "<>" in patternString) div nReps --> 4
colorDescriptions's item ix --> "This is no color."

-- Or for one-line key-value retrievals:
colorDescriptions's item ((offset of "<green>" in patternString) div nReps) --> "This is the color green."
colorDescriptions's item ((offset of "<blue-green>" in patternString) div nReps) --> "This is the color blue-green."
colorDescriptions's item ((offset of "<>" in patternString) div nReps) --> "This is no color."


Having described the more rigorous form of the technique, the following is a simpler version that is easier to implement and is recommended in most situations. It adds only one new constraint, namely that no key may consist of the empty text string. With that single exception, it is equally as applicable as the more rigorous version.

Here are the same color lists but with the empty-string key removed:


set colorNames to {"red", "green", "blue-green", "blue"}
set colorDescriptions to {"This is the color red.", "This is the color green.", "This is the color blue-green.", "This is the color blue."}

Perform the following steps to finding the index of a matching key:

(1) Calculate the number of characters in the longest key, add 2 to that value, and call the resulting value nReps. In the above example, "blue-green" with 10 characters is the longest key, and thus:
      nReps = 10 + 2 = 12

 (2) Choose a special character that does not appear in any of the keys. When feasible, I prefer to use the middle dot character "·" (Unicode code point = 183, keyboard shortcut = Shift-Option-9); alternatively, the space character may be used provided that no key contains a space. Any character that does not appear in any of the keys will suffice.

 (3) Create a pattern string that starts with nReps repetitions of the special character. For the current example, with the middle dot character "·" as the special character, the pattern string will start with 12 repetitions of the middle dot character. Then, for each key in the order it appears in the key list, append to the end of the pattern string: one special character + the key + a sufficient number of repetitions of the special character such that the entire substring length = nReps. For instance, for the "red" key in the current example, the appended substring would consist of 1 middle dot character & "red" & 8 middle dot characters for a total substring length = 12. The final pattern string would thus be:
      pattern string = "············" & "·red········" & "·green······" & "·blue-green·" & "·blue·······"
      -or equivalently-
      pattern string = "·············red·········green·······blue-green··blue·······"

 (4) The index of a key in the key list may then be retrieved with the following simple command:
      matching index = (offset of <special character><key value><special character> in <pattern string>) div nReps

Here are examples of key-value retrievals for the color lists:


set colorNames to {"red", "green", "blue-green", "blue"} -- the key list
set colorDescriptions to {"This is the color red.", "This is the color green.", "This is the color blue-green.", "This is the color blue."} -- a list of associated values

set {patternString, nReps} to {"·············red·········green·······blue-green··blue·······", 12}

-- For the green color description:
set ix to (offset of "·green·" in patternString) div nReps --> 2
colorDescriptions's item ix --> "This is the color green."
-- For the blue-green color description:
set ix to (offset of "·blue-green·" in patternString) div nReps --> 3
colorDescriptions's item ix --> "This is the color blue-green."
-- For the red color description:
set ix to (offset of "·red·" in patternString) div nReps --> 1
colorDescriptions's item ix --> "This is the color red."

-- Or for one-line key-value retrievals:
colorDescriptions's item ((offset of "·green·" in patternString) div nReps) --> "This is the color green."
colorDescriptions's item ((offset of "·blue-green·" in patternString) div nReps) --> "This is the color blue-green."
colorDescriptions's item ((offset of "·red·" in patternString) div nReps) --> "This is the color red."


There are certainly more advanced techniques for keyed access to associative data, notably by structuring the data as a Cocoa array of dictionaries that is amenable to key-value searching. Still, I find myself using the above code-sparing technique in simple situations where the following conditions are met: (1) the data is structured as a group of associative lists, one of which is the key list, (3) there is no other reason to convert the lists to Cocoa arrays, (3) a pre-existing search handler is not readily available, and (4) the key list (and associated value lists) has a small enough number of items that it is easy to manually code the pattern string.