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"