Iterating through a string

Hi Guys,


Having decided to start looking at swift I decided to translate an old unfinished project from Objective C. I have some code translated (that is working nicely) but uses integers to iterate over a string:


let text = "this is a piece \n of text \n over multiple lines."
let cursorPosition = 17

var startIndex = 0
if (cursorPosition > 0){

    for counter in stride(from: cursorPosition, through:0, by: -1){

        startIndex = counter
        var index = text.index(text.startIndex, offsetBy:counter)

        if (text[index] == "\n") {
            break
        }
    }
}


However, I have read that this is really inefficient as each time the index is found (line 10) swift iterates through the whole string.


Do you any advice on how I could improve the efficiency of the code ?


Best Regards


Gethyn

Accepted Reply

Or as an extension to String:


import Foundation // required for lineRange(for:) on String

extension String {
    func lineRange(atPosition position: Int) -> Range<String.Index> {
        let i = index(startIndex, offsetBy: position)
        return lineRange(for: i ..< i)
    }
}


let text = "This is a piece \nof text \nover multiple lines."
let cursorPosition = 17 // immediately after first newline character
let line = text[text.lineRange(atPosition: cursorPosition)] // "of text \n"



Regarding line 6, the range (i ..< i) instead of (i ..< index(after: i)) may look strange because it's an empty range, but it allows for a possible cursor position immediately after the last character in the text.


Using the extension or previous function in IBM Sandbox does not produce expected results; blame its Linux Foundation implementation, I guess.

Replies

The immediately answer to your question is to use

index(after:)
. For example:
let s = "Hello Cruel World!"
var i = s.startIndex
while i != s.endIndex {
    print(s[i])
    i = s.index(after: i)
}

However, iterating through the characters in a string is almost always the wrong thing to do. When dealing with strings, you have to work at a higher level. For example, the code you posted seems like it’s trying to find the first line break in the string. You can do this with code like this:

import Foundation

let firstLineBreakIndex = s.range(of: "\n")!.lowerBound
print(s[s.startIndex..<firstLineBreakIndex])

but even that is probably not the right join. For example, if just want to get the first line, the following seems clearer to me:

print(s.components(separatedBy: "\n").first!)

but even that’s broken in the presence of not standard line breaks and you’d want to use this instead:

s.enumerateLines { (thisline, stop) in
    print(thisline)
    stop = true
}

If you give a high-level description of your intentions, we should be able to suggest the right approach.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi Quinn,

Thank you for the examples you have shared, you asked about the high level description of what the requirement is :


I have an NSTextView that the user is typing stuff into, I want to be able to get just the text on the particular line where the cursor currently is.


I have done this by getting the cursor position and iterating backwards till I either run out of text, or find a \n, this is the start index of the current line. Next, I do the same thing but iterate forwards from the cursor position to the end of the text again until I get \n or the end, this gives me the end index.


With the start and end index values I can now get the substring that is the line of text on which the cursor is found.


Interested in hearing your ideas.


Gethyn

Maybe this function will do the trick:


func rangeOfLine(atPosition position: Int, in string: String) -> Range<String.Index> {
    let index = string.index(string.startIndex, offsetBy: position)
    return string.lineRange(for: index ..< index)
}


let text = "This is a piece \nof text \nover multiple lines."

let cursorMin = 0                                       // immediately before the first character
let cursorMid = 17                                      // immediately after first newline character
let cursorMax = text.characters.count                   // immediately after the last character

let firstLineRange = rangeOfLine(atPosition: cursorMin, in: text)
let middleLineRange = rangeOfLine(atPosition: cursorMid, in: text)
let lastLineRange = rangeOfLine(atPosition: cursorMax, in: text)

let firstLine = text[firstLineRange]                                        // "This is a piece \n"
let middleLine = text[middleLineRange]                                      // "of text \n"
let lastLine = text[lastLineRange]                                          // "over multiple lines."

let trimmed1 = firstLine.trimmingCharacters(in: .newlines)                  // "This is a piece "
let trimmed2 = firstLine.trimmingCharacters(in: .whitespacesAndNewlines)    // "This is a piece"

Or as an extension to String:


import Foundation // required for lineRange(for:) on String

extension String {
    func lineRange(atPosition position: Int) -> Range<String.Index> {
        let i = index(startIndex, offsetBy: position)
        return lineRange(for: i ..< i)
    }
}


let text = "This is a piece \nof text \nover multiple lines."
let cursorPosition = 17 // immediately after first newline character
let line = text[text.lineRange(atPosition: cursorPosition)] // "of text \n"



Regarding line 6, the range (i ..< i) instead of (i ..< index(after: i)) may look strange because it's an empty range, but it allows for a possible cursor position immediately after the last character in the text.


Using the extension or previous function in IBM Sandbox does not produce expected results; blame its Linux Foundation implementation, I guess.

gethynb wrote:

I have an NSTextView that the user is typing stuff into, I want to be able to get just the text on the particular line where the cursor currently is.

The easiest option is

lineRange(for:)
.
import Foundation

let s = "Hello\nCruel\nWorld!"
let r = s.range(of: "u")!
print(s.substring(with: s.lineRange(for: r)))
// prints "Cruel\n"

You can also look at:

  • paragraphRange(for:)
  • getLineStart(_:end:contentsEnd:for:)
  • getParagraphStart(_:end:contentsEnd:for:)
  • and the various

    enumerate
    routines

goldsdad wrote:

func lineRange(atPosition position: Int) -> Range<String.Index>

Be careful here:

  • In this example

    position
    is relative to the character view of
    String
    . However, if you’re working with NSTextView then the chances are that you’re getting the position relative to an NSString, which is equivalent to the UTF-16 view of
    String
    . These are not equivalent, although they’re equivalent often enough that you might not notice the difference in your initial testing.
  • Converting from character view indexes to UTF-16 view indexes is a linear operation in general, so it’s best to avoid it where you can. I generally recommend that you survey your use of these views, decide on the one that makes the most sense for you, and then use it consistently, only doing minimal conversions as required by the OS.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for the advice. 🙂