44

How could I use data in this kind of CSV file? Or how could I print for example row 2 value for "inside" column and assign it to a property / entity?

I have this kind of file I got from excel file converted to Numbers, I'd like to grab data for each column and use them.

The original CSV file opened in numbers:

enter image description here

The console output I got:

enter image description here

Using this methods:

func readDataFromCSV(fileName:String, fileType: String)-> String!{
        guard let filepath = Bundle.main.path(forResource: fileName, ofType: fileType)
            else {
                return nil
        }
        do {
            var contents = try String(contentsOfFile: filepath, encoding: .utf8)
            contents = cleanRows(file: contents)
            return contents
        } catch {
            print("File Read Error for file \(filepath)")
            return nil
        }
    }


func cleanRows(file:String)->String{
    var cleanFile = file
    cleanFile = cleanFile.replacingOccurrences(of: "\r", with: "\n")
    cleanFile = cleanFile.replacingOccurrences(of: "\n\n", with: "\n")
    //        cleanFile = cleanFile.replacingOccurrences(of: ";;", with: "")
    //        cleanFile = cleanFile.replacingOccurrences(of: ";\n", with: "")
    return cleanFile
}

SOLUTION thanks to Jens Meder

using

 func csv(data: String) -> [[String]] {
        var result: [[String]] = []
        let rows = data.components(separatedBy: "\n")
        for row in rows {
            let columns = row.components(separatedBy: ";")
            result.append(columns)
        }
        return result
    }

in viewDidLoad

var data = readDataFromCSV(fileName: kCSVFileName, fileType: kCSVFileExtension)
    data = cleanRows(file: data!)
    let csvRows = csv(data: data!)
    print(csvRows[1][1]) // UXM n. 166/167
Maximilian Kohl
  • 632
  • 1
  • 9
  • 23
biggreentree
  • 1,633
  • 3
  • 20
  • 35
  • What's the problem? It seems you've already read the data. – matt Apr 08 '17 at 14:18
  • yes I read it , but I only got a long string, How could I print for example row 2 value for "inside" column and assign it to a property / entity? – biggreentree Apr 08 '17 at 14:20
  • Did you try searching Stack Overflow? Do you really imagine you are the first person ever to want to split a CSV file into components? – matt Apr 08 '17 at 14:25
  • Possible duplicate of [swift parsing CSV file from API does not separate with the delimiter](http://stackoverflow.com/questions/35857330/swift-parsing-csv-file-from-api-does-not-separate-with-the-delimiter) – matt Apr 08 '17 at 14:30
  • obviously!, infact this code is the result of my searching and another question of mine, but many explained to me that CSV is very hard to manipulate, and I'd better change the file extension, the matter is that it's not possible. More, all solutions are tailored to different CSV files and I don't know how to solve this kind of problem. Finally design patterns change a lot between swift versions and answers, so I find very hard even to understand the staples of converting CSV to a dictionary or array – biggreentree Apr 08 '17 at 14:31

5 Answers5

35

What you want to do is splitting up the string in rows and then into columns (basically a two dimensional array of Strings). Swift already provides the components method for that on String structs.

func csv(data: String) -> [[String]] {
    var result: [[String]] = []
    let rows = data.components(separatedBy: "\n")
    for row in rows {
        let columns = row.components(separatedBy: ";")
        result.append(columns)
    }
    return result
}

Then you can access any value via:

var data = readDataFromCSV(fileName: kCSVFileName, fileType: kCSVFileExtension)
data = cleanRows(file: data)
let csvRows = csv(data: data)
print(csvRows[1][1]) //UXM n. 166/167.
ChaosPredictor
  • 3,777
  • 1
  • 36
  • 46
Jens Meder
  • 4,237
  • 1
  • 25
  • 25
  • thanks for your help, I added a test to my question with your code, but I think I did not understand what to put in data: could you check if I misunderstood something? – biggreentree Apr 08 '17 at 14:52
  • with `` I was referring to the result of your `readDataFromCSV` method. It is basically the content of the csv file that you read into a String. – Jens Meder Apr 08 '17 at 15:00
  • done! I should use cswRows, updated the entire answer with your solution, thanks for your help! – biggreentree Apr 08 '17 at 15:14
  • 6
    This isn't going to work if you have special characters in the values, like a `;` in one of the strings. And there are more escaping rules. It would be better to find a mature library to parse this for you. – sudo Sep 26 '17 at 15:16
19

Swift 4

Sometime CSV file is more complicated such as special characters (e.g. comma), the values are surrounded by double quotes as examples below:

Hello, "Complicated String, with a comma inside", 123

In this case, I use:

let dataString: String! = String.init(data: data!, encoding: .utf8)
var items: [(String, String, String)] = []
let lines: [String] = dataString.components(separatedBy: NSCharacterSet.newlines) as [String]

for line in lines {
    var values: [String] = []
    if line != "" {
        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(",", into: &value)
                }

                 values.append(value! as 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: ",")
        }

        // Put the values into the tuple and add it to the items array
        let item = (values[0], values[1], values[2])
        items.append(item)
        print(item.1)
        print(item.2)
        print(item.3)
     }
 }

It is just written in Swift 4, the original is from https://www.appcoda.com/core-data-preload-sqlite-database/

Chhaileng
  • 2,428
  • 1
  • 27
  • 24
  • 1
    This solution doesn't work either. If a value has a newline inside (which is possible if the value has double quotes surrounding it), then this will also not parse correctly. – Toad Jan 12 '20 at 14:17
  • It's also not scalable. This solution makes the assumption that we'll only have 3 columns, but if you're working with a dataset of say 30 columns, you shouldn't be defining 30-element tuples – Chris Oct 10 '20 at 14:10
16

Starting from iOS15 there is a new Official framework called TabularData, try to use it.

import TabularData

let url = Bundle.main.url(forResource: "csvFileName", withExtension: "csv")!
let result = try? DataFrame(contentsOfCSVFile: url)
print(result)

enter image description here

Then you can convert them into the data model you need

More Detail About TabularData(WWDC)

wlixcc
  • 1,132
  • 10
  • 14
13


This is for CSV file for swift 4.2

var dataArray = [[String]]()
if let path = Bundle.main.path(forResource: "file", ofType: "csv") {
    dataArray = []
    let url = URL(fileURLWithPath: path)
    do {
        let data = try Data(contentsOf: url)
        let dataEncoded = String(data: data, encoding: .utf8)
        if  let dataArr = dataEncoded?.components(separatedBy: "\r\n").map({ $0.components(separatedBy: ";") }) {
            for line in dataArr {
                dataArray.append(line)
            }
        }
    } catch let jsonErr {
        print("\n Error reading CSV file: \n ", jsonErr)
    }
}
  
Mohamed Salah
  • 868
  • 1
  • 15
  • 34
13

Swift 5.0 .scanLocaion and .scanUpTo() were deprecated in iOS13. Here's a working version of Chhaileng's answer.

 func openCSV(fileName:String, fileType: String)-> String!{
    guard let filepath = Bundle.main.path(forResource: fileName, ofType: fileType)
        else {
            return nil
    }
    do {
        let contents = try String(contentsOfFile: filepath, encoding: .utf8)

        return contents
    } catch {
        print("File Read Error for file \(filepath)")
        return nil
    }
}

 func parseCSV(){

    let dataString: String! = openCSV(fileName: "MeislinDemo", fileType: "csv")
    var items: [(String, String, String)] = []
    let lines: [String] = dataString.components(separatedBy: NSCharacterSet.newlines) as [String]

    for line in lines {
       var values: [String] = []
       if line != "" {
           if line.range(of: "\"") != nil {
               var textToScan:String = line
               var value:String?
               var textScanner:Scanner = Scanner(string: textToScan)
            while !textScanner.isAtEnd {
                   if (textScanner.string as NSString).substring(to: 1) == "\"" {


                       textScanner.currentIndex = textScanner.string.index(after: textScanner.currentIndex)

                       value = textScanner.scanUpToString("\"")
                       textScanner.currentIndex = textScanner.string.index(after: textScanner.currentIndex)
                   } else {
                       value = textScanner.scanUpToString(",")
                   }

                    values.append(value! as String)

                if !textScanner.isAtEnd{
                        let indexPlusOne = textScanner.string.index(after: textScanner.currentIndex)

                    textToScan = String(textScanner.string[indexPlusOne...])
                    } 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: ",")
           }

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

}
Rob.R
  • 481
  • 5
  • 11
  • 7
    This parser (like any of the parsers on this page) is flawed. It starts off splitting lines by newline, which is an incorrect assumption. A csv file might contain a newline as it's value when it is in double quotes. So the only way to go is properly parse from the beginning, use a mini-state machine, and don't assume anything – Toad Jan 12 '20 at 14:15