Mavericks SmartList

This is a script object that encompasses a Cocoa array, for use from Mavericks scripts via an ASObjC-based library. It is similar to the one for sets. See macscripter.net/viewtopic.php?id=41638 for how to set up and use a script library.

use framework "Foundation"

script BaseObject -- the parent script object, required for AppleScriptObjC inheritance
end script

on smartListWith:aList -- call to make a new smartList
	script SmartList
		property parent : BaseObject -- for inheritance
		property arrayStore : missing value -- where the array is stored
		
		on countOfArray() -- get count of items
			return arrayStore's |count|() as integer
		end countOfArray
		
		-- get objects and indexes
		on objectAtIndex:anInteger
			set anInteger to my correctIndex:anInteger
			set theResult to arrayStore's objectAtIndex:anInteger
			return my coerceToASClass:theResult
		end objectAtIndex:
		
		on objectsFrom:anInteger toIndex:endInteger -- get range of objects
			set anInteger to my correctIndex:anInteger
			set endInteger to my correctIndex:endInteger
			return (arrayStore's subarrayWithRange:(current application's NSMakeRange(anInteger, endInteger - anInteger + 1))) as list
		end objectsFrom:toIndex:
		
		on indexOfObject:anObject
			try -- will error if not found because NSNotFound is too big to coerce to an integer
				set theResult to ((arrayStore's indexOfObject:anObject) as integer) + 1
			on error
				return 0
			end try
			return theResult
		end indexOfObject:
		
		-- add objects
		on addObject:anObject -- adds to end
			arrayStore's addObject:anObject
		end addObject:
		
		on addObjectsFromArray:anObject -- adds to end
			arrayStore's addObjectsFromArray:anObject
		end addObjectsFromArray:
		
		on insertObject:anObject atIndex:anInteger
			set anInteger to my correctIndex:anInteger
			arrayStore's insertObject:anObject atIndex:anInteger
		end insertObject:atIndex:
		
		-- remove objects
		on removeObject:anObject
			arrayStore's removeObject:anObject
		end removeObject:
		
		on removeObjects:newList
			arrayStore's removeObjectsInArray:newList
		end removeObjects:
		
		on removeObjectAtIndex:anInteger
			set anInteger to my correctIndex:anInteger
			arrayStore's removeObjectAtIndex:anInteger
		end removeObjectAtIndex:
		
		on removeObjectsFrom:anInteger toIndex:endInteger
			set anInteger to my correctIndex:anInteger
			set endInteger to my correctIndex:endInteger
			arrayStore's removeObjectsInRange:(current application's NSMakeRange(anInteger, endInteger - anInteger))
		end removeObjectsFrom:toIndex:
		
		-- replace objects
		on replaceObjectAtIndex:anInteger withObject:anObject
			set anInteger to my correctIndex:anInteger
			arrayStore's replaceObjectAtIndex:anInteger withObject:anObject
		end replaceObjectAtIndex:withObject:
		
		-- swap objects
		on swapObjectAtIndex:anInteger withObjectAtIndex:endInteger
			set anInteger to my correctIndex:anInteger
			set endInteger to my correctIndex:endInteger
			arrayStore's exchangeObjectAtIndex:anInteger withObjectAtIndex:endInteger
		end swapObjectAtIndex:withObjectAtIndex:
		
		-- sort objects
		on sortIgnoringCase()
			my sortUsingSelector:"localizedCaseInsensitiveCompare:"
		end sortIgnoringCase
		
		on sortConsideringCase()
			my sortUsingSelector:"localizedCompare:"
		end sortConsideringCase
		
		on sortLikeFinder()
			my sortUsingSelector:"localizedStandardCompare:"
		end sortLikeFinder
		
		on standardSort() -- use for other than strings
			my sortUsingSelector:"compare:"
		end standardSort
		
		-- misc functions
		on containsObject:anObject -- whether array contains an object
			return ((arrayStore's containsObject:anObject) as integer = 1)
		end containsObject:
		
		on firstObjectCommonWithArray:newArray
			set theResult to arrayStore's firstObjectCommonWithArray:newArray
			return my coerceToASClass:theResult
		end firstObjectCommonWithArray:
		
		on componentsJoinedByString:aString
			return (arrayStore's componentsJoinedByString:aString) as text
		end componentsJoinedByString:
		
		on pathsMatchingExtensions:aList -- no "." in extensions
			return (arrayStore's pathsMatchingExtensions:aList) as list
		end pathsMatchingExtensions:
		
		on valueForKey:aString -- calls valueForKey: on every item in array
			return (arrayStore's valueForKey:aString) as list
		end valueForKey:
		
		on valueForKeyPath:aString -- calls valueForKey: on every item in array, one key after the other (keys separated by ".")
			return (arrayStore's valueForKeyPath:aString) as list
		end valueForKeyPath:
		
		on sumAndAverage() -- return {sum, average} if list is numbers
			return {(arrayStore's valueForKeyPath:"@sum.self") as real, (arrayStore's valueForKeyPath:"@avg.self") as real}
		end sumAndAverage
		
		on maxAndMin() -- return {max, min} 
			return {(arrayStore's valueForKeyPath:"@max.self") as real, (arrayStore's valueForKeyPath:"@min.self") as real}
		end maxAndMin
		
		on setValue:aValue forKey:aString
			arrayStore's setValue:aValue forKey:aString
		end setValue:forKey:
		
		-- filtering; requires valid predicate string		
		on filteredArrayUsingPredicateString:aString -- return filtered list
			set thePredicate to current application's NSPredicate's predicateWithFormat:aString
			return (arrayStore's filteredArrayUsingPredicate:thePredicate) as list
		end filteredArrayUsingPredicateString:
		
		on filterUsingPredicateString:aString -- filter in place
			set thePredicate to current application's NSPredicate's predicateWithFormat:aString
			arrayStore's filterUsingPredicate:thePredicate
		end filterUsingPredicateString:
		
		-- return as list/array
		on asArray()
			return arrayStore
		end asArray
		
		on asList()
			return arrayStore as list
		end asList
		
		-- handlers for script's use
		on correctIndex:anInteger -- for script's use; convert AS index to Cocoa index
			if anInteger < 0 then
				return anInteger + (my countOfSet())
			else
				return anInteger - 1
			end if
		end correctIndex:
		
		on coerceToASClass:anObject -- for script's use; coerce to AS class for return
			if ((anObject's isKindOfClass:(current application's NSArray)) as integer = 1) then
				return anObject as list
			else -- coerce to list and return item 1; workaround to coerce item of unknown class
				set anObject to anObject as list
				return item 1 of anObject
			end if
		end coerceToASClass:
		
		on sortUsingSelector:theSel -- for script's use
			set theDesc to current application's NSSortDescriptor's sortDescriptorWithKey:"self" ascending:true selector:theSel
			arrayStore's sortUsingDescriptors:{theDesc}
		end sortUsingSelector:
		
	end script
	
	set arrayStore of SmartList to current application's NSMutableArray's arrayWithArray:aList
	return SmartList
end smartListWith:

Some example code:

use theLib : script "<name of library>"
use scripting additions

set theList to theLib's smartListWith:{1, 2, 3, 5, 3, 2, 6}
log (theList's indexOfObject:5)
theList's insertObject:"blah" atIndex:3
log theList's asList()
log (theList's firstObjectCommonWithArray:{4, 5, 3})
theList's removeObjectAtIndex:3
log theList's asList()
log (theList's filteredArrayUsingPredicateString:"SELF > 2")
log (theList's componentsJoinedByString:" and ")
log (theList's valueForKey:"doubleValue")
(theList's valueForKey:"stringValue")

Edited to add handlers valueForKeyPath:, sumAndAverage(), and maxAndMin().

1 Like

@Shane_Stanley

Hi,

I am curious about the following lines here:

I do know about inheritance in AppleScript, but what exactly is the function of this line here (in connection with the parent property, of course).

I am especially curious because when googeling things like “parent : BaseObject” etc. I get no meaningful results except the three libraries (set, list, and record) you posted here. So maybe the naming “BaseObject” is arbitrary here, it just hast to be an empty script on the top level of the file, or so?

In AppleScriptObjC explored I find several self explantatory references to

property : parent class “NSObject”

but I have difficulties when trying to come to terms with this one.

Please enlighten me! Thanks a lot, Peter

As I understand, the script BaseObject incapsulates the library handlers. But I have other question: I can log items of result, but can’t get it into the AppleScript. I am on Catalina.

use theLib : script "Untitled.scptd"
use framework "Foundation"
use scripting additions

set theNSArray to theLib's smartListWith:{1, 2, 3, 5, 3, 2, 6}

log (theNSArray's objectAtIndex:2) --> (*2*) in the Log window
(theNSArray's objectAtIndex:2) --> no result
1 Like

If AS is working as JS in that respect, you have to convert the result to the appropriate AS type, perhaps with as string or so.

no result returned at all, so coercion doesn’t make sense.

Strange.
No problem getting a result here on Mojave.

.
I will clarify. The result is not returned to the Script Debugger. Everything is fine in the Script Editor:
.

use theLib : script "Untitled.scptd"
use framework "Foundation"
use scripting additions

set theNSArray to theLib's smartListWith:{1, 2, 3, 5, 3, 2, 6}

log (theNSArray's objectAtIndex:2) --> (*2*) in the Log window
set a to (theNSArray's objectAtIndex:2) --> 2
a + 2 --> 4

I seem to have figured out what’s going on with the Script Debugger. The result is received correctly, but for some reason the Script Debugger refuses to display it in the Results window. In the Variables window, the result is displayed correctly. Looks like an application bug.
.

use theLib : script "Untitled.scptd"
use framework "Foundation"
use scripting additions

set theNSArray to theLib's smartListWith:{1, 2, 3, 5, 3, 2, 6}

log (theNSArray's objectAtIndex:2) --> (*2*) in the Log window
set a to (theNSArray's objectAtIndex:2) --> 2
a + 2 --> 4
set b to result --> b is 4, but not displayed in the Result window

The default parent is the AppleScript component itself. This script object follows a use framework statement, and hence uses ASObjC. By making this script – which inherits from AppleScript itself – the parent, we effectively interpose ASObjC in the inheritance chain. The actual name of the script object is arbitrary.

They demonstrated a technique, but it turned out that most people just found it confusing. It also makes debugging more difficult. Basically, I gave up on that approach.

I’m guessing you have debugging on. There’s a bug in AppleScript that makes it impossible for Script Debugger to get the result of lines that include the term result like that.

I am using

Script Debugger 8
v8.0.6 (8A65) 28. März 2023, 64-bit Intel (Expires June 1, 2023)

and it works (on Mojave) as is, not matter wether debugging is on or off.

Does this version make a difference, perhaps?
Or is this a bug in or starting with Catalina?

It could. I was going from memory, but now I try it here, it’s working fine in Ventura.

OK, I see. At least things start to make (some more) sense for me now. The ASObjC inheritance becomes neccessary because you adopted the admittedly very elegant strategy to use a script object as the main body.

To view the matter from a different angle: given that you abandoned this approach, what changes would be neccessary to make the library work without the parent property?

As far as I can see, one way should be to simply flatten the library out, i. e. do no longer use a script but instead transform the library into a simple collection of handlers at the files root level.

Would this be the best approach?
Or the only (sensible) one?

Thanks a lot, again. Peter

Yes, pretty much. It just means you’d be passing the value back and forth each time.

Yes - a lot less elegant. Thanks a lot!

.
The problem I mentioned above disappeared by itself after installing the trial version of Script Debugger 8 version 8.0.5. Now the result is displayed correctly.

NOTE: I have tested the script before and now not in Debug mode

@Shane_Stanley
In another post I found this - highly interesting! - quote:

Which leads me to bring up another aspect / experience I’ve had using SmartList:
I have a script where I initialize a property with the return value from the initializer of SmartList in the run handler (which is only for initialisation purposes). This property then is used in an idle handler, which monitors the contents of the clipboard constantly.

This setup worked for years under Mojave, without any problems. Still on the very same old OS installation (no longer getting security updates etc.) out of the blue I started to get odd, mostly unintelligible errors. I could only suspect they were related to this or another AS library I used in this script. At first it was enough to open it in ScriptDebugger, option-recompile it and the error went away. Sometimes for weeks, sometimes for days. Somehow it seemed that this error was related to system crashes or even simple OS or app restarts. As if the AS library (or libraries?) where somehow cached by the system but the link into the cache got broken or something and could only be restored be recompiling.

Then more recently I got errors from the idle handler where I was told that the property mentioned above, i. e. the SmartList script object, could not deal with one of its handlers because it was a missing value. I hadn’t changed anything in the script but out of the blue an initialized property went blank.
The only remedy I could find (recompiling was not enough) was to initialize the SmartList script object within the idle handler itself (checking for missing value on each call of the idle handler). Now again, it works for weeks without any issues.

I can not believe this myself, but this is what happened.
Now my question is: is this the kind of issue you found causing grief in your quote above (though mine is no Xcode project, just a simple AS stay open app)?

They may have ultimately similar causes, but it’s impossible to say any more than that – it would be pure speculation.