3
\$\begingroup\$

I'd had a hard time getting this to work. I’d like to parse a file containing data and copy this data into a struct.

The data file (test.dat) looks like this:

1,"Tom","Smith"
2,"Peter","Perth Junior"
3,"Cathy","Johnson"

I use the following function.

func parseDataFile() {

    struct person {
        var id: Int
        var first: String
        var last: String
    }

    var friends: [person] = []
    let filePath = Bundle.main.path(forResource: "test", ofType: "dat")
    guard filePath != nil else { return }
    let fileURL = URL(fileURLWithPath: filePath!)
    do {
        let file = try String(contentsOf: fileURL, encoding: .utf8)
        let arrayOfLines = file.split { $0.isNewline }
        for line in arrayOfLines {
            let arrayOfItems = line.components(separatedBy: ",")
            let tempPerson = person(id: Int(arrayOfItems[0])!,
                                    first: arrayOfItems[1].replacingOccurrences(of: "\"", with: ""),
                                    last: arrayOfItems[2].replacingOccurrences(of: "\"", with: "")
            )
            friends.append(tempPerson)
        }
    } catch {
        print(error)
    }
}

I read the data into a string, split it up into an array (every line is a record). The records are copied into the struct person. This works fine, but I'm sure this is far from optimum.

I was testing a lot with PropertyListDecoder().decode(), but this seems to accept only dictionary formatted data (not sure though ?!).

Next, is it necessary to use an array record, as intermediate step, prior putting the line into a struct?

Last, is there no method to copy all comma separated strings to an array, without filling every property of the struct individually (like I did)?

Besides that, I believe that there is also a much more efficient way to get rid of the string delimiting quotes than using replacingOccurrences(of:with:)?

Any comments would be appreciated.

\$\endgroup\$
  • \$\begingroup\$ Is this struct used outside of this function? \$\endgroup\$ – ielyamani Sep 12 at 8:56
  • \$\begingroup\$ Yes it is. But this would just be to put the struct outside the function. \$\endgroup\$ – geohei Sep 12 at 13:10
3
\$\begingroup\$

This

let filePath = Bundle.main.path(forResource: "test", ofType: "dat")
guard filePath != nil else { return }
let fileURL = URL(fileURLWithPath: filePath!)

is an anti-pattern in Swift, see also When should I compare an optional value to nil? on Stack Overflow. Instead of testing a value against nil and then force-unwrapping it, you should use optional binding:

guard let filePath = Bundle.main.path(forResource: "test", ofType: "dat") else { return }
let fileURL = URL(fileURLWithPath: filePath)

This can be further simplified by retrieving an URL in the first place, instead of getting a file path and converting that to an URL:

guard let fileURL = Bundle.main.url(forResource: "test", withExtension: "dat")
    else { return }

But actually this is a situation where I would prefer force-unwrapping: A missing resource file in the application bundle is a programming error and there is no way of recovering from that situation in a sensible way. It should be detected early during development:

let fileURL = Bundle.main.url(forResource: "test", withExtension: "dat")!

The same reasoning applies to reading and decoding the file:

let file = try! String(contentsOf: fileURL, encoding: .utf8)

Using split() here

let arrayOfLines = file.split { $0.isNewline }

is good: It creates an array, but the array elements are substrings, i.e. they contain references into the character storage of the original file string. The same method can be applied to split each line to avoid data duplication:

for line in arrayOfLines {
    let arrayOfItems = line.split(separator: ",")
    // ...
}

This

let tempPerson = Person(id: Int(arrayOfItems[0])!,
                        first: arrayOfItems[1].replacingOccurrences(of: "\"", with: ""),
                        last: arrayOfItems[2].replacingOccurrences(of: "\"", with: "")

would crash if a line has less than 3 comma-separated items, or if the first item is not an integer. However, as mentioned above, this is acceptable if the data is read from your own resource file (and not from some external resource): We can rely on the data to be well-formed.

It is a bit difficult to read though, and I would split the single statement into parts:

let id = Int(arrayOfItems[0])!
let firstName = arrayOfItems[1].replacingOccurrences(of: "\"", with: "")
let lastName = arrayOfItems[2].replacingOccurrences(of: "\"", with: "")
let tempPerson = Person(id: id, first: firstName, last: lastName)

With respect to removing the delimiters: What you do is fine if the data file has exactly the format from your example, and there are no embedded quotation marks or commas in the fields. An alternative is

let firstName = String(arrayOfItems[1].dropFirst().dropLast())
let lastName = String(arrayOfItems[2].dropFirst().dropLast())

which might be slightly more efficient because it does not need bridging to NSString. (But I did not measure it.)

Finally note that your code does not parse all valid CSV files. Problems are for example

  • embedded commas: 4,"Sammy","Davis, Jr."
  • embedded quotation marks: 5,"John","Foo""Bar"
  • or embedded newlines.

Before you try to implement all those features yourself I would suggest to have a look at some existing Swift CSV parsing libraries.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.