Conversion of NSSet objects to Applescript lists

Nice one! :cool:

I’ve been trying to improve on it all day to no avail. :wink: My own effort’s somewhat more compact, but only just over half as fast:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"

property || : a reference to current application

on convertToASValue(theObj)
	-- An internal script object containing a recursive handler.
	script o
		on itemiseSetsIn(theObj)
			-- If theObj is a set or an ordered set, get an array version of it.
			if (theObj's isKindOfClass:(||'s class "NSSet")) as boolean then
				set theObj to theObj's allObjects()
			else if (theObj's isKindOfClass:(||'s class "NSOrderedSet")) as boolean then
				set theObj to theObj's array()
			end if
			-- If what we have is an array or a dictionary, get a mutable version of it and recursively edit its items.
			-- It's irritating that mutable copies have to be made and the items set even though nothing may actually need changing, but it takes just as long to find out beforehand if it's necessary!
			if (theObj's isKindOfClass:(||'s class "NSArray")) as boolean then
				set theObj to theObj's mutableCopy()
				repeat with i from 1 to (count theObj)
					set item i of theObj to itemiseSetsIn(item i of theObj)
				end repeat
			else if (theObj's isKindOfClass:(||'s class "NSDictionary")) as boolean then
				set theObj to theObj's mutableCopy()
				set theKeys to theObj's allKeys()
				repeat with thisKey in theKeys
					(theObj's setObject:(itemiseSetsIn(theObj's objectForKey:(thisKey))) forKey:(thisKey))
				end repeat
			end if
			
			-- Return the edited object to the recursion level above,
			return theObj
		end itemiseSetsIn
	end script
	
	-- Call the recursive handler to exchange any sets or ordered sets for arrays.
	set convertedObj to o's itemiseSetsIn(theObj)
	-- Get an AS version of the result by coercing an array containing it to list and extracting the only item.
	return ((||'s class "NSArray"'s arrayWithObject:(convertedObj)) as list)'s beginning
end convertToASValue

Nigel,

Thank you for your kind words.

I like your direct reference to Cocoa classes (e.g., class “NSSet”) rather than my method-based expression (e.g., NSSet’s |class|()) when testing for class type. I also appreciate your adding the Cocoa NSOrderedSet collection class that I neglected to include. I incorporated both features, along with some minor coding and wording improvements, into the script below that I consider to be the preferred version over that which was originally submitted.

Inspired by your effort, I also tried extracting collection objects and values into Cocoa NSArray objects rather than into Applescript lists (as is currently the case in my script) before processing them recursively in the repeat loop. It eliminates the initial conversion step for an input argument that is a Cocoa object, but it adds a new conversion step for an input argument that is an Applescript value. Alas, I too found an execution speed hit, with my modified version executing at only 40% of the speed of the current version. Thus, I abandoned that approach.

I would like to ask a couple of questions:

(1) Why do you use the expression a reference to current application rather than simply current application in your property statement?
(2) Towards the end of your script, you use the expression ||'s class “NSArray”'s arrayWithObject:…. Is there an advantage of that form over ||'s NSArray’s arrayWithObject:…, which seems simpler?
(3) This is a rhetorical question. How did it take me so many years to learn that one can use beginning as an alternative to first item or item 1, and end as an alternative to last item or item -1, when referencing the first or last item of a list? :confused: I like it and find myself gravitating toward using end to retrieve the sole value of a single-item list to avoid having to type the longer word beginning. But I also find the terminology a bit confusing:

set x to {1, 2, 3}
set y to {1, 2, 3}

get item 1 of x --> 1
get beginning of y --> 1

but...

set item 1 of x to 4 --> {4, 2, 3}
set beginning of y to 4 --> {4, 1, 2, 3}

It is because of the insertion rather than the replacement effect of the term beginning in the final statement that I always thought of it as meaning the location before a list’s first item rather than the first item itself.

In any case, here is an example of usage of the improved version of the handler with the addition of NSOrderedSet objects among the nested values:

set cocoaArray to (my ||'s NSArray)'s arrayWithArray:{1, 2.2, "three"}
set cocoaDictionary to (my ||'s NSDictionary)'s dictionaryWithDictionary:{a:false, b:{4, 5, 6}, c:{aa:7, bb:8, cc:9}}
set cocoaSet to (my ||'s NSSet)'s setWithArray:{10, 11.11, "twelve"}
set cocoaOrderedSet to (my ||'s NSOrderedSet)'s orderedSetWithArray:{97, 98.98, "ninety-nine"}
set cocoaArrayWithNestedObjects to (my ||'s NSArray)'s arrayWithArray:{cocoaArray, cocoaDictionary, cocoaSet, cocoaOrderedSet, {cocoaArray, cocoaDictionary, cocoaSet, cocoaOrderedSet, {cocoaArray, cocoaDictionary, cocoaSet, cocoaOrderedSet, {cocoaArray, cocoaDictionary, cocoaSet, cocoaOrderedSet}}}}

my cocoaToASValue(cocoaArrayWithNestedObjects)

--> {{1, 2.200000047684, "three"}, {a:false, b:{4, 5, 6}, c:{aa:7, bb:8, cc:9}}, {11.109999656677, 10, "twelve"}, {97, 98.980003356934, "ninety-nine"}, {{1, 2.200000047684, "three"}, {a:false, b:{4, 5, 6}, c:{aa:7, bb:8, cc:9}}, {11.109999656677, 10, "twelve"}, {97, 98.980003356934, "ninety-nine"}, {{1, 2.200000047684, "three"}, {a:false, b:{4, 5, 6}, c:{aa:7, bb:8, cc:9}}, {11.109999656677, 10, "twelve"}, {97, 98.980003356934, "ninety-nine"}, {{1, 2.200000047684, "three"}, {a:false, b:{4, 5, 6}, c:{aa:7, bb:8, cc:9}}, {11.109999656677, 10, "twelve"}, {97, 98.980003356934, "ninety-nine"}}}}}

And here is the improved version of the handler:

use framework "Foundation"
property || : current application

on cocoaToASValue(theObj)
	-- Converts a Cocoa object to a corresponding Applescript value
	-- If the Cocoa object can't be converted, or if the input argument is an Applescript value, returns the input argument unchanged
	set {tmpList, tmpKeys} to {null, null}
	-- If the input argument is a Cocoa or Applescript collection, extract its item values into an Applescript list; otherwise, set the return value to its Applescript value
	tell theObj
		try
			-- Handle the case where the input argument is a Cocoa object
			-- If the input argument is instead an Applescript value, it will trigger an error when it encounters the isKindOfClass method call and will be processed in the "on error" clause
			if (its isKindOfClass:(my ||'s class "NSArray")) as boolean then
				-- If the input argument is a Cocoa NSArray or one of its subclasses, convert it to a temporary Applescript list
				set tmpList to it as list
			else if (its isKindOfClass:(my ||'s class "NSSet")) as boolean then
				-- If the input argument is a Cocoa NSSet or one of its subclasses, first convert it to an NSArray, then convert the NSArray to a temporary Applescript list
				-- Note that since an NSSet is unordered, the order of the Applescript list items will be undefined
				set tmpList to (its allObjects()) as list
			else if (its isKindOfClass:(my ||'s class "NSOrderedSet")) as boolean then
				-- If the input argument is a Cocoa NSOrderedSet or one of its subclasses, first convert it to an NSArray, then convert the NSArray to a temporary Applescript list
				set tmpList to (its array()) as list
			else if (its isKindOfClass:(my ||'s class "NSDictionary")) as boolean then
				-- If the input argument is a Cocoa NSDictionary or one of its subclasses, extract its keys into an NSArray and its values into a temporary Applescript list
				set tmpKeys to its allKeys()
				set tmpList to (its objectsForKeys:tmpKeys notFoundMarker:(null)) as list
			else
				-- Otherwise, set the return value to its Applescript value via the Cocoa-Applescript bridge by making it the item of a single-item Applescript list, then extracting the list's item
				set asValue to (it as list)'s end
			end if
		on error
			-- Handle the case where the input argument is an Applescript value
			if its class = list then
				-- If the input argument is an Applescript list, assign its value to a temporary Applescript list
				set tmpList to it
			else if its class = record then
				-- If the input argument is an Applescript record, extract its keys into an NSArray and its values into a temporary Applescript list
				tell ((my ||'s NSDictionary)'s dictionaryWithDictionary:it)
					set tmpKeys to its allKeys()
					set tmpList to (its objectsForKeys:tmpKeys notFoundMarker:(null)) as list
				end tell
			else
				-- Otherwise, if the input argument is of any other Applescript class, set the return value to it
				set asValue to it
			end if
		end try
	end tell
	-- If the input argument was a Cocoa or Applescript collection, convert its extracted objects/values to Applescript values in a recursive fashion so that nested collections are converted properly
	if tmpList ≠ null then
		-- Convert the extracted objects/values recursively
		set asValue to {}
		repeat with i in tmpList
			set end of asValue to my cocoaToASValue(i's contents)
		end repeat
		-- If the input argument was a Cocoa NSDictionary (or one of its subclasses) or an Applescript record, reconstruct it as an Applescript record from its extracted keys and converted values
		if tmpKeys ≠ null then set asValue to ((my ||'s NSDictionary)'s dictionaryWithObjects:((my ||'s NSArray)'s arrayWithArray:asValue) forKeys:tmpKeys) as record
	end if
	-- Return the input argument's Applescript value
	return asValue
end cocoaToASValue

I think that’s why I couldn’t make my script as fast as yours. Using some ObjC classes in scripts isn’t as fast as using their AS counterparts. Rather counterintuitive. :confused:

Hedging my bets, mainly. :wink: I’m not sure what’s compiled into the script with just current application. Is it a general reference to a current application or a specific reference to the one in which the script’s compiled? Querying the property in either form returns the keyword current application, so perhaps a reference to isn’t necessary.

W-e-l-l… Not really. When I was learning ASObjC from Shane’s book and trying out the examples in Script Editor, I found my brain kept rebelling and switching off because there was so much I didn’t like about the monolithic appearance of the code on the screen. On the basis that the best way to learn anything is to make it your own, I spent a couple of days thinking how I might do this with ASObjC and eventually decided that for optimum clarity, narrative, and consistency in my own scripts, I’d depart from Shane’s style in a number of ways:

• Using a glyph variable instead of congesting the screen with 'current application’s.
• Specifying classes as the keyword ‘class’ followed by the class name as text. Shane’s book gives this as one way of getting round a clash between NSURL and a similar keyword in one of the Satimage OSAXen. My logic is that doing it this way every time means that NSURL doesn’t have to be special-cased and classes stand out from methods on the page. (ASObjCExplorer and Script Debugger have a facility for changing the colour of method keywords to make them stand out from class keywords, but this isn’t much help in Script Editor or on scripting fora. It also changes the colour of handler labels (except for handlers with labelled parameters), but it’s not too irritating if you choose the right colour.)
• Parenthesising all values passed to methods, not just those that won’t compile otherwise, so that they break up the monotony and are easily visible.
• Using ‘tell’ (unless it makes more sense not to) where ‘set’ can’t be, in order to convey the impression of something happening.
• Not using interleaved syntax for AppleScript handlers unless it’s absolutely unavoidable.

These are just my personal preferences, of course, based on my own ideas of clarity and how AS and ASObjC should coexist in a script. (And I did find ASObjC more interesting and easier to learn after adopting them! ;)) But if I’m commenting on or further developing someone else’s script, I’ll generally try to adopt their style — both out of politeness and so that they can follow the changes.

OK. I won’t answer it. :wink: beginning and end are properties of a list, which may explain why getting them is minusculely faster than getting the elements item 1 and item -1.

I’ll try out your improved script in the morning. :slight_smile:

Nigel,

Thanks again for all the helpful pieces of information.

I can especially relate to your willingness to be a bit verbose if it promotes consistency and clarity. For example, I use tell blocks that might otherwise be expressed in fewer lines without the block for the validation of a handler’s input variables, where I delineate each variable’s validation code within a tell [variable name]…end tell block whenever feasible. I find it to be a great organizational tool that promotes clarity. Another mildly verbose habit I’ve developed that I find extremely helpful is to use an Applescript record as the single input parameter and single output parameter for virtually all my handlers. (I break the rule when posting online because positional parameters seem to be so much more commonly used.) The use of a single record is not only consistent across all handlers, but the record property labels afford the opportunity to give expressive names to each input and output property reminiscent of the expressive names used for Cocoa entities. Another benefit is the ease with which default values can be supplied to individual input properties by simply concatenating a record with the default values to the end of the input record. Yet another benefit is that an arbitrary number of input or output properties may be specified, including zero properties by coding the record as {} (i.e., the empty list/record.)

One final thing that I’m sure you’ve noticed…I quickly adopted your use of the glyph variable || in place of current application in my ASObjC code. It’s so-o-o-o much easier to both read and write!

FWIW, I did ask one of the then AppleScript engineers some time ago whether there was any difference between referring to classes by name or a variable. The answer was that by variable was now the “preferred” way, but I couldn’t get any more response than that. I suspect it makes little difference, although using names initially caused some problems because the AppleScript compiler had never accepted named classes – editors have to include some vaguely hackish code to get it to compile.

The issue of current application is a vexed question. I initially tried using other variables, and even having it so they didn’t actually appear in code. I also pleaded for alternatives that were at least shorter and one word, obviously to no avail. My impression was there there was a hope that they could eventually be made unnecessary, but that may have just been my wishful thinking.

My ultimate preference is to define them in properties, but that doesn’t translate to places like this very well. But especially in the case of enums, it is much safer because so many enums have been renamed in recent releases.

Wearing my Script Debugger hat, the || approach doesn’t play well with code-completion. If you’re not using Script Debugger, or you simply don’t care for code-completion, it doesn’t matter. I toyed with the idea of supporting it as an alternative, but the choice of a non-ASCII character helped dissuade me for technical reasons. It’s also a bit of a slippery slope because I’ve seen others preferring different schemes.

My feeling is that whoever supplies the code gets to use their preference. But I also think there’s something to be said, at least in places like this particular section, to keeping things reasonably vanilla unless there are reasons of performance involved.

That’s using my “style”, of course. If you prefer the variable syntax, it would just be NSSet, as in:

else if (its isKindOfClass:(||'s NSSet)) as boolean then

I haven’t been able to catch it out so far. :slight_smile: To make things as pathological as possible, I set the NSOrderedSet first, made it an additional member of the NSSet, and made both additional values in the NSDictionary. Everything was correctly rendered. :slight_smile:

I did notice that. :slight_smile:

I’m not a great fan of text completion generally, although I find it quite handy in Numbers. Obviously I don’t use it in Script Debugger, but I do use text substitutions to mitigate the use of || (or at the moment |⌘|) and the class “name” syntax and could theoretically use it to enter commonly used methods whose use I didn’t have to look up anyway.

That’s good feedback to get. While the handler started out as a means of converting Cocoa NSSet’s to Applescript lists, it quickly evolved into a what I hope is a general purpose Cocoa object-to-Applescript value converter.

I wasn’t either, until I started with Objective-C. Those long names and case-sensitivity made me a convert.

PS.

It doesn’t give me those floating-point inaccuracies in the results:

--> {{1, 2.2, "three"}, {a:false, b:{4, 5, 6}, c:{bb:8, cc:9, aa:7}}, {"twelve", 10, 11.11}, {97, 98.98, "ninety-nine"}, {{1, 2.2, "three"}, {a:false, b:{4, 5, 6}, c:{bb:8, cc:9, aa:7}}, {"twelve", 10, 11.11}, {97, 98.98, "ninety-nine"}, {{1, 2.2, "three"}, {a:false, b:{4, 5, 6}, c:{bb:8, cc:9, aa:7}}, {"twelve", 10, 11.11}, {97, 98.98, "ninety-nine"}, {{1, 2.2, "three"}, {a:false, b:{4, 5, 6}, c:{bb:8, cc:9, aa:7}}, {"twelve", 10, 11.11}, {97, 98.98, "ninety-nine"}}}}}

That happens before 10.11 – doubles get converted to floats, losing precision. Of course several other classes are also not converted pre-10.11.

It’s time for me to get up with the times. I dread the tweaks that will be necessary in the 100+ utility handlers that are at the core of my automations. But when it’s over, I’m sure it will be delightful.

From an ASObjC point of view 10.11 or later is more reliable, and the bridging of dates is a great leap forward.

I can’t test this at the moment, but I wonder if something else is involved. Let me explain…

In your previous example:

set {theResult, isFile} to (anItem's getResourceValue:(reference) forKey:theKey |error|:(missing value))

the call to getResourceValue:forKey: returns an AppleScript list like this:

{true, (NSNumber) YES}

That’s because the AppleScript bridge has sees the method returns a boolean, and therefore converts theResult to an AppleScript boolean, but it has no way of telling what class the key should be, so it passes the Objective-C value unconverted as an NSNumber.

However, here:

the bridge knows that the isKindOfClass: method returns a boolean, so it again does the conversion. (Early versions of ASObjC didn’t always do this conversion of booleans, so a coercion was a safe fallback.)

That suggest to me that you’re forcing an unnecessary conversion of AsObjCTrue each time. I’m surprised that that would be quicker than using “as boolean”, given you already have a boolean.

In any event, it should be quickest to do neither, but change:

if (its isKindOfClass:(my ||'s class "NSArray")) is AsObjCTrue then

to:

if (its isKindOfClass:(my ||'s class "NSArray")) then

How did I not pay attention to the fact that the returned value is already boolean !!!

My script just returned faster an erroneous result. :frowning: So, I just removed it.

I was making efforts to improve the speed of using data retrieved from an API using Applescript objC and tried the handler above and had 2 issues I need some advice with:

  1. When adding
property || : current application

to the beginning of the code and testing in Script Debugger it ended up making the run time many times slower. Is there any issue with leaving it out?

  1. The data I am trying to convert to AppleScript record is a NSDictionary containing NSArrays.

if I try:


set resultAPI to my cocoaToASValue(aRESTres)

the handler reports an error “No result was returned from some part of this expression.”

if I try:


set resultAPI to my cocoaToASValue(aRESTres) as record
set resultData to my cocoaToASValue(resultAPI)

the coercion takes forever and is pointless since it is already an AppleScript value.

Is there a faster method someone could recommend for converting NSDictionary containing NSArrays?

This is making me feel very stupid.

Trying to gain an understanding of NSSet and testing with the code as suggested:-

set || to current application
set mySet to ||'s NSSet's setWithArray:{1, "two", 3.3}
set myArray to mySet's allObjects()
set myList to myArray as list

But even the very simple:-

set mySet to current application's NSSet's setWithArray:{1, "two", 3.3}

just returns an error in Script Editor or Script Debugger:-

“NSSet doesn’t understand the “setWithArray_” message.” number -1708 from NSSet

I must be missing something very obvious as it all looks right to me and was copied directly from above.

So, a bit stumped. Anyone?

Hi UKenGB.

Most of the examples above are lazy excerpts from other scripts. To work, scripts using ASObjC must begin with declaration(s) of the system framework(s) which provide their classes and methods.

use framework "Foundation"

set || to current application
set mySet to ||'s NSSet's setWithArray:{1, "two", 3.3}
set myArray to mySet's allObjects()
set myList to myArray as list

Doh! Thanks. Got it working now.

wrong thread…