Index of an Item in an AppleScript List

The “Mac Automation Scripting Guide” (Mac Automation Scripting Guide: Manipulating Lists of Items)
Listing 21-20 has an example handler for locating the position of a text string in an AppleScript list:

on getPositionOfItemInList(theItem, theList)
   repeat with a from 1 to count of theList
      if item a of theList is theItem then return a
   end repeat
   return 0
end getPositionOfItemInList

Using this handler in a test AppleScript listed below does not work:

on getPositionOfItemInList(theItem, theList)
   log (the class of theItem) & "- theItem-" & theItem
   repeat with position from 1 to count of theList
      if item position of theList is equal to theItem then return position
   end repeat
   return 0
end getPositionOfItemInList

set NAS_Names to {"Time Machine NAS", "Share Drives NAS"} -- Initialise the Disk names
set selectedNAS_List to choose from list NAS_Names with prompt "Choose NAS drive/s to wake up" with multiple selections allowed
set pass to 0
repeat with selectedNAS in selectedNAS_List
   set pass to pass + 1
   log "Pass # " & pass
   log (the class of selectedNAS) & "- the class of selectedNAS-" & selectedNAS
   log getPositionOfItemInList(selectedNAS, NAS_Names) & "- the item position in the list"
end repeat

Resulting in the following output from the logs:

(*Pass # 1*)
(*text, - the class of selectedNAS-, Time Machine NAS*)
(*text, - theItem-, Time Machine NAS*)
(*0, - the item position in the list*) <== NOTE the item is not found

However the following does work:

on getPositionOfItemInList(theItem as text, theList)
   log (the class of theItem) & "- theItem-" & theItem
   repeat with position from 1 to count of theList
      if item position of theList is equal to theItem then return position
   end repeat
   return 0
end getPositionOfItemInList

set NAS_Names to {"Time Machine NAS", "Share Drives NAS"} -- Initialise the Disk names
set selectedNAS_List to choose from list NAS_Names with prompt "Choose NAS drive/s to wake up" with multiple selections allowed
set pass to 0
repeat with selectedNAS in selectedNAS_List
   set pass to pass + 1
   log "Pass # " & pass
   log (the class of selectedNAS) & "- the class of selectedNAS-" & selectedNAS
   log getPositionOfItemInList(selectedNAS, NAS_Names) & "- the item position in the list"
end repeat

With the resulting log output:

(*Pass # 1*)
(*text, - the class of selectedNAS-, Time Machine NAS*)
(*text, - theItem-, Time Machine NAS*)
(*1, - the item position in the list*) <== NOTE the item is now found!

Also:

If the handler is “on getPositionOfItemInList(theItem, theList)” and the line in the repeat is
“on getPositionOfItemInList(theItem as text, theList)” or “if theItem contains item position of theList then return position” or “if (offset of theItem in (item position of theList)) is equal to 1 then return position”

the item in the list is found.

If the Handler is as given in the original (i.e., no “as text” etc) and in the calling script the call to the handler is “getPositionOfItemInList(selectedNAS as text, NAS_Names)” or “set selectedNAS to selectedNAS as text” is done before the call to the handler, the script works just fine.

My question is why is, what appears to be a Class of Text having to be coerced to a Class of Text for this handler to work? The “repeat with – in” appears to be extracting a Text String from the AppleScript list, so for this example to work it has to be coerced again to Text before being passed to the Handler or coerced inside the handler.

I have got this Handler to do what I’m after, but I would love to know why the need for coercion.

The reason for the odd behavior is pretty simple:

The index variable in a repeat with .. in loop – which is technically a list element, not an index – is a reference to the item rather than the item itself.

In your first example – if both items are selected – the first passed value theItem in the handler is actually item 1 of {"Time Machine NAS", "Share Drives NAS"} instead of expected "Time Machine NAS".

To fix the issue put contents of in front of selectedNAS which flattens the reference

log getPositionOfItemInList(contents of selectedNAS, NAS_Names) & "- the item position in the list"

This behavior occurs only by checking for equality, not by greater than/ less than comparison.

On the other hand the index variable i in the repeat with i from ... to ... syntax is just the pure index integer.

1 Like

Hey Andrew,

This little nugget has confused a whole lot of folks over the years.

See this section of the Applescript Language Guide:

repeat with loopVariable (in list)

What’s happening is that your loop variable selectedNAS is a reference not a value.

This is tricky, because you sometimes have to de-reference it to use it in a given context and sometimes you don’t.

To de-reference use contents of VarName.

Here’s a quick example:

set theList to {1, 2, 3}

repeat with i in theList
   set theReference to i
end repeat

# Stop and look at i

i = 3

--> false

(contents of i)  = 3

--> true

-Chris

1 Like

Thanks, StefanK.

I thought it had to be something like that, I was just bamboozled but the Class was always Text.

Never would occur to me to use “contents of”. Found a reference to it in the Control Statements Reference in the AppleScript Language Guide " To access the actual value of an item in the list, rather than a reference to the item, use the contents of property:"

Much appreciated. I have been scratching my head over this issue for several days. I can sleep easy until the next strange quirk of AppleScript raises its ugly head.

Hi ccstone,
Thanks for your reply. Nice to know that gem :+1:

Andrew

Here is a routine using text item delimiters to find the index. On larger lists it is twice as fast

on getIndexOfItemInList(theItem, theList)
	local tid, lText, c
	set tid to text item delimiters
	set text item delimiters to return
	set lText to theList as text
	set text item delimiters to tid
	set c to offset of theItem in lText
	if c > 0 then
		set c to count paragraphs of (text 1 thru c of lText)
	end if
	return c
end getIndexOfItemInList

but if you use the Script object method to speed up large lists, below is fastest by far

on getIndexOfItemInList(theItem, theList)
	script L
		property aList : theList
	end script
	repeat with a from 1 to count of L's aList
		if item a of L's aList is theItem then return a
	end repeat
	return 0
end getIndexOfItemInList

I had never run any timing tests on the different options and decided to do that. The list contained 4,098 items and the matching item was item 4,097. The results were:

repeat loop - 250 milliseconds
repeat loop with implicit script object - 5 milliseconds
repeat loop with explicit script object - 5 milliseconds
text item delimiters - 5 milliseconds

My test script:

use framework "Foundation"
use scripting additions

-- untimed code
set theList to {"a", "b", "c", "d"}
repeat 10 times
	set theList to theList & theList
end repeat
set theList to theList & {"e", "f"}

-- start time
set startTime to current application's CACurrentMediaTime()

-- timed code - repeat loop - 250 milliseconds
set theItem to "e"
set theIndex to getIndexOfItemInList(theItem, theList)
on getIndexOfItemInList(theItem, theList)
	repeat with i from 1 to (count theList)
		if item i of theList is theItem then exit repeat
	end repeat
	return i
end getIndexOfItemInList

-- timed code - repeat loop with implicit script object - 5 milliseconds
# set theItem to "e"
# repeat with i from 1 to (count theList)
# if item i of my theList is theItem then exit repeat
# end repeat
# set theIndex to i

-- timed code - repeat loop with explicit script object - 5 milliseconds
# set theItem to "e"
# set theIndex to getIndexOfItemInList(theItem, theList)
# on getIndexOfItemInList(theItem, theList)
# script L
# property aList : theList
# end script
# repeat with a from 1 to count of L's aList
# if item a of L's aList is theItem then return a
# end repeat
# return 0
# end getIndexOfItemInList

--timed code - text item delimiters - 5 milliseconds
# set theItem to "e"
# set theIndex to getIndexOfItemInList(theItem, theList)
# on getIndexOfItemInList(theItem, theList)
# local tid, lText, c
# set tid to text item delimiters
# set text item delimiters to return
# set lText to theList as text
# set text item delimiters to tid
# set c to offset of theItem in lText
# if c > 0 then
# set c to count paragraphs of (text 1 thru c of lText)
# end if
# return c
# end getIndexOfItemInList

-- elapsed time
set elapsedTime to (current application's CACurrentMediaTime()) - startTime
set numberFormatter to current application's NSNumberFormatter's new()
if elapsedTime > 1 then
	numberFormatter's setFormat:"0.000"
	set elapsedTime to ((numberFormatter's stringFromNumber:elapsedTime) as text) & " seconds"
else
	(numberFormatter's setFormat:"0")
	set elapsedTime to ((numberFormatter's stringFromNumber:(elapsedTime * 1000)) as text) & " milliseconds"
end if

-- result
elapsedTime
# count theList --> 4098
# theIndex --> 4097
2 Likes

I ran the original script, the text item delimiter version, and the script object version in Script Geek. I used a 1000 item script, and searched for the 991st string in a loop of 1000 times.

The original script came in over 26 seconds.
The text item delimiter version came in a little over 1.6 seconds.
The Script Object version came in a little over 0.8 seconds

3 Likes

On an M1 Max I was seeing about 200 milliseconds for the repeat loop and 2 milliseconds for the implicit.

Wanting to see the geometric performance loss, I set the list to repeat 14 times, got an index 65537

Repeat loop took 69 seconds (69000 milliseconds)
Implicit loop took 30 milliseconds

(!)

1 Like

There does appear to be an additional alternative that uses the top-level script object. The timing result in my test script was 5 millisecond, so it’s competitive in that respect. I’m not sure why anyone would use this but it’s interesting that it works.

use framework "Foundation"
use scripting additions

-- untimed code
set theList to {"a", "b", "c", "d"}
repeat 10 times
	set theList to theList & theList
end repeat
set theList to theList & {"e", "f"}

-- start time
set startTime to current application's CACurrentMediaTime()

-- timed code - repeat loop using top-level script object - 5 milliseconds
set theItem to "e"
set theIndex to getIndex(theItem)

on getIndex(theItem)
	repeat with i from 1 to (count its theList)
		if item i of its theList is theItem then exit repeat
	end repeat
	return i
end getIndex

-- elapsed time
set elapsedTime to (current application's CACurrentMediaTime()) - startTime
set numberFormatter to current application's NSNumberFormatter's new()
if elapsedTime > 1 then
	numberFormatter's setFormat:"0.000"
	set elapsedTime to ((numberFormatter's stringFromNumber:elapsedTime) as text) & " seconds"
else
	(numberFormatter's setFormat:"0")
	set elapsedTime to ((numberFormatter's stringFromNumber:(elapsedTime * 1000)) as text) & " milliseconds"
end if

-- result
elapsedTime
# count theList --> 4098
# theIndex --> 4097
1 Like

I ran that one in the 65537 item list on the M1 Max … 34 milliseconds
definitely a contender!

as a further stress test:
index 131073 (15 iterations of the list building loop)

repeat loop - 276.122 seconds [n.b. this causes a beach ball on script debugger at the inception of the script run]
repeat loop using top-level script object - 69 milliseconds
repeat loop with implicit script object - 72 milliseconds
repeat loop with explicit script object - 70 milliseconds
text item delimiters - 69 milliseconds

the repeat loop is truly geometric performance loss – that was 4X longer than 14 iteration (half list size).

so… this has me wanting to try NSString just to see how much faster, if at all it is.

okay… tried it with ASObjC

--timed code - NSArray index - 3 milliseconds
on indexOf:aValue inList:theList
	set theArray to current application's NSArray's arrayWithArray:theList
	set theIndex to theArray's indexOfObject:aValue
	return (theIndex + 1)
end indexOf:inList:
my indexOf:"e" inList:theList
set theIndex to the result

for the list size of 131000,
78 millisends - this appears to be the cost of passing large data on the script bridge… native indices run faster

for the 10 iteration loop size: 3 milliseconds…

figure that out – for smaller lists, it is the fastest!

1 Like

@JBManos: You should check if the value was found:

if theIndex is greater than or equal to theArray's |count|() then
	return -1
end if
return (theIndex + 1)

All of my suggestions in this thread are not documented, and I skipped the one solution that is documented. It took 15 milliseconds to run, which is competitive with the other solutions which took about 5 milliseconds in my testing.

set theList to {"a", "b", "c", "d", "e"}
set theItem to "e"
set theListReference to a reference to theList
repeat with i from 1 to (count theList)
	if item i of theListReference is theItem then exit repeat
end repeat
set theIndex to i --> 5

This is discussed in the AppleScript Language Guide here

1 Like

The variables on script debugger show it found it because the index value is one less than what the count of the list would be.