Eliminate repeat loop with predicate

I ran my own timing tests this morning, timing just the filter code in each script. The initial URL fetch is common to all the scripts and takes up the bulk of their running time, so any vagaries while it’s in progress to need to be eliminated from the filter method comparison results.

Actual timings, as always, depend on the model and age of the computer, the system it’s running, how much processor time is diverted to background tasks during the tests, the user’s set-up, etc. In the current case, the number and distribution of files and folders in the user’s home directory could be factors. But although my home folder clearly contains far more stuff than peavine’s (junk accumulated over 27 years of Mac ownership), my results are broadly in line with his in regard to what’s faster than what. Only the original three folders were “skipped” in my tests. The scripts were run with all of them open at the same time in both Script Editor and Script Debugger and with a Numbers document open to note the results. I include the results below in case anyone’s interested — and because I wanted to experiment with peavine’s table posting technique. :wink: The most interesting results, as I mentioned somewhere above, are actually the different numbers of items reportedly returned depending on whether the scripts are run in Script Editor or Script Debugger! I’ll have to look into this later on.

Script Editor Script Debugger
Seconds Result Count Seconds Result Count
INITIAL GET 4.849 174260 4.276 230133
-
FILTER
Peavine 1 2.360 171599 3.255 227473
Jonas 2.304 171599 3.243 227473
Nigel 1 1.259 171599 1.755 227473
Nigel 2 1.256 171599 1.756 227473
CJK 1 1.452 171599 2.044 227473
CJK 2 3.473 171599 4.627 227473
Nigel 3 1.351 171599 1.916 227473

@Nigel_Garvey
Can you tell us what’s your configuration?
The timing results you shared seem very fast to me.
With my MacPro 2013 under Monterey, your second script takes 28 seconds for 383696 total files and 375463 filtered.

Can you run this script and add it to your timing report?

use framework "Foundation"
use scripting additions

tell application "Finder" to set toKeep to (folders of (path to current user folder) whose name is not in {"Movies", "Music", "Pictures", "Library"}) as alias list

set filteredArray to current application's NSMutableArray's new()
set fileManager to current application's NSFileManager's defaultManager()
repeat with aKept in toKeep
	set entireContent to (fileManager's enumeratorAtURL:aKept includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()
	set filteredArray to (filteredArray's arrayByAddingObjectsFromArray:entireContent)
end repeat

filteredArray's |count|()

:wink:

Hi @ionah. I did the tests on my desktop machine, which is an iMac 18,3 with a 3.4 GHz Quad-Core Intel Core I5 processor running macOS Ventura 13.6.7. I didn’t bother copying the files over to my M3 MacBook to test there. Its screen’s tiny. :wink:

As I explained above, the comparative timings are only for the scripts’ filter set-up-and-execution parts, which will obviously be less than when the fetching of the disk data and creation of the original URL array are included.

On my iMac, your script takes 0.96 seconds in Script Editor and 1.047 seconds in Script Debugger, reporting 57190 items in both cases. Only getting the folder contents you actually want is clearly more efficient than getting your entire home folder contents and filtering out what you don’t. But in the spirit of peavine’s original enquiry and of not assuming that the “skipped” folders will be siblings in the same root folder, I didn’t pursue that path myself. :wink:

Nigel–a point of clarification.

My original script got the entire contents of my home folder and skipped three folders (and their contents). In that context, I would not expect your above statement to be the case. In retesting and for testing purposes only, I skipped all but a few folders, and in that context I would expect your above statement to be the case. Do I misunderstand what you are saying?

I misread this part…
In this context, my results are close to yours.
Many thanks!

Hi @peavine. I’m not quite clear myself what you’re asking. :smile: But what I was thinking is that your script and those derived from it don’t skip folders, they get the entire contents of the root folder and then filter the unwanted folders and their contents from the result. ionah’s most recent script does skip folders, hence its speed. But it’s only good where the folders are immediate subfolders of the root.

If anyone whose not an expert is passing by, the script can accept a list of folders coming from any place. It needs a small adaptation:

use framework "Foundation"
use scripting additions

set toKeep to {"/Applications", "/Users/Shared"}

set filteredArray to current application's NSMutableArray's new()
set fileManager to current application's NSFileManager's defaultManager()
repeat with aKept in toKeep
	set anURL to (current application's NSURL's fileURLWithPath:aKept)
	set entireContent to (fileManager's enumeratorAtURL:anURL includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()
	set filteredArray to (filteredArray's arrayByAddingObjectsFromArray:entireContent)
end repeat

filteredArray's |count|()

Nigel. You were responding to Jonas’ script, which I hadn’t read when I posted, and I now understand your comment. Sorry about that. And, BTW, in actual testing my thought was wrong anyways. :frowning:

On investigation, it turned out that Script Debugger had been given full disk access on my machines whereas Script Editor hadn’t. Granting it to the latter has resulted in the scripts returning the same, higher counts in both editors. Another of life’s great mysteries solved! :grin:

FWIW, I did a bit more research to determine the relative speeds of the following approaches:

  • Get the contents of a home folder and subtract what is not wanted.
  • Get the wanted folders and get their contents.

The following script takes the second approach and is faster by a few milliseconds. It should be noted that this script has a flaw in that it does not return files in the home folder.

use framework "Foundation"
use scripting additions

on main()
	set theFolder to "/Users/Robert"
	set skipFolders to {"/Users/Robert/Movies", "/Users/Robert/Music", "/Users/Robert/Pictures"}
	set keepFolders to getKeepFolders(theFolder, skipFolders)
	set folderContents to getFolderContents(keepFolders)
end main

on getKeepFolders(theFolder, skipFolders)
	set fileManager to current application's NSFileManager's defaultManager()
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set directoryKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set folderContents to fileManager's contentsOfDirectoryAtURL:theFolder includingPropertiesForKeys:{} options:4 |error|:(missing value)
	set theFolders to current application's NSMutableArray's new()
	repeat with anItem in folderContents
		set {theResult, aDirectory} to (anItem's getResourceValue:(reference) forKey:directoryKey |error|:(missing value))
		if aDirectory as boolean is true then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
			if aPackage as boolean is false then (theFolders's addObject:anItem)
		end if
	end repeat
	set skipURLs to current application's NSMutableArray's new()
	repeat with aFolder in skipFolders
		set aURL to (current application's |NSURL|'s fileURLWithPath:aFolder)
		(skipURLs's addObject:aURL)
	end repeat
	theFolders's removeObjectsInArray:skipURLs
	return theFolders
end getKeepFolders

on getFolderContents(theFolders)
	set fileManager to current application's NSFileManager's defaultManager()
	set folderContents to current application's NSMutableArray's new()
	repeat with aFolder in theFolders
		set aFolderContents to (fileManager's enumeratorAtURL:aFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects()
		(folderContents's addObjectsFromArray:aFolderContents)
	end repeat
	folderContents's addObjectsFromArray:theFolders
	return folderContents
end getFolderContents

main()

BTW, I did not coerce folderContents to a list because I did not test it that way. To view the script result in Script Editor, folderContents should be coerced to a list.