6 Replies
      Latest reply: Oct 4, 2016 12:56 PM by goldsdad RSS
      gethynb Level 1 Level 1 (0 points)

        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

        • Re: Iterating through a string
          eskimo Apple Staff Apple Staff (7,190 points)

          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"

            • Re: Iterating through a string
              gethynb Level 1 Level 1 (0 points)

              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

                • Re: Iterating through a string
                  goldsdad Level 4 Level 4 (580 points)

                  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"
                  
                    • Re: Iterating through a string
                      goldsdad Level 4 Level 4 (580 points)

                      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.

                    • Re: Iterating through a string
                      eskimo Apple Staff Apple Staff (7,190 points)

                      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"