Here’s a quasi ‘where’ filter handler for AS lists. The predicate can be presented in various ways for convenience. Demos at the bottom of the script.
(*
Handler: filterList:forInstance:ofClass:|where|: — Quasi 'whose'/'where' filter for lists.
filterList:
The list from which to derive the filtered result.
forInstance:
Either (1) an integer denoting the instance number of the match to return;
or (2) a list of two integers, indicating the range of matching instances to return;
or (3) an empty list, signifying that every matching item should be returned.
Values which are coercible to integer may be used instead of actual integers.
ofClass:
A class keyword for the class of item to match. It will need to be parenthesised if coded directly into the call.
|where|:
Either (1) a script object containing an isMatch(a) handler which tests a passed item against the required 'whose'/'where' condition(s);
or (2) text containing source code for an AS predicate which might come after 'where' in a real filter. eg.:
"it is 7"
"(item 4 is 5) and ((item 2 begins with \"z\") or (item 7 is {|name|:\"Fred\", age:109}))"
or: (3) a list containing text(s) and/or {text, actual value} list(s) from which such a predicate can be constucted.
{"it is 7"}
{"(item 4 is 5)", "and", "((item 2 begins with \"z\")", "or", "(item 7 is {|name|:\"Fred\", age:109}))"}
{{"item 4 is", 5}, "and (", {"item 2 begins with", "z"}, "or", {"item 7 is", {|name|:"Fred", age:109}}, ")"}
Result:
The requested match(es) if fully achievable. Otherwise an error.
*)
on filterList:theList forInstance:n ofClass:requiredClass |where|:whereConditions
-- Original and result lists in a script object for speed of access.
script o
property originalList : theList
property matchedItems : {}
end script
-- Analyse the instance parameter. Indexed single match, range, or every?
try
set singleMatchWanted to (class of n is not list)
if (singleMatchWanted) then
-- If it's not a list, make sure it's a non-zero integer.
set n to n as integer
if (n is 0) then error
-- If it's negative, it's convenient to reverse the source list and use a positive index.
if (n < 0) then
set o's originalList to reverse of o's originalList
set n to -n
end if
else if (n is not {}) then
-- If it's a non-empty list, check that it only contains two items and derive non-zero integers from both of them.
if ((count n) is not 2) then error
set {n1, n2} to {beginning of n as integer, end of n as integer}
if ((n1 is 0) or (n2 is 0)) then error
end if
on error
error "filterList:forInstance:ofClass:|where|: : bad forInstance: parameter."
end try
-- Analyse the 'whose'/'where' conditions.
set classOfWhereConditions to class of whereConditions
if (classOfWhereConditions is script) then
-- If they're supplied as a script object, use that to match values.
set matcher to whereConditions
else if (classOfWhereConditions is text) then
-- If as a line of text, insert it into the source code for a script-object-creating script (!) and run the script.
set matcher to (run script ¬
("on run
script
on isMatch(a)
tell a to return (" & whereConditions & ")
end
end
return result
end"))
else if (classOfWhereConditions is list) then
-- If as a list, assemble the source code for a script-object-creating script from its contents. This script will take a parameter list.
set matcherCode to "on run argv" & linefeed & "script" & linefeed & "on isMatch(a)" & linefeed & "tell a to return ("
set argv to {}
set argvCount to 0
repeat with i from 1 to (count whereConditions)
set thisFragment to item i of whereConditions
set classOfThisFragment to class of thisFragment
if (classOfThisFragment is text) then
-- Append items which are simply text to the source code.
set matcherCode to matcherCode & " " & thisFragment
else if ((classOfThisFragment is list) and ((count thisFragment) is 2)) then
-- With items which are two-item lists containing a text and another value, append the text to the source code along with an index reference into the parameter list and add the other item to that list.
set fragmentCode to beginning of thisFragment
if (class of fragmentCode is not text) then error
set argvCount to argvCount + 1
set matcherCode to matcherCode & (" (" & fragmentCode & " (item " & argvCount & " of argv))")
set end of argv to end of thisFragment
else
error
end if
end repeat
set matcherCode to matcherCode & (")" & linefeed & "end" & linefeed & "end" & linefeed & "return result" & linefeed & "end")
-- Run the created source code to get the script object.
set matcher to (run script matcherCode with parameters argv)
else
error "filterList:forInstance:ofClass:|where|: : bad |where|: parameter."
end if
-- Work through the original list, testing each item for the required class and, where that matches, for the 'where' conditions.
set matchCount to 0
repeat with i from 1 to (count o's originalList)
set thisItem to item i of o's originalList
set classOfThisItem to class of thisItem
if (((classOfThisItem is requiredClass) or (requiredClass is item) or ((requiredClass is number) and ((classOfThisItem is integer) or (classOfThisItem is real)))) and (matcher's isMatch(thisItem))) then
set matchCount to matchCount + 1
if (singleMatchWanted) then
-- If returning a single match, simply return it when shows up.
if (matchCount = n) then return thisItem
else
-- Otherwise add all matches to the matched items list.
set end of o's matchedItems to thisItem
end if
end if
end repeat
if (singleMatchWanted) then
-- An nth match wasn't returned above.
error "filterList:forInstance:ofClass:|where|: : Can't get instance " & n & " of " & matchCount & " matches." number -1728
else if (n is {}) then
-- Return every matched item (if any).
return o's matchedItems
else
-- Try to return the requested range of matched items.
try
return items n1 thru n2 of o's matchedItems
on error
error "filterList:forInstance:ofClass:|where|: : Can't get instances " & n1 & " thru " & n2 & " of " & matchCount & " matches." number -1728
end try
end if
end filterList:forInstance:ofClass:|where|:
(* Demos: *)
-- A list containing some four-item lists and a few records and numbers.
set listOfMixedItems to {{missing value, "The Great Escape", missing value, 3}, {missing value, "Attack of the Killer Aardvark", missing value, 2}, {|name|:"Fred", age:27}, {missing value, "The Framework Foundation", missing value, 1}, {missing value, "Bourne Again", missing value, 3}, -5, {|name|:"Fred", age:109}, {|name|:"Bert", age:74}, 99, {missing value, "Harry Potter and the Finder Script", missing value, 3}, 75, {|name|:"Fred", age:45}, 75.0, {missing value, "The Ed Stockly Story", missing value, 5}, 5, {missing value, "Star Wars Episode CCXIV: The Empire Goes Bankrupt", missing value, 1}, {missing value, "Colonel Panic", missing value, 1}, {missing value, "The Bucket Dictionary", missing value, 1}, 2.4}
-- To get the first list of listOfMixedItems where ((item 4 is 1) and ((item 2 contains "the") or (item 2 contains "Panic"))),
-- make the filterList: parameter listOfMixedItems, the forInstance: parameter 1, the ofClass: parameter (list),
-- and the |where|: parameter either a script object with an isMatch() handler performing the relevant AS 'whose/where' predicate …
script theseConditionAreMet
on isMatch(a)
tell a to return ((item 4 is 1) and ((item 2 contains "the") or (item 2 contains "Panic")))
end isMatch
end script
my filterList:listOfMixedItems forInstance:1 ofClass:(list) |where|:theseConditionAreMet
-- … or a text version of the predicate …
my filterList:listOfMixedItems forInstance:1 ofClass:(list) |where|:"(item 4 is 1) and ((item 2 contains \"the\") or (item 2 contains \"Panic\"))"
-- … or a list containing a text version of the predicate or the parts thereof …
my filterList:listOfMixedItems forInstance:1 ofClass:(list) |where|:{"(item 4 is 1)", "and", "((item 2 contains \"the\") or (item 2 contains \"Panic\"))"}
-- … or a list containing similar text(s) and/or lists representing templates for individual conditions, each of these containing a single predicate text and an actual value.
set {int, w1, w2} to {1, "the", "panic"}
my filterList:listOfMixedItems forInstance:1 ofClass:(list) |where|:{{"item 4 is", int}, "and (", {"item 2 contains", w1}, "or", {"item 2 contains", w2}, ")"}
-- To get the last list which matches the conditions, make the forInstance: value -1.
my filterList:listOfMixedItems forInstance:-1 ofClass:(list) |where|:theseConditionAreMet
-- For a range of matching lists, say matches 1 thru 2, make the forInstance: value a list containing two integers.
my filterList:listOfMixedItems forInstance:{1, 2} ofClass:(list) |where|:theseConditionAreMet
-- To get every matching list, make the forInstance: value an empty list.
my filterList:listOfMixedItems forInstance:{} ofClass:(list) |where|:theseConditionAreMet
(* Other examples: *)
-- Every number of listOfMixedItems where ((its class is real) or (it > 10)).
my filterList:listOfMixedItems forInstance:{} ofClass:(number) |where|:{{"its class is ", real}, "or", {"it >", 10}}
-- The last two records of listOfMixedItems where ((its |name| is "Fred) and (its age < 80))
my filterList:listOfMixedItems forInstance:{-2, -1} ofClass:(record) |where|:{{"its |name| is", "Fred"}, "and", {"its age <", 80}}
It’s also possible to filter lists of application objects, but it may depend on the application. The class parameter must be specific rather than generic (eg. document file
rather than just file
in the Finder) or it can be sidelined altogether by passing (item)
.
-- A list of Finder items.
tell application "Finder" to set l to items of desktop
-- Get every document file of the list whose name extension begins with "scpt"
-- Either:
script classIsDocumentFileAndNameExtensionBeginsWithScpt
on isMatch(a)
tell application "Finder"
return (a's name extension begins with "scpt")
end tell
end isMatch
end script
using terms from application "Finder" -- For 'document file'.
my filterList:l forInstance:{} ofClass:(document file) |where|:classIsDocumentFileAndNameExtensionBeginsWithScpt
end using terms from
-- Or some variation on:
using terms from application "Finder" -- For 'document file'.
my filterList:l forInstance:{} ofClass:(document file) |where|:{{"its name extension begins with", "scpt"}}
end using terms from
The above demo, which fetches every item of the desktop and then sorts out the relevant document files twice for demo purposes, takes only half as long on my machine as the standard Finder method executed just once!
tell application "Finder"
set l to (document files of desktop) whose name extension begins with "scpt"
end tell