3

I'm working from a previous posting on AppCode called "Core Data Basics: Preload Data and Use Existing SQLite Database" located here: https://www.appcoda.com/core-data-preload-sqlite-database/

Within Simon Ng's posting is a function called parseCSV which does all the heavy lifting of scanning through a .csv and breaking it up into it's respective rows so that each row's elements can then be saved into their respective managedObjectContext in core data.

Unfortunately all of the code appears to be written in either Swift 1.0 or Swift 2.0 and I have been unable to understand the errors I'm getting in converting it into Swift 4.

I've made all of the changes suggested by Xcode with regards to "this" has been replaced with "that", with the final error telling me "Argument labels '(contentsOfURL:, encoding:, error:)' do not match any available overloads" which I have been unable to understand nor correct.

// https://www.appcoda.com/core-data-preload-sqlite-database/

    func parseCSV (contentsOfURL: NSURL, encoding: String.Encoding, error: NSErrorPointer) -> [(name:String, detail:String, price: String)]? {
        // Load the CSV file and parse it
        let delimiter = ","
        var items:[(name:String, detail:String, price: String)]?

        if let content = String(contentsOfURL: contentsOfURL, encoding: encoding, error: error) {
            items = []
            let lines:[String] = content.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]

            for line in lines {
                var values:[String] = []
                if line != "" {
                    // For a line with double quotes
                    // we use NSScanner to perform the parsing
                    if line.range(of: "\"") != nil {
                        var textToScan:String = line
                        var value:NSString?
                        var textScanner:Scanner = Scanner(string: textToScan)
                        while textScanner.string != "" {

                            if (textScanner.string as NSString).substring(to: 1) == "\"" {
                                textScanner.scanLocation += 1
                                textScanner.scanUpTo("\"", into: &value)
                                textScanner.scanLocation += 1
                            } else {
                                textScanner.scanUpTo(delimiter, into: &value)
                            }

                            // Store the value into the values array
                            values.append(value! as String)

                            // Retrieve the unscanned remainder of the string
                            if textScanner.scanLocation < textScanner.string.count {
                                textToScan = (textScanner.string as NSString).substring(from: textScanner.scanLocation + 1)
                            } else {
                                textToScan = ""
                            }
                            textScanner = Scanner(string: textToScan)
                        }

                        // For a line without double quotes, we can simply separate the string
                        // by using the delimiter (e.g. comma)
                    } else  {
                        values = line.components(separatedBy: delimiter)
                    }

                    // Put the values into the tuple and add it to the items array
                    let item = (name: values[0], detail: values[1], price: values[2])
                    items?.append(item)
                }
            }
        }

        return items
    }

The 5th line:

if let content = String(contentsOfURL: contentsOfURL, encoding: encoding, error: error) {

is throwing the following error:

Argument labels '(contentsOfURL:, encoding:, error:)' do not match any available overloads

Which is beyond my understanding and skill level. I'm really just trying to find the best way of importing a comma separated .csv file into a core data object.

Any assistance would be appreciated. The original example by Simon Ng appears perfect for what I'm trying to achieve. It just hasn't been updated in a very long time.

pscarnegie
  • 109
  • 1
  • 11
  • 1
    Let Xcode help by using code completion. Type `if let content = String.init(` and it will show you the available initializers. Once you get the one you want, you can remove the `.init`. – rmaddy Apr 17 '19 at 17:52
  • 1
    See https://stackoverflow.com/questions/24010569/error-handling-in-swift-language/24030038#24030038 but there are many other issues. In Swift 3 the syntax has changed considerably. – vadian Apr 17 '19 at 18:09
  • Learned something new from rmaddy - tossing in the ol' .init really does provide additional help when formatting code completions. Thanks rmaddy! – pscarnegie Apr 17 '19 at 19:57
  • I gotta tell ya - I'm amazed at all the great help I got so fast. All great reads. Thanks to all of you. – pscarnegie Apr 17 '19 at 19:57

4 Answers4

2

First of all - you all are brilliant contributors and bloody fast at your intel. I'd like to thank all of you for answering so quickly. Here's where I ended up with that particular function in the latest Swift 5 syntax.

func parseCSV (contentsOfURL: NSURL, encoding: String.Encoding, error: NSErrorPointer) -> [(name:String, detail:String, price: String)]? {
   // Load the CSV file and parse it
    let delimiter = ","
    var items:[(name:String, detail:String, price: String)]?

    //if let content = String(contentsOfURL: contentsOfURL, encoding: encoding, error: error) {
    if let content = try? String(contentsOf: contentsOfURL as URL, encoding: encoding) {
        items = []
        let lines:[String] = content.components(separatedBy: NSCharacterSet.newlines) as [String]

        for line in lines {
            var values:[String] = []
            if line != "" {
                // For a line with double quotes
                // we use NSScanner to perform the parsing
                if line.range(of: "\"") != nil {
                    var textToScan:String = line
                    var value:NSString?
                    var textScanner:Scanner = Scanner(string: textToScan)
                    while textScanner.string != "" {

                        if (textScanner.string as NSString).substring(to: 1) == "\"" {
                            textScanner.scanLocation += 1
                            textScanner.scanUpTo("\"", into: &value)
                            textScanner.scanLocation += 1
                        } else {
                            textScanner.scanUpTo(delimiter, into: &value)
                        }

                        // Store the value into the values array
                        values.append(value! as String)

                        // Retrieve the unscanned remainder of the string
                        if textScanner.scanLocation < textScanner.string.count {
                            textToScan = (textScanner.string as NSString).substring(from: textScanner.scanLocation + 1)
                        } else {
                            textToScan = ""
                        }
                        textScanner = Scanner(string: textToScan)
                    }

                    // For a line without double quotes, we can simply separate the string
                    // by using the delimiter (e.g. comma)
                } else  {
                    values = line.components(separatedBy: delimiter)
                }

                // Put the values into the tuple and add it to the items array
                let item = (name: values[0], detail: values[1], price: values[2])
                items?.append(item)
            }
        }
    }

    return items
}
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
pscarnegie
  • 109
  • 1
  • 11
  • 1
    Don't use `NSCharacterSet`, `NSString`, `NSURL` in Swift 3+. There are native equivalents. And `NSErrorPointer` became obsolete. And don't `try?`, **catch** the error. – vadian Apr 18 '19 at 10:02
0

As of Swift 3, that function has been changed to String(contentsOf:, encoding:) so you just need to modify the argument labels in code.

It's also worth mentioning, that this function will now throw so you will have to handle that. It wouldn't do any harm for you to take a look at this page on exception handling in Swift.

rpecka
  • 1,168
  • 5
  • 17
0

Because Scanner has been changed up in iOS 13 in ways that seem to be poorly explained, I rewrote this to work without it. For my application, the header row is of interest, so it's captured separately; if it's not meaningful then that part can be omitted.

The code starts with workingText which has been read from whatever file or URL is the source of the data.

var headers : [String] = []
var data : [[String]]  = []

let workingLines = workingText.split{$0.isNewline}
if let headerLine = workingLines.first {
    headers = parseCsvLine(ln: String(headerLine))
    for ln in workingLines {
        if ln != headerLine {
            let fields = parseCsvLine(ln: String(ln))
            data.append(fields)
        }
    }
}

print("-----------------------------")
print("Headers: \(headers)")
print("Data:")
for d in data {
    print(d)    // gives each data row its own printed row; print(data) has no line breaks anywhere + is hard to read
}
print("-----------------------------")

func parseCsvLine(ln: String) -> [String] {
// takes a line of a CSV file and returns the separated values
// so input of 'a,b,2' should return ["a","b","2"]
// or input of '"Houston, TX","Hello",5,"6,7"' should return ["Houston, TX","Hello","5","6,7"]

let delimiter = ","
let quote = "\""
var nextTerminator = delimiter
var andDiscardDelimiter = false

var currentValue = ""
var allValues : [String] = []

for char in ln {
    let chr = String(char)
    if chr == nextTerminator {
        if andDiscardDelimiter {
            // we've found the comma after a closing quote. No action required beyond clearing this flag.
            andDiscardDelimiter = false
        }
        else {
            // we've found the comma or closing quote terminating one value
            allValues.append(currentValue)
            currentValue = ""
        }
        nextTerminator = delimiter  // either way, next thing we look for is the comma
    } else if chr == quote {
        // this is an OPENING quote, so clear currentValue (which should be nothing but maybe a single space):
        currentValue = ""
        nextTerminator = quote
        andDiscardDelimiter = true
    } else {
        currentValue += chr
    }
}
return allValues

}

I freely acknowledge that I probably use more conversions to String than those smarter than I am in the ways of Apple strings, substrings, scanners, and such would find necessary. Parsing a file of a few hundred rows x about a dozen columns, this approach seems to work fine; for something significantly larger, the extra overhead may start to matter.

ConfusionTowers
  • 911
  • 11
  • 34
0

An alternative is to use a library to do this. https://github.com/dehesa/CodableCSV supports this and has a list of other swift csv libraries too

wheeliebin
  • 716
  • 1
  • 6
  • 20