Replacement for paragraphRangeForRange method

I’m wrote a handler that is functionally equivalent to NSString’s paragraphRangeForRange method, except the handler works with characters (e.g.emojis). I wondered if anyone knows a simpler method to do this. Thanks!

set theString to "This is line x
This is line xx
This is line xxx
This is line xxxx"

set paragraphRange to getParagraphRange(theString, 25, 10) --25 and 10 are range location and length

on getParagraphRange(theString, stringStart, stringLength)
	set stringEnd to stringStart + stringLength
	set {TID, text item delimiters} to {text item delimiters, linefeed}
	set theParagraphs to text items of theString
	set {paragraphStart, paragraphEnd, paragraphsLength} to {(missing value), (missing value), 0}
	repeat with aParagraph in theParagraphs
		set aParagraphLength to (count aParagraph) + 1
		set paragraphsLength to paragraphsLength + aParagraphLength
		if paragraphStart is (missing value) and stringStart is less than paragraphsLength then
			set paragraphStart to (paragraphsLength - aParagraphLength)
		end if
		if paragraphStart is not (missing value) and stringEnd is less than or equal to paragraphsLength then
			set paragraphEnd to paragraphsLength
			exit repeat
		end if
	end repeat
	set text item delimiters to TID
	return {paragraphStart, paragraphEnd - paragraphStart}
end getParagraphRange

I’ve included below a script I used for comparison testing, and the tested scripts seem to return identical results. I also tested the new script with emojis in theString and the emojis are counted as one character as desired.

I also tested the two scripts with Script Geek. The test string contained 100 lines and the substring was near the end of these strings. The timing results with both scripts was about 0.3 millisecond.

use framework "Foundation"
use scripting additions

set theString to "This is line x
This is line xx
This is line xxx
This is line xxxx
This is line xxxxx
"

set stringStart to 1
set stringLength to 48

set {s, l} to getParagraphRangeOne(theString, stringStart, stringLength)
set {ss, ll} to getParagraphRangeTwo(theString, stringStart, stringLength)

return (s as text) & space & l & space & ss & space & ll

on getParagraphRangeOne(theString, stringStart, stringLength)
	set stringEnd to stringStart + stringLength
	set {TID, text item delimiters} to {text item delimiters, linefeed}
	set theParagraphs to text items of theString
	set {paragraphStart, paragraphEnd, paragraphsLength} to {(missing value), (missing value), 0}
	repeat with aParagraph in theParagraphs
		set aParagraphLength to (count aParagraph) + 1
		set paragraphsLength to paragraphsLength + aParagraphLength
		if paragraphStart is (missing value) and stringStart is less than paragraphsLength then
			set paragraphStart to (paragraphsLength - aParagraphLength)
		end if
		if paragraphStart is not (missing value) and stringEnd is less than or equal to paragraphsLength then
			set paragraphEnd to paragraphsLength
			exit repeat
		end if
	end repeat
	set text item delimiters to TID
	return {paragraphStart, paragraphEnd - paragraphStart}
end getParagraphRangeOne

on getParagraphRangeTwo(theString, cr1, cr2)
	set theString to current application's NSString's stringWithString:theString
	set paragraphRange to theString's paragraphRangeForRange:{cr1, cr2}
	return {paragraphRange's location, paragraphRange's |length|}
end getParagraphRangeTwo

Hi @peavine. I think this does what you want:

set theString to "This is line 😀
This is line xx
This is line xxx
This is line xxxx
This is line xxxxx
"

set rangeStart to 1
set rangeLength to 48

set {s, l} to getParagraphRange(theString, rangeStart, rangeLength)
return text (s + 1) thru (s + l) of theString

on getParagraphRange(theString, rangeStart, rangeLength) -- rangeStart is a 0-based index.
	set rangeEnd to rangeStart + rangeLength -- Use 1-based indexing until exit.
	set rangeStart to rangeStart + 1
	set stringLength to (count theString)
	if ((rangeStart < 1) or (rangeEnd > stringLength)) then error
	if (rangeStart > 1) then set rangeStart to rangeStart - (count theString's text 1 thru (rangeStart - 1)'s last paragraph)
	if (rangeEnd < stringLength) then set rangeEnd to rangeEnd + (count theString's text (rangeEnd + 1) thru end's first paragraph)
	return {rangeStart - 1, rangeEnd - rangeStart + 1} -- 0-based output
end getParagraphRange
1 Like

Thanks Nigel–that’s an excellent solution. I tested it with Script Geek, and it took 0.3 millisecond, which is the same as the other scripts. However, your suggestion is much more compact and is the script I will use.

Nigel. I worked through your script just to make sure I understood everything, but I’m not familiar with the use of end. I reviewed the documentation, and if I understand correctly end makes first paragraph refer back to (rangeEnd + 1) instead of the first paragraph of theString, but I wasn’t sure. Also, can this be written without using the possessive form of end. Thanks!

set theString to "This is line x
This is line xx
This is line xxx"

set rangeEnd to 23
set aSubstring to text (rangeEnd + 1) thru end's first paragraph of theString -->"line xx"

Hi peavine.

Sorry to have put you through that. I think I got a bit carried away with “Saxon genitives” and other stuff. Some parenthesis might have made things clearer too.

In most cases when reading text and lists, end and beginning can be used in range specifiers instead of -1 and 1.

theString's text (rangeEnd + 1) thru end's first paragraph

… is the same as:

(theString's text (rangeEnd + 1) thru -1)'s first paragraph

… or:

paragraph 1 of (text (rangeEnd + 1) thru -1 of theString)

Hope this clarifies things. :slightly_smiling_face:

1 Like

Thanks Nigel. That makes everything clear.