Manipulating a list of lists

Is there some way to accomplish the following without the repeat loop? Thanks for the help.

use framework "Foundation"
use scripting additions

set theListOfLists to {{"a", 10}, {"b", 20}, {"c", 30}}

set theListOfLetters to {}
repeat with aList in theListOfLists
	set end of theListOfLetters to item 1 of aList
end repeat

set theLetter to text returned of (display dialog "Enter a letter" default answer "a, b, or c")

set theArray to current application's NSArray's arrayWithArray:theListOfLetters
set i to ((theArray's indexOfObject:theLetter) + 1)
set theNumber to item 2 of (item i of theListOfLists)

I don’t believe there’s any way around a repeat loop (ignoring third-party frameworks).

Thanks Shane–I won’t spend any more time looking at that option.

I ran some timing tests with a repeat loop using core AppleScript (enhanced with an implicit script object) with 3,000 items in the list of lists, and it only took 3 milliseconds. My approach from post 1 took over ten times longer.

repeat with i from 1 to (count my theListOfLists)
	if item 1 of (item i of my theListOfLists) = 3000 then
		set theCharacter to item 2 of (item i of my theListOfLists)
		exit repeat
	end if
end repeat

Hi peavine.

An array filter would do the job, I think.

use framework "Foundation"
use scripting additions

set theListOfLists to {{"a", 10}, {"b", 20}, {"c", 30}}
set theArrayofArrays to current application's NSArray's arrayWithArray:theListOfLists

set theLetter to text returned of (display dialog "Enter a letter" default answer "a, b, or c")

set filter to current application's NSPredicate's predicateWithFormat_("self[FIRST] == %@", theLetter) -- or "self[0] == %@"
set theNumber to ((theArrayofArrays's filteredArrayUsingPredicate:(filter)) as list)'s end's end

(* -- Or if there's a chance the user may enter a non-existent letter, change the last line to this:
set filteredList to (theArrayofArrays's filteredArrayUsingPredicate:(filter)) as list
if (filteredList is {}) then error "No sublists begin with '" & theLetter & "'"
set theNumber to filteredList's end's end
*)

Nice, Nigel :cool:

Thanks Nigel. That works great.

BTW, I was a bit stumped at first by the code “end’s end” but came to realize that it was equivalent to item -1 of item -1. Also, the ability to specify an item number after “self” will be quite useful in other situations when working with lists of lists.

Yeah. It’s a shame we can’t use negative indices too, but there is at least the expression “LAST”, which of course is the opposite of the “FIRST” in my script above. The index number itself can be passed as a parameter if the occasion demands:

set filter to current application's NSPredicate's predicateWithFormat_("self[%@] == %@", 0, theLetter)

Thanks Nigel. Just for myself and others new to ASObjC, I thought I’d write a simple script utilizing the stuff discussed in this thread. I made the script work without regard to case, although this can be changed by removing [c].

use framework "Foundation"
use scripting additions

set theList to {{"John", "Doe", "New York"}, {"John", "Smith", "Chicago"}, {"Jane", "Doe", "Los Angeles"}}

display dialog "Enter a first or last name." default answer "Doe" buttons {"Cancel", "First Name", "Last Name"} default button 3
set {findOption, findText} to {button returned, text returned} of result

if findOption = "First Name" then
	set findOption to 0
else
	set findOption to 1
end if

set theArray to current application's NSArray's arrayWithArray:theList
set thePredicate to current application's NSPredicate's predicateWithFormat_("self[%@] ==[c] %@", findOption, findText)
set matchingPersons to (theArray's filteredArrayUsingPredicate:(thePredicate)) as list

I’m just finishing my work researching ASObjC and list of lists and had one final question, which I haven’t been able to resolve. The following returns a list of every item of every list in a list of lists:

use framework "Foundation"

set theList to {{"a", 1}, {"b", 2}, {"c", 3}}
set theArray to current application's class "NSArray"'s arrayWithArray:theList
(theArray's valueForKeyPath:"@unionOfArrays.self") as list --> {"a", 1, "b", 2, "c", 3}

What I’d like it to do is return a list of the first item of every list in the list of lists: {“a”, “b”, “c”}. I spent a lot of time on this but just couldn’t get it to work. Thanks.

I don’t think that is possible using valueForKey: or valueForKeyPath:. In the current case, you could finish with a bit more AS:

use framework "Foundation"

set theList to {{"a", 1}, {"b", 2}, {"c", 3}}
set theArray to current application's class "NSArray"'s arrayWithArray:theList
((theArray's valueForKeyPath:"@unionOfArrays.self") as list)'s text --> {"a", "b", "c"}

Thanks Nigel. I kept trying different stuff like adding firstObject after “self” but nothing worked. I won’t spend any more time on this.

Peavine,

Using plain AppleScript’s flattening handler you can perform this task 7 times faster than using ASObjC. I compared following script with Nigel’s AsObjC script. Plain AppleScript will work with any type items and will return result as is, and not as text, as well:


set bigListOfLists to {}
repeat with i from 1 to 3000
	if i < 3000 then
		set theCharacter to "a"
	else
		set theCharacter to "b"
	end if
	set the end of bigListOfLists to {i, theCharacter}
end repeat

set theFirstItems to flatten(bigListOfLists) of me

on flatten(l)
	script o
		property fl : {}
		on flttn(l)
			script p
				property lol : l
			end script
			repeat with i from 1 to (count l)
				set v to item 1 of item i of p's lol -- THIS: retrieves every first item
				if (v's class is list) then
					flttn(v)
				else
					set end of my fl to v
				end if
			end repeat
		end flttn
	end script
	tell o
		flttn(l)
		return its fl
	end tell
end flatten

NOTE: regarding the first question of this topic: to flatten all items of list of lists, and not only first ones, you should set v to item i of p’s lol


set littleListOfLists to {{"a", 1}, {file, 2}, {list, 3}}

set theFirstItems to flatten(littleListOfLists) of me

on flatten(l)
	script o
		property fl : {}
		on flttn(l)
			script p
				property lol : l
			end script
			repeat with i from 1 to (count l)
				set v to item i of p's lol -- THIS
				if (v's class is list) then
					flttn(v)
				else
					set end of my fl to v
				end if
			end repeat
		end flttn
	end script
	tell o
		flttn(l)
		return its fl
	end tell
end flatten

Thanks KniazidisR. I always like to look at new and faster ways to do stuff.

I ran timing tests on Nigel’s suggestion (edited to work with the modified list of lists), your suggestion, and a script-object-enhanced repeat loop, and the results were:

Nigel’s suggestion - 0.059 second
KniazidisR’s suggestion - 0.018 second
Enhanced repeat loop - 0.008 second

Just as a point of information, Nigel’s suggestion was in response to my request for an ASObjC solution and wasn’t presented as being particularly fast.

My test script (comment-out scripts not being tested):

use framework "Foundation"
use scripting additions

-- untimed code
set bigListOfLists to {}
repeat with i from 1 to 3000
	if i < 3000 then
		set theCharacter to "a"
	else
		set theCharacter to "b"
	end if
	set the end of bigListOfLists to {i, theCharacter}
end repeat

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

-- timed code --> 0.059 second
set theArray to current application's class "NSArray"'s arrayWithArray:bigListOfLists
set theFirstItems to ((theArray's valueForKeyPath:"@unionOfArrays.self") as list)'s integers

-- timed code --> 0.018 second
set theFirstItems to flatten(bigListOfLists) of me
on flatten(l)
	script o
		property fl : {}
		on flttn(l)
			script p
				property lol : l
			end script
			repeat with i from 1 to (count l)
				set v to item 1 of item i of p's lol -- THIS: retrieves every first item
				if (v's class is list) then
					flttn(v)
				else
					set end of my fl to v
				end if
			end repeat
		end flttn
	end script
	tell o
		flttn(l)
		return its fl
	end tell
end flatten

-- timed code --> 0.008 second
set theFirstItems to my {}
repeat with i from 1 to (count my bigListOfLists)
	set end of my theFirstItems to item 1 of (item i of my bigListOfLists)
end repeat

-- elapsed time
set elapsedTime to (current application's CFAbsoluteTimeGetCurrent()) - startTime
set nf to current application's NSNumberFormatter's new()
nf's setFormat:("0.000")
set elapsedTime to ((nf's stringFromNumber:elapsedTime) as text) & " seconds"

-- result
elapsedTime

Peavine,

I myself love simple and fast scripts. This time I did not notice that your original list is structured as a special case (2 levels) of nested lists. And flattening handler in my examples is designed to flatten a list of any nesting level. Therefore, in this case, you correctly used its simplified version. :slight_smile:

I tried to fix some things of your script to get it faster (using my specifier to create implicit script objects for theArray and theList) and without hardcoding of array’s indexes. I provide it here:


use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

set bigListOfLists to {}
repeat with i from 1 to 3000
	if i < 3000 then
		set theCharacter to "a"
	else
		set theCharacter to "b"
	end if
	set the end of bigListOfLists to {i, theCharacter}
end repeat

set theArray to ((current application's class "NSArray"'s arrayWithArray:bigListOfLists)'s valueForKeyPath:"@unionOfArrays.self") as list
set firstItems to my {}
repeat with i from 1 to (count theArray) - 1 by 2
	set end of my firstItems to item i of my theArray
end repeat
return firstItems

Times I tested was as follows:

Nigel Garvey - 0.375 seconds
Fredrik71 ----- 0.308 seconds
KniazidisR ---- 0.058 seconds
Peavine ------- 0.031 seconds

Congratulations, Fredrik71,

You managed to create the slowest script (33.823 seconds) in this topic. :frowning: Because not only are you not bypassing the repeat loop, but you are adding new time-consuming instructions:


use framework "Foundation"

set bigListOfLists to {}
repeat with i from 1 to 3000
	if i < 3000 then
		set theCharacter to "a"
	else
		set theCharacter to "b"
	end if
	set the end of bigListOfLists to {i, theCharacter}
end repeat

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

set theArray to (current application's class "NSArray"'s arrayWithArray:bigListOfLists)'s valueForKeyPath:"@unionOfArrays.self"
set theIndex to current application's NSMutableIndexSet's alloc()'s init()
repeat with i from 0 to ((theArray's |count|()) - 1) by 2
	(theIndex's addIndex:i)
end repeat
(theArray's objectsAtIndexes:theIndex) as list

-- elapsed time
set elapsedTime to (current application's CFAbsoluteTimeGetCurrent()) - startTime
set nf to current application's NSNumberFormatter's new()
nf's setFormat:("0.000")
set elapsedTime to ((nf's stringFromNumber:elapsedTime) as text) & " seconds"