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?
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