How to read a specific file's line in swift4? - swift

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?

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

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()
}
}
}

Related

Convert list of AppleScript strings to a Swift array

I have a complicated AppleScript that returns a list of strings that I need to access from Swift. I've boiled it down to a simple example and I just can't figure out how to map the AppleScript strings to an array of Swift strings.
let listOfStringsScript = """
set listOfStrings to { "one", "two", "three" }
"""
if let scriptObject = NSAppleScript(source: listOfStringsScript) {
var errorDict: NSDictionary? = nil
let resultDescriptor = scriptObject.executeAndReturnError(&errorDict)
if errorDict == nil {
// TODO: convert the resultDescriptor (NSAppleEventDescriptor) into an array of strings
print(resultDescriptor)
// OUTPUT: <NSAppleEventDescriptor: [ 'utxt'("one"), 'utxt'("two"), 'utxt'("three") ]>
}
}
Answer with help from #Alexander and #MartinR:
extension NSAppleEventDescriptor {
func toStringArray() -> [String] {
guard let listDescriptor = self.coerce(toDescriptorType: typeAEList) else {
return []
}
return (0..<listDescriptor.numberOfItems)
.compactMap { listDescriptor.atIndex($0 + 1)?.stringValue }
}
}
...
let resultDescriptor = scriptObject.executeAndReturnError(&errorDict)
let subjectLines = resultDescriptor.toStringArray()
An alternative is to gather the Apple Script result as lines of text separated by line breaks and then parse the string in Swift.
So break up the Apple Script result using
set AppleScript's text item delimiters to linefeed
Then simply parse
let selectedItems = scriptExecuted.stringValue!
let selectedItemsFiltered = selectedItems.components(separatedBy: .newlines)
.components returns a string array

Using Swift to write a character to a file

I'm trying to write a Swift program that writes a single character to a file. I've researched this but so far haven't figured out how to do this (note, I'm new to Swift). Note that the text file I'm reading and writing to can contain a series of characters, one per line. I want to read the last character and update the file so it only contains that last character.
Here's what I have so far:
let will_file = "/Users/willf/Drobox/foo.txt"
do {
let statusStr = try String(contentsOfFile: will_file, encoding: .utf8)
// find the last character in the string
var strIndex = statusStr.index(statusStr.endIndex, offsetBy: -1)
if statusStr[strIndex] == "\n" {
// I need to access the character just before the last \n
strIndex = statusStr.index(statusStr.endIndex, offsetBy: -2)
}
if statusStr[strIndex] == "y" {
print("yes")
} else if statusStr[strIndex] == "n" {
print("no")
} else {
// XXX deal with error here
print("The char isn't y or n")
}
// writing
// I get a "cannot invoke 'write with an arg list of type (to: String)
try statusStr[strIndex].write(to: will_file)
}
I would appreciate advice on how to write the character returned by statusStr[strIndex].
I will further point out that I have read this Read and write a String from text file but I am still confused as to how to write to a text file under my Dropbox folder. I was hoping that there was a write method that could take an absolute path as a string argument but I have not found any doc or code sample showing how to do this that will compile in Xcode 9.2. I have also tried the following code which will not compile:
let dir = FileManager.default.urls(for: .userDirectory, in: .userDomainMask).first
let fileURL = dir?.appendingPathComponent("willf/Dropbox/foo.txt")
// The compiler complains about extra argument 'atomically' in call
try statusStr[strIndex].write(to: fileURL, atomically: false, encoding: .utf8)
I have figured out how to write a character as a string to a file thanks to a couple answers on stack overflow. The key is to coerce a character type to a string type because the string object supports the write method I want to use. Note that I used both the answers in Read and write a String from text file and in Swift Converting Character to String to come up with the solution. Here is the Swift code:
import Cocoa
let will_file = "/Users/willf/Dropbox/foo.txt"
do {
// Read data from will_file into String object
let statusStr = try String(contentsOfFile: will_file, encoding: .utf8)
// find the last character in the string
var strIndex = statusStr.index(statusStr.endIndex, offsetBy: -1)
if statusStr[strIndex] == "\n" {
// I need to access the character just before the last \n
strIndex = statusStr.index(statusStr.endIndex, offsetBy: -2)
}
if statusStr[strIndex] != "n" && statusStr[strIndex] != "y" {
// XXX deal with error here
print("The char isn't y or n")
}
// Update file so it contains only the last status char
do {
// String(statusStr[strIndex]) coerces the statusStr[strIndex] character to a string for writing
try String(statusStr[strIndex]).write(toFile: will_file, atomically: false, encoding: .utf8)
} catch {
print("There was a write error")
}
} catch {
print("there is an error!")
}

How can I bring in data from a text file on disk without adding it to the project?

I'm trying to read in a file from disk and parse its data into a nice format. However, the function is not returning anything. It returns an empty array. Why is this?
Note: I've been tinkering around with this and I've managed to simplify the previous 2 functions to just this one.
The function:
func openFile(_ fileName:String, _ fileType:String) -> [(Double, Double)] {
let file = fileName + fileType //this is the file. we will write to and read from it
var text2: String = ""
if let dir = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first {
let path = dir.appendingPathComponent(file)
//reading
do {
text2 = try String(contentsOf: path, encoding: String.Encoding.utf8)
}
catch {/* error handling here */}
}
var pairs = [(Double,Double)]()
var words: [String] = []
for line in (text2.components(separatedBy: "\n").dropFirst()){
if line != "" {
words = line.components(separatedBy: "\t")
pairs.append((Double(words[0])!,Double(words[1])!))
}
}
return pairs
}

Reading a string char by char is very slow in my swift implementation

i have to read a file char by char in swift. The way I am doing it is to read a chunk from a FileHandler and returning the first character of a string.
This is my code so far:
/// Return next character, or nil on EOF.
func nextChar() -> Character? {
precondition(fileHandle != nil, "Attempt to read from closed file")
if atEof {
return nil
}
if self.stored.characters.count > 0 {
let c: Character = self.stored.characters.first!
stored.remove(at: self.stored.startIndex)
return c
}
let tmpData = fileHandle.readData(ofLength: (4096))
print("\n---- file read ---\n" , terminator: "")
if tmpData.count == 0 {
return nil
}
self.stored = NSString(data: tmpData, encoding: encoding.rawValue) as String!
let c: Character = self.stored.characters.first!
self.stored.remove(at: stored.startIndex)
return c
}
My problem with this is that the returning of a character is very slow.
This is my test implementation:
if let aStreamReader = StreamReader(path: file) {
defer {
aStreamReader.close()
}
while let char = aStreamReader.nextChar() {
print("\(char)", terminator: "")
continue
}
}
even without a print it took ages to read the file to the end.
for a sample file with 1.4mb it took more than six minutes to finish the task.
time ./.build/debug/read a.txt
real 6m22.218s
user 6m13.181s
sys 0m2.998s
Do you have an opinion how to speed up this part?
let c: Character = self.stored.characters.first!
stored.remove(at: self.stored.startIndex)
return c
Thanks a lot.
ps
++++ UPDATEED FUNCTION ++++
func nextChar() -> Character? {
//precondition(fileHandle != nil, "Attempt to read from closed file")
if atEof {
return nil
}
if stored_cnt > (stored_idx + 1) {
stored_idx += 1
return stored[stored_idx]
}
let tmpData = fileHandle.readData(ofLength: (chunkSize))
if tmpData.count == 0 {
atEof = true
return nil
}
if let s = NSString(data: tmpData, encoding: encoding.rawValue) as String! {
stored = s.characters.map { $0 }
stored_idx = 0
stored_cnt = stored.count
}
return stored[0];
}
Your implementation of nextChar is terribly inefficient.
You create a String and then call characters over and over and you update that set of characters over and over.
Why not create the String and then only store a reference to its characters. And then track an index into characters. Instead of updating it over and over, simply increment the index and return the next character. No need to update the string over and over.
Once you get to the last character, read the next piece of the file. Create a new string, reset the characters and the index.

streamReader for server URLs

I've been working with the code form this answer, provided by Martin R. The code is awesome and it is very useful. However, it doesn't work with the links, while working fine with files. After putting some NSLogs and breaks, I have actually found that problem is in this code block:
init?(path: String, delimiter: String = "\n", encoding: UInt = NSUTF8StringEncoding, chunkSize : Int = 4096) {
self.chunkSize = chunkSize
self.encoding = encoding
self.fileHandle = NSFileHandle(forReadingFromURL: NSURL(string: path)!, error: nil)
println("PATH IS \(path)")
println("FILE HANDLE IS \(fileHandle)")
if self.fileHandle == nil {
println("FILE HANDLE IS NIL!")
return nil
}
The code above actually contains some minor changes, compared with Martin's answer. Also Apple says that it is possible to use fileHandle with forReadingFromURL and it shouldn't return nil.
But here is the console output:
PATH IS http://smth.com
FILE HANDLE IS nil
FILE HANDLE IS NIL!!!!!
The question is what's wrong?
UPDATE
As Martin R has kindly explained to me, this code won't work with URLs, this answer states the same, so I have rewritten the code, guiding by previous answers:
import Foundation
import Cocoa
class StreamReader {
let encoding : UInt
let chunkSize : Int
var atEof : Bool = false
var streamData : NSData!
var fileLength : Int
var urlRequest : NSMutableURLRequest
var currentOffset : Int
var streamResponse : NSString
var fileHandle : NSFileHandle!
let buffer : NSMutableData!
let delimData : NSData!
var reponseError: NSError?
var response: NSURLResponse?
init?(path: NSURL, delimiter: String = "\n", encoding: UInt = NSUTF8StringEncoding, chunkSize : Int = 10001000) {
println("YOUR PATH IS \(path)")
self.chunkSize = chunkSize
self.encoding = encoding
self.currentOffset = 0
urlRequest = NSMutableURLRequest(URL: path)
streamData = NSURLConnection.sendSynchronousRequest(urlRequest, returningResponse:&response, error:&reponseError)
streamResponse = NSString(data:streamData!, encoding:NSUTF8StringEncoding)!
self.fileLength = streamData.length
//println("WHAT IS STREAMDATA \(streamData)")
//println("WHAT IS URLREQUEST \(urlRequest)")
if streamData == nil {
println("LINK HAS NO CONTENT!!!!!")
}
self.fileLength = streamResponse.length
println("FILE LENGTH IS \(fileLength)")
self.buffer = NSMutableData(capacity: chunkSize)!
// Create NSData object containing the line delimiter:
delimData = delimiter.dataUsingEncoding(NSUTF8StringEncoding)!
println("WHAT DOES THE DELIMITER \(delimiter)LOOK LIKE?")
println("WHAT IS DELIMDATA \(delimData)")
}
deinit {
self.close()
}
/// Return next line, or nil on EOF.
func nextLine() -> String? {
if atEof {
println("AT THE END OF YOUR FILE!!!")
return nil
}
// Read data chunks from file until a line delimiter is found:
if currentOffset >= fileLength {
return nil
}
var blockLength : Int = buffer.length
var range = buffer.rangeOfData(delimData, options: NSDataSearchOptions(0), range: NSMakeRange(currentOffset, blockLength))
//println("STREAM DATA \(streamData)")
println("RANGE IS \(range)")
while range.location == NSNotFound {
var nRange = NSMakeRange(currentOffset, chunkSize)
println("nRange is \(nRange)")
var tmpData = streamData.subdataWithRange(nRange)
//println("TMP data length \(tmpData.length)")
currentOffset += blockLength
//println("TMPDATA is \(tmpData)")
if tmpData.length == 0 {
// EOF or read error.
println("ERROR ????")
atEof = true
if buffer.length > 0 {
// Buffer contains last line in file (not terminated by delimiter).
let line = NSString(data: buffer, encoding: encoding);
buffer.length = 0
println("THE LINE IS \(line)")
return line
}
// No more lines.
return nil
}
buffer.appendData(tmpData)
range = buffer.rangeOfData(delimData, options: NSDataSearchOptions(0), range: NSMakeRange(0, buffer.length))
}
// Convert complete line (excluding the delimiter) to a string:
let line = NSString(data: buffer.subdataWithRange(NSMakeRange(0, range.location)),
encoding: encoding)
// Remove line (and the delimiter) from the buffer:
buffer.replaceBytesInRange(NSMakeRange(0, range.location + range.length), withBytes: nil, length: 0)
return line
}
/// Start reading from the beginning of file.
func rewind() -> Void {
//streamData.seekToFileOffset(0)
buffer.length = 0
atEof = false
}
/// Close the underlying file. No reading must be done after calling this method.
func close() -> Void {
if streamData != nil {
streamData = nil
}
}
}
extension StreamReader : SequenceType {
func generate() -> GeneratorOf<String> {
return GeneratorOf<String> {
return self.nextLine()
}
}
}
But actually this code is very far from being perfect and I would like to see any recommendations on improving it. Please, be kind. I am very amateur and very inexperienced( but sooner or later I will learn it)
Finally, it is working. And probably the last problem is left, the code doesn't stop, it continues to read file from the beginning.
So now the question is probably more close to 'What's wrong with my code?', compared with previous: 'What's wrong?'
UPDATE
I have rewritten last parts of code like this:
let line = NSString(data: buffer.subdataWithRange(NSMakeRange(0, range.location + 1)),
encoding: encoding)
buffer.replaceBytesInRange(NSMakeRange(0, range.location + range.length), withBytes: nil, length: 0)
println("COMPLETE LINE IS \(line)")
if line!.containsString("\n"){
println("CONTAINS NEW LINE")
//println("BUFFER IS \(buffer)")
//
println("COMPLETE LINE IS \(line)")
return line
}
else {
println("NO LINE!")
atEof == true
return nil
}
The idea is to go through all the lines which contain \n and to exclude one line, which is the last one and shouldn't have the \n. But! Despite the fact that I have checked non-printing characters and there was no \n, here is the surprising console output: Optional("lastline_blablabla\n")
Probably now the question is, how to stop at the last line even if it contains \n?
If you need to retrieve data from the URL my code above (the one under first update) will work
But my own problem with \n has several ways to be solved. One of which I used in my code. As it's very individual I won't post the solution to \n at the end of file issue. Also I am not sure that my both solutions for urlStreamReader and \n are the best, so if you advice any better solution it will be much appreciated by me and probably some other people.
Great thanks to Martin R, who explained a lot, wrote great code and was very nice