1

Testing in Playground I read a whole file in an array of String, one string per line. But what I need is a specific line only:

let dir = try? FileManager.default.url(for: .documentDirectory,
                                   in: .userDomainMask, appropriateFor: nil, create: true)
let fileURL = dir!.appendingPathComponent("test").appendingPathExtension("txt")
let text: [String] = try String(contentsOf: fileURL).components(separatedBy: NSCharacterSet.newlines)
let i = 2   // computed before, here to simplify
print(text[i])

There is a way to avoid reading the complete big file?

Luca_65
  • 123
  • 9
  • https://stackoverflow.com/questions/24581517/read-a-file-url-line-by-line-in-swift#24648951 – Price Ringo Oct 28 '17 at 17:49
  • You should try and search for your answers first before posting a new question. – Price Ringo Oct 28 '17 at 17:50
  • I'm not sure what you mean by "avoid reading the complete big file". You have an array of `String` objects. Is that the problem? You don't want to read the whole file into the array? Otherwise, your `text[i]` option would work, if you know the specific line number. Or, are you asking about having to loop through the array to find some line? And, if that's the case, please add how you would know you reached the line you want. – leanne Oct 28 '17 at 18:07
  • Thanks @Prince , I was not enough able to perform a successful search – Luca_65 Oct 29 '17 at 17:48
  • @leanne: I know in advance the line number, ´2´ in my example above. My code is working. I would avoid to load the whole file, which can be huge, but retrive the nth line only. – Luca_65 Oct 29 '17 at 17:52

2 Answers2

0

I'm guessing you mean that you want to retrieve the index without manually searching the array with, say, a for-in loop.

In Swift 4 you can use Array.index(where:) in combination with the StringProtocol's generic contains(_:) function to find what you're looking for.

Let's imagine you're looking for the first line containing the text "important stuff" in your text: [String] array.

You could use:

text.index(where: { $0.contains("important stuff") })

Behind the scenes, Swift is looping to find the text, but with built-in enhancements, this should perform better than manually looping through the text array.


Note that the result of this search could be nil if no matching lines are present. Therefore, you'll need to ensure it's not nil before using the result:

Force unwrap the result (risking the dreaded fatal error: unexpectedly found nil while unwrapping an Optional value):

print(text[lineIndex!)

Or, use an if let statement:

if let lineIndex = stringArray.index(where: { $0.contains("important stuff") }) {
    print(text[lineIndex])
}
else {
    print("Sorry; didn't find any 'important stuff' in the array.")
}

Or, use a guard statement:

guard let lineIndex = text.index(where: {$0.contains("important stuff")}) else {
    print("Sorry; didn't find any 'important stuff' in the array.")
    return
}
print(text[lineIndex])
leanne
  • 7,940
  • 48
  • 77
0

To find a specific line without reading the entire file in, you could use this StreamReader answer. It contains code that worked in Swift 3. I tested it in Swift 4, as well: see my GitHub repo, TEST-StreamReader, for my test code.

You would still have to loop to get to the right line, but then break the loop once you've retrieved that line.

Here's the StreamReader class from that SO answer:

class StreamReader  {

    let encoding : String.Encoding
    let chunkSize : Int
    var fileHandle : FileHandle!
    let delimData : Data
    var buffer : Data
    var atEof : Bool

    init?(path: String, delimiter: String = "\n", encoding: String.Encoding = .utf8,
          chunkSize: Int = 4096) {

        guard let fileHandle = FileHandle(forReadingAtPath: path),
            let delimData = delimiter.data(using: encoding) else {
                return nil
        }
        self.encoding = encoding
        self.chunkSize = chunkSize
        self.fileHandle = fileHandle
        self.delimData = delimData
        self.buffer = Data(capacity: chunkSize)
        self.atEof = false
    }

    deinit {
        self.close()
    }

    /// Return next line, or nil on EOF.
    func nextLine() -> String? {
        precondition(fileHandle != nil, "Attempt to read from closed file")

        // Read data chunks from file until a line delimiter is found:
        while !atEof {
            if let range = buffer.range(of: delimData) {
                // Convert complete line (excluding the delimiter) to a string:
                let line = String(data: buffer.subdata(in: 0..<range.lowerBound), encoding: encoding)
                // Remove line (and the delimiter) from the buffer:
                buffer.removeSubrange(0..<range.upperBound)
                return line
            }
            let tmpData = fileHandle.readData(ofLength: chunkSize)
            if tmpData.count > 0 {
                buffer.append(tmpData)
            } else {
                // EOF or read error.
                atEof = true
                if buffer.count > 0 {
                    // Buffer contains last line in file (not terminated by delimiter).
                    let line = String(data: buffer as Data, encoding: encoding)
                    buffer.count = 0
                    return line
                }
            }
        }
        return nil
    }

    /// Start reading from the beginning of file.
    func rewind() -> Void {
        fileHandle.seek(toFileOffset: 0)
        buffer.count = 0
        atEof = false
    }

    /// Close the underlying file. No reading must be done after calling this method.
    func close() -> Void {
        fileHandle?.closeFile()
        fileHandle = nil
    }
}

extension StreamReader : Sequence {
    func makeIterator() -> AnyIterator<String> {
        return AnyIterator {
            return self.nextLine()
        }
    }
}
leanne
  • 7,940
  • 48
  • 77