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
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
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"
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)