Key value coding compliance error

I’ve been attempting to do something that probably can’t be done, but I thought I would ask just in case. The following script returns an error that the class is not key value coding compliant. Is there any way to get this to work without the repeat loop. Thanks.

use framework "Foundation"
use scripting additions

set theString to "This is a test line with a Test word"
set thePattern to "(?i)test"
set matchingData to getMatches(theString, thePattern)
on getMatches(theString, thePattern)
	set theString to current application's NSString's stringWithString:theString
	set theRegex to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
	set regexResults to theRegex's matchesInString:theString options:0 range:{location:0, |length|:theString's |length|()}
	set theRanges to (regexResults's valueForKey:"range")
	set theLocations to (theRanges's valueForKey:"location") -- returns error
	# set theLocations to {} -- this and following returns the desired result
	# repeat with aMatch in regexResults
	# set end of theLocations to (aMatch's range()'s location())
	# end repeat
	# return theLocations --> {10, 27}
end getMatches

Unfortunately it’s not possible. NSRange is not an object, only types which inherit from NSObject can be key value coding compliant.

If NSRange was KVC compliant you could even write valueForKey:"range.location"

1 Like

Is the value {10,27} you are looking for??..

use framework "Foundation"
use scripting additions

set theString to "This is a test line with a Test word"
set thePattern to "(?i)test"
set matchingData to getMatches(theString, thePattern)
on getMatches(theString, thePattern)
	set theString to current application's NSString's stringWithString:theString
	set theRegex to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
	set regexResults to theRegex's matchesInString:theString options:0 range:{location:0, |length|:theString's |length|()}
	set theRanges to (regexResults's valueForKey:"range")
	set {theLoc, theLen} to theRanges as list
	return {location of theLoc, location of theLen}
end getMatches

@StefanK
You mean valueForKeyPath…

Thanks Stefan for the explanation.

Thanks Fredrik71 for looking at my post. I want the script to return a list of the location of every instance of the search text (which is test). The string will vary in length and may have hundreds of matches of the search text.

Hey peavine,

It sound like he wants the routine i made in a previous post

No need to use ASObjC as it’s actually slower

Robert. I’m not sure who you are referring to?

My question was a purely technical one–which arose in the context of the thread you mention–and my question was whether key value coding could be used to get the locations from an array of NSRanges. Stefan answered my question and explained why this was not possible.

Maybe not the best approach but it give you… the location for the words test

Ps. @peavine I realized that you already had a solution… sorry

use framework "Foundation"
use scripting additions

set theString to "This is a test line with a Test word"
set thePattern to "(?i)test"
set matchingData to getMatches(theString, thePattern)
on getMatches(theString, thePattern)
	set theString to current application's NSString's stringWithString:theString
	set theRegex to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
	set regexResults to theRegex's matchesInString:theString options:0 range:{location:0, |length|:theString's |length|()}
	set theRanges to (regexResults's valueForKey:"range")
	log theRanges's |description|() as string
	set theObjects to {}
	repeat with i from 1 to count theRanges
		set theObject to (item i of theRanges) as list
		set the end of theObjects to theObject
	end repeat
	return theObjects
end getMatches

repeat with i from 1 to count matchingData
	log (item 1 of (item i of matchingData))'s location
end repeat

Or you could do… the repeat loop do not use any AppleScriptObjC calls, so it should be faster. I did a fast test with 100 words and it fast faster not to use AppleScriptObjC calls inside a repeat loop.

use framework "Foundation"
use scripting additions

set theString to "This is a test line with a Test word"
set thePattern to "(?i)test"
set matchingData to getMatches(theString, thePattern)
on getMatches(theString, thePattern)
	set theString to current application's NSString's stringWithString:theString
	set theRegex to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
	set regexResults to theRegex's matchesInString:theString options:0 range:{location:0, |length|:theString's |length|()}
	set theRanges to (regexResults's valueForKey:"range") as list
	set theLocations to {}
	repeat with i from 1 to count theRanges
		set theObject to (item i of theRanges)
		set theLoc to theObject's location
		set the end of theLocations to theLoc
	end repeat
	return theLocations
end getMatches

RegEx has a function to Enumerate it’s matches, which are TextChecking results:

If a capture group match is not found the range with be nil.

I don’t know if NSTextCheckingResult is a subclass if NSOBject, NSRange definitely is not.

Hi, seems easier to use shell

hi,
seems easier with shell:

set theString to “This is a test line with a Test word”
set thePattern to “(?i)test”
set matchingData to getMatches(theString, thePattern)

on getMatches(str, pattern)
set shellcmd to "echo " & qt(str) & "| grep -bEo " & qt(pattern)
set res to (do shell script shellcmd)
set off to offset of “:” in res
return {text 1 thru (off - 1) of res as integer, count of (characters (off + 1) thru -1 of res)}
end getMatches
on qt(str)
return “"” & str & “"”
end qt

Hallenstal. Thanks for responding to my thread. I tested your script but it did not seem to return the expected results, which were {10, 27}. I am not knowledgeable with grep so perhaps I’m doing something wrong. BTW, your script threw an error as written and I had to escape the quote in the qt handler.

set theString to "This is a test line with a Test word"
set thePattern to "(?i)test"
set matchingData to getMatches(theString, thePattern) --> {10, 12}

on getMatches(str, pattern)
	set shellcmd to "echo " & qt(str) & "| grep -bEo " & qt(pattern)
	set res to (do shell script shellcmd)
	set off to offset of ":" in res
	return {text 1 thru (off - 1) of res as integer, count of (characters (off + 1) thru -1 of res)}
end getMatches

on qt(str)
	return "\"" & str & "\""
end qt

@peavine

Try this:

-- Tested on Monterey 12.6.3
use framework "Foundation"
use scripting additions

set theString to "This is a test line with a Test word"
set thePattern to "(?i)test"
set matchingData to getMatches(theString, thePattern)

on getMatches(theString, thePattern)
	set theString to current application's NSString's stringWithString:theString
	set theRegex to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
	set regexResults to theRegex's matchesInString:theString options:0 range:{location:0, |length|:theString's |length|()}
	set theRanges to (regexResults's valueForKey:"range") as list
	set theArray to current application's NSArray's arrayWithArray:theRanges
	set theLocations to (theArray's valueForKey:"location")
	return theLocations as list --> {10, 27}
end getMatches

Your script do not work with this string “This is a test line with a Test word other test”

@Hallenstal shell is working. It’s just the parsing method that is wrong.

set theString to "This is a test line with a Test word other test"
set thePattern to "(?i)test"
set theLocations to getLocations(theString, thePattern) --> {"10", "27", "43"}

on getLocations(str, pattern)
	set res to do shell script ("echo " & quoted form of (str) & "| grep -bEo " & quoted form of (pattern))
	return paragraphs of (do shell script ("echo " & quoted form of (res) & "| grep -Eo " & quoted form of ("\\d+")))
end getLocations

@ionah. Many thanks for your suggestions, both of which work great.

It was mentioned above that my original request arose from another thread (see link below), which had to do with timing results in finding 1288 instances of a substring in a string. I reran these tests with @ionah’s two suggestions and the results were as follows (all times are milliseconds):

ionah’s Grep script - 76
robertfern’s AppleScript script - 116
ionah’s ASObjC script - 160
peavine’s ASObjC script - 262

Although not of much (or any) significance, there are a few differences in the results returned by the script suggestions:

  • The substring locations in the grep and ionah’s ASObjC script are zero-based.
  • The grep script returns numbers as text; the other suggestions return integers.

The thread mentioned above can be found here

@peavine

I don’t know what method you’re using to get those results.
Here are the ones I get with Script Geek:

MacPro6.1, macOS Version 12.6.3 (21G419), 100 iterations

First Run Total Time Average
AppleScriptObjC 0.408 0.078 0.001
Shell grep 0.016 1.263 0.013

Ratio (excluding first run): 1:16.27

@peavine
The issue with the type ‘string’ for do shell script is when paragraph is used. It would be easy to convert item 1 of something to integer. Harder to convert paragraphs to integer without other repeat loop. Or AppleScript delimiter hack… I find the AppleScriptObjC version to be more simple and its far more easier to convert any type to anything integer, float or string.

1 Like

@ionah. I don’t use Script Geek in this particular instance because of the difficulty in getting a very large string without including the time it takes to get that string in the total timing results. My grep timing script:

use framework "Foundation"
use scripting additions

-- untimed code
set theString to "My Rob is a cool Robert! His name is Robert... "
repeat 12 times
	set theString to theString & theString
end repeat

-- start time
set startTime to current application's CACurrentMediaTime()

-- timed code
set thePattern to "(?i)Rob"
set theOffsets to getLocations(theString, thePattern)
on getLocations(str, pattern)
	set res to do shell script ("echo " & quoted form of (str) & "| grep -bEo " & quoted form of (pattern))
	return paragraphs of (do shell script ("echo " & quoted form of (res) & "| grep -Eo " & quoted form of ("\\d+")))
end getLocations

-- elapsed time
set elapsedTime to (current application's CACurrentMediaTime()) - startTime
set numberFormatter to current application's NSNumberFormatter's new()
if elapsedTime > 1 then
	numberFormatter's setFormat:"0.000"
	set elapsedTime to ((numberFormatter's stringFromNumber:elapsedTime) as text) & " seconds"
else
	(numberFormatter's setFormat:"0")
	set elapsedTime to ((numberFormatter's stringFromNumber:(elapsedTime * 1000)) as text) & " milliseconds"
end if

-- result
elapsedTime --> 76 milliseconds
# count theOffsets --> 12288
# theOffsets

My ASObjC timing script:

use framework "Foundation"
use scripting additions

-- untimed code
set theString to "My Rob is a cool Robert! His name is Robert... "
repeat 12 times
	set theString to theString & theString
end repeat

-- start time
set startTime to current application's CACurrentMediaTime()

-- timed code
set thePattern to "(?i)Rob"
set theOffsets to getMatches(theString, thePattern)
on getMatches(theString, thePattern)
	set theString to current application's NSString's stringWithString:theString
	set theRegex to current application's NSRegularExpression's regularExpressionWithPattern:thePattern options:0 |error|:(missing value)
	set regexResults to theRegex's matchesInString:theString options:0 range:{location:0, |length|:theString's |length|()}
	set theRanges to (regexResults's valueForKey:"range") as list
	set theArray to current application's NSArray's arrayWithArray:theRanges
	set theLocations to (theArray's valueForKey:"location")
	return theLocations as list
end getMatches

-- elapsed time
set elapsedTime to (current application's CACurrentMediaTime()) - startTime
set numberFormatter to current application's NSNumberFormatter's new()
if elapsedTime > 1 then
	numberFormatter's setFormat:"0.000"
	set elapsedTime to ((numberFormatter's stringFromNumber:elapsedTime) as text) & " seconds"
else
	(numberFormatter's setFormat:"0")
	set elapsedTime to ((numberFormatter's stringFromNumber:(elapsedTime * 1000)) as text) & " milliseconds"
end if

-- result
elapsedTime --> 160 milliseconds
# count theOffsets --> 12288
# theOffsets

To avoid this, you can build a large string in any text editor and copy it as a global variable in your script. This way the string will be loaded at compile time and will not be included in the time calculation.

As you can see, using a loop or not does not make a significant difference.

1 Like

@peavine
Its simple to covert array of strings to array of integers…

use framework "Foundation"

set arrayOfStrings to current application's NSArray's arrayWithArray:{"1", "2", "3"}
set integerValues to (arrayOfStrings's valueForKey:"intValue") as list

@ionah
Peavine script use AppleScriptObjC inside the repeat loop. The script you did test on was mine. I didn’t use AppleScriptObjC calls in the repeat loop and thats why it was faster. On my Test it was big difference. Your version… why did I not think about that :).