Count the number of lines in a Swift String

Viewed 8455

After reading a medium sized file (about 500kByte) from a web-service I have a regular Swift String (lines) originally encoded in .isolatin1. Before actually splitting it I would like to count the number of lines (quickly) in order to be able to initialise a progress bar.

What is the best Swift idiom to achieve this?

I came up with the following:

let linesCount = lines.reduce(into: 0) { (count, letter) in
   if letter == "\r\n" {
      count += 1
   }
}

This does not look too bad but I am asking myself if there is a shorter/faster way to do it. The characters property provides access to a sequence of Unicode graphemes which treat \r\n as only one entity. Checking this with all CharacterSet.newlines does not work, since CharacterSet is not a set of Character but a set of Unicode.Scalar (a little counter-intuitively in my book) which is a set of code points (where \r\n counts as two code points), not graphemes. Trying

var lines = "Hello, playground\r\nhere too\r\nGalahad\r\n"
lines.unicodeScalars.reduce(into: 0) { (cnt, letter) in
if CharacterSet.newlines.contains(letter) {
    cnt += 1
}

}

will count to 6 instead of 3. So this is more general than the above method, but it will not work correctly for CRLF line endings.

Is there a way to allow for more line ending conventions (as in CharacterSet.newlines) that still achieves the correct result for CRLF? Can the number of lines be computed with less code (while still remaining readable)?

5 Answers

If it's ok for you to use a Foundation method on an NSString, I suggest using

enumerateLines(_ block: @escaping (String, UnsafeMutablePointer<ObjCBool>) -> Void)

Here's an example:

import Foundation

let base = "Hello, playground\r\nhere too\r\nGalahad\r\n"
let ns = base as NSString

ns.enumerateLines { (str, _) in
    print(str)
}

It separates the lines properly, taking into account all linefeed types, such as "\r\n", "\n", etc:

Hello, playground
here too
Galahad

In my example I print the lines but it's trivial to count them instead, as you need to - my version is just for the demonstration.

Swift 5 Extension

extension String {
    
    func numberOfLines() -> Int {
        return self.numberOfOccurrencesOf(string: "\n") + 1
    }

    func numberOfOccurrencesOf(string: String) -> Int {
        return self.components(separatedBy:string).count - 1
    }
}

Example:

let testString = "First line\nSecond line\nThird line"
let numberOfLines = testString.numberOfLines() // returns 3

You can use the following extension

extension String {

    var numberOfLines: Int {
        return self.components(separatedBy: "\n").count
    }

}

I use this, a CharacterSet which Apple provides, made for this task:

let newLines = text.components(separatedBy: .newlines).count - 1
Related