Read CSV file, parse into arrays, Swift 4, Xcode 9 broken, Xcode 8.3 OK

Can somebody please help me to find a way of reading a CSV file and parsing the contents into arrays of float, int and string values. There are many online code suggestions but they are all apparently broken in Xcode 9 (Swift 4). It appears that there have been significant changes recently and things that used to work no longer work. What I would really like to do is to be able to pick a text file from anywhere on my file system that has the appropriate pirvelages, convert that file to a string in Xcode, and then parse the string into arrays of different types (including string, int, float, double, etc.). When I try to run samples available on the internet I discover that many of the methods have changed or are no longer available.


I have code that works in Xcode 8.3. Despite trying many things, I cannot get Xcode 9 to do this. Xcode 9 can't even read the same file (same location) with identical code. The code is given below. Debugging, file = nil in Xcode 9.


let file: FileHandle? = FileHandle(forReadingAtPath: "/Volumes/Mac.Ext.J.1/Swift/CSV TRY 3/CSV.Parser/ECG1.csv")

if file != nil {

/

let data = file?.readDataToEndOfFile()

/

file?.closeFile()

/

let str = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)

print(str!)

}

else {

print("Ooops! Something went wrong!")

}


Is this possible in Xcode 9? If so, how? Does anybody have actual working code that does this in Xcode 9? It looks like I will be using Xcode 8.3 until this issue is resolved.


Thank you.

Answered by DTS Engineer in 272170022

As with most things in programming, it pays to break down your problem into parts:

  1. Reading a file

  2. Breaking it into lines

  3. Breaking the lines in columns

  4. Converting a string to the appropriate data value

I’ll cover each in turn.

If you’re just getting started, a good place to start is a command line tool. This avoids the need for dealing with file selection dialogs, the App Sandbox, and so on. Here’s a tiny shell command line tool that’ll get you going:

import Foundation

func process(string: String) throws {
    … your code here …
}

func processFile(at url: URL) throws {
    let s = try String(contentsOf: url)
    try process(string: s)
}

func main() {
    guard CommandLine.arguments.count > 1 else {
        print("usage: \(CommandLine.arguments[0]) file...")
        return
    }
    for path in CommandLine.arguments[1...] {
        do {
            let u = URL(fileURLWithPath: path)
            try processFile(at: u)
        } catch {
            print("error processing: \(path): \(error)")
        }
    }
}

main()
exit(EXIT_SUCCESS)

Note I’ve made some shortcuts here (like assuming the file is UTF-8 and printing errors to

stdout
not
stderr
) but it’s good enough to get your started.

Breaking a string into lines is super easy:

let lines = string.split(separator: "\n")
for line in lines {
    … your code here …
}

Or, if you want to ‘see’ blank lines:

let lines = string.split(separator: "\n", omittingEmptySubsequences: false)

Note The above assumes platform-standard line breaks (LF).

Breaking lines into columns is easy for tab-separated files (literally take the previous code and change

\n
to
\t
) but it’s quite difficult for comma separate files. This is nothing to with Swift and everything to do with fundamental properties of CSV. To quote The Fount of All Knowledge:

… in popular usage "CSV" is not a single, well-defined format.

For the moment let’s assume that column are never quoted, and thus cannot contain commas, in which case we can just split on commas:

let columns = line.split(separator: ",", omittingEmptySubsequences: false)
for column in columns {
    … your code here …
}

In this case you really want to not omit empty sequences so that you can keep track of column numbers. Speaking of that, here’s how you’d do that:

for (columnNumber, column) in columns.enumerated() {
    … your code here …
}

Note If the column can contain commas that you need to parse the line to work out the columns. How you do this depends on how your CSV file is set up.

Finally, converting a string (like

column
in the snippet above) to a number is done by an initialiser. For example, if you know the column is an integer you can do this:
if let i = Int(column) {
    print("Int = \(i)")
} else {
    … handle the fact that the column isn't the right type …
}

Or if you don’t know the types you can chain these together:

if let i = Int(column) {
    print("Int = \(i)")
} else if let d = Double(column) {
    print("Double = \(d)")
} else {
    print("String = \(column)")
}

Note Again I’m taking shortcuts here. For example, this assumes the integer is rendered using standard ASCII digits, is in base 10, and so on.

Share and Enjoy

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

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

As far as I tested, your code shown above works exactly the same way both in Xcode 8.3.3 and Xcode 9.0.1.

How have you confirmed that your code works in Xcode 8.3? Isn't that just you have picked up some lines of code once worked in another app?


Check all these things.

  • The path to the file is valid and right.
  • Your app runs with enough permission to access the file.
  • Your app is not sandboxed.

> It looks like I will be using Xcode 8.3 until this issue is resolved.


Weren't we told toavoid Xcode 8.3...?

OOper. Thank you for your response. I decided to create an exact duplicate directory of the code that works in 8.3. I then opened the project using 9.01 in File Manager. Everything else was constant. And this time it worked in 9.0.1. I then tried to figure out what was different between the two versions I was working on when I made the original post. Because I did not save the directories at that time I could not complete the analysis. I am new at this and do not yet understand how this happened. I was running both version 8.1 and 9.01 with their default settings and I did not do anything knowingly to change permissions or sandbox anything. But, you must be correct. Thanks again.

Accepted Answer

As with most things in programming, it pays to break down your problem into parts:

  1. Reading a file

  2. Breaking it into lines

  3. Breaking the lines in columns

  4. Converting a string to the appropriate data value

I’ll cover each in turn.

If you’re just getting started, a good place to start is a command line tool. This avoids the need for dealing with file selection dialogs, the App Sandbox, and so on. Here’s a tiny shell command line tool that’ll get you going:

import Foundation

func process(string: String) throws {
    … your code here …
}

func processFile(at url: URL) throws {
    let s = try String(contentsOf: url)
    try process(string: s)
}

func main() {
    guard CommandLine.arguments.count > 1 else {
        print("usage: \(CommandLine.arguments[0]) file...")
        return
    }
    for path in CommandLine.arguments[1...] {
        do {
            let u = URL(fileURLWithPath: path)
            try processFile(at: u)
        } catch {
            print("error processing: \(path): \(error)")
        }
    }
}

main()
exit(EXIT_SUCCESS)

Note I’ve made some shortcuts here (like assuming the file is UTF-8 and printing errors to

stdout
not
stderr
) but it’s good enough to get your started.

Breaking a string into lines is super easy:

let lines = string.split(separator: "\n")
for line in lines {
    … your code here …
}

Or, if you want to ‘see’ blank lines:

let lines = string.split(separator: "\n", omittingEmptySubsequences: false)

Note The above assumes platform-standard line breaks (LF).

Breaking lines into columns is easy for tab-separated files (literally take the previous code and change

\n
to
\t
) but it’s quite difficult for comma separate files. This is nothing to with Swift and everything to do with fundamental properties of CSV. To quote The Fount of All Knowledge:

… in popular usage "CSV" is not a single, well-defined format.

For the moment let’s assume that column are never quoted, and thus cannot contain commas, in which case we can just split on commas:

let columns = line.split(separator: ",", omittingEmptySubsequences: false)
for column in columns {
    … your code here …
}

In this case you really want to not omit empty sequences so that you can keep track of column numbers. Speaking of that, here’s how you’d do that:

for (columnNumber, column) in columns.enumerated() {
    … your code here …
}

Note If the column can contain commas that you need to parse the line to work out the columns. How you do this depends on how your CSV file is set up.

Finally, converting a string (like

column
in the snippet above) to a number is done by an initialiser. For example, if you know the column is an integer you can do this:
if let i = Int(column) {
    print("Int = \(i)")
} else {
    … handle the fact that the column isn't the right type …
}

Or if you don’t know the types you can chain these together:

if let i = Int(column) {
    print("Int = \(i)")
} else if let d = Double(column) {
    print("Double = \(d)")
} else {
    print("String = \(column)")
}

Note Again I’m taking shortcuts here. For example, this assumes the integer is rendered using standard ASCII digits, is in base 10, and so on.

Share and Enjoy

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

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

Thank you Eskimo. It is starting to come together. I am positively amazed at the quality of this Developer Forum. I have created a solution with proactive filtering so that if I get a line that is not in the required CSV format the code can ignore that line and go on to the next line.


First I read the file into a String:

l et file: FileHandle? = FileHandle(forReadingAtPath: "/Volumes/Mac.Ext.J.1/Swift/Test.csv")


Then I parse the string into an array with the newline delimeter:

et fileNameArr : [String] = fileName.components(separatedBy: "\n")


I am expecting the CSV file to be formated with an Int, followed by a Float, delimited by a comma. Like this:


21122,89.43434

21124,100.43423

...


Becasue I won't have control over the source of the CSV file, I need to ensure that the program skips over any lines that are not of this format. And I have encountered lines like this, on the first line (text header information), and the last line (blank line).


I split each line as follows:

var stringValue = arrayValue.components(separatedBy: ",")


I check to make sure that the program can split the lines into an Int and Float by doing this:

var timeInt = Int(String(stringValue[0]))


And then I skip lines that don't resolve properly like this:

if timeInt != nil {

if ii > 0 {

timeArray.append(timeInt!)

}


var ecgInt = Float(String(stringValue[1]))

if ii > 0 {

ecgArray.append(ecgInt!)

}

if ii > 0 {

print("Index: [\(ii-1)] Time: \(timeArray[ii-1]) ECG: \(ecgArray[ii-1])")

}

}


I should also check to ensure that the second value in the CSV file lines can be resolved in a similar way as a condition. But it is starting to come together.


Thanks again. I really appreciate it. Your help has got me through the rough part. It may be easier from here. I will need to read in large CSV files and create graphical PDF files, and it looks good right now. I am going to try to do this without learning all of the Swift language. In some ways Swift is very different from Java or C#. I have created complex graphical programs in Java and C# and expect some snags as I try to transition (maybe too qiuckly).


Thanks again.

Read CSV file, parse into arrays, Swift 4, Xcode 9 broken, Xcode 8.3 OK
 
 
Q