JXA vs. AppleScript - 'whose' filters

Note that the examples in this post are all in JXA . You’ll need to select JavaScript as the language in Script Editor (either in its Preference or in the pop-up menu below each document window toolbar) if you want to try them.

Inspired by a recent query here concerning JavaScript for Automation (JXA), I’ve been spending a few days looking into it. It’s basically a recent JavaScript standard to which has been added the OSA-bridging facilities described in Apple’s JavaScript for Automation Release Notes.

One of the first things you notice when trying through the examples in the 10.10 notes is that the section called “Filtering Arrays” is almost entirely rubbish. For a start, it’s not arrays which are filtered, but, as with AppleScript’s ‘whose’ filters, the application references which return them. Furthermore, only the syntax described for single, positive conditions actually works!

// tell application "Finder" to set theseFiles to (files of target of front Finder window whose name begins with "JXA")

var theseFiles = Application("Finder").finderWindows[0].target.files.whose({name: {_beginsWith: "JXA"}})();
theseFiles; // --> Array of JavaScript-style Finder references to the matching files.

The syntax for ‘not’, ‘and’, and ‘or’ conditions is even more complex, presumably reflecting the structure of the underlying events. But in Mojave and El Capitan, and presumably on all systems between, it simply throws a “Can’t convert types.” error when run. And quite frankly, who can blame it? :wink:

// tell application "Finder" to set theseFiles to (files of target of front Finder window whose name does not begin with "JXA")

var theseFiles = Application("Finder").finderWindows[0].target.files.whose({_not: [{name: {_beginsWith: "JXA"}}]})();
// --> Error -1700: Can't convert types.
// tell application "Finder" to set theseFiles to (files of target of front Finder window whose name does not begin with "JXA" and name extension is "scpt")

var theseFiles = Application("Finder").finderWindows[0].target.files.whose({_and: [{_not: [{name: {_beginsWith: "JXA"}}]}, {nameExtension: {_equals: "scpt"}}]})();
// --> Error -1700: Can't convert types.

Since JXA can use the StandardAdditions, the simplest way to handle compound ‘whose’ filters (for the Finder, at least) would be to use ‘run script’ with the source code for the equivalent AppleScript:

var app = Application.currentApplication();
app.includeStandardAdditions = true;

var theseFiles = app.runScript(`tell application "Finder" to return (files of target of front Finder window whose name does not begin with "JXA" and name extension is "scpt") as alias list`, {in: "AppleScript"});
theseFiles; // Array of JavaScript Path() objects for the matching files.

The ‘as alias list’ in the AppleScript source code above ensures that the JavaScript result contains Path() objects (equivalent to AS file specifiers) instead of invalid ‘Application.currentApplication()’ references.

There are longer workarounds which may turn out to be faster. I haven’t found a way to coerce JavaScript Finder specifiers directly to Path() objects, but the latter can be derived quite easily from the Finder objects’ ‘url’ values, through the medium of NSURL. The Foundation framework’s objects are conveniently available to JXA scripts by default:

// tell application "Finder" to set theseFiles to (files of target of front Finder window whose name does not begin with "JXA" and name extension is "scpt") as alias list

// Initially filter positively for files whose name extension is "scpt".
var firstPass = Application("Finder").finderWindows[0].target.files.whose({nameExtension: {_equals: "scpt"}})();

// Then go through these with a repeat to identify any whose name doesn't begin with "JXA".
var theseFiles = [];
for (let thisFile of firstPass) {
	if (!thisFile.name().startsWith("JXA")) {
		// Derive an NSURL from this Finder specifier's 'url' value …
		thisURL = $.NSURL.URLWithString(thisFile.url());
		// … and from this a JavaScript path and then a Path(), which is appended to the output array.
		theseFiles.push(Path(thisURL.path.js));
	};
};

theseFiles; // Array of Path() objects.

Or a more “JavaScriptObjC” version:

// tell application "Finder" to set theseFiles to (files of target of front Finder window whose name does not begin with "JXA" and name extension is "scpt") as alias list

// Derive an NSURL from the url property of the front Finder window's target folder.
var targetURL = Application("Finder").finderWindows[0].target.url();
targetURL = $.NSURL.URLWithString(targetURL);

// Get the folder's visible contents, including information about whether or not they're regular files or packages.
var fileManager = $.NSFileManager.defaultManager;
var fileAndPackageKeys = $([$.NSURLIsRegularFileKey, $.NSURLIsPackageKey]);
var targetContents = fileManager.contentsOfDirectoryAtURLIncludingPropertiesForKeysOptionsError(targetURL, fileAndPackageKeys, $.NSDirectoryEnumerationSkipsHiddenFiles, $());

// Filter these for URLs satisfying both the name and extension criteria.
var filter = $.NSPredicate.predicateWithFormat("(!lastPathComponent BEGINSWITH 'JXA') && (pathExtension == 'scpt')");
var filteredContents = targetContents.filteredArrayUsingPredicate(filter).js; // JavaScript array containing NSURLs.

// Go through what's left and collect JavaScript Path()s for NSURLs representing files or packages.
var theseFiles = [];
for (let thisURL of filteredContents) {
	let resourceValues = thisURL.resourceValuesForKeysError(fileAndPackageKeys, $()).js
	if ((resourceValues.NSURLIsRegularFileKey) || (resourceValues.NSURLIsPackageKey)) {
		theseFiles.push(Path(thisURL.path.js)) ;
	};
};
theseFiles;

Nigel, many thanks for another great reference❗