Parse CSV to Kotlin List

Viewed 6243

I'm a bit confused. I have this csv line with double quote("") as grouped strings, and want to convert to Kotlin List. However it produces a single array with size 1. I want to be able to get the group, assumes at position 2.

val s = "John Doe, 13, \"Subject 1, Subject 2, Subject 3\""
var list: List<String> = s.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)")
Log.d("size:", list.size.toString() + " - subjects:" + list[2])

This gives me error

java.lang.IndexOutOfBoundsException: Index: 2, Size: 1
4 Answers

The given regex just works fine. Currently you try to split the string s at the raw regex as delimeter, which does not exists in s. Simply add .toRegex() to the regex.

val s = "John Doe, 13, \"Subject 1, Subject 2, Subject 3\""
var list: List<String> = s.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)".toRegex())
Log.d("size:", list.size.toString() + " - subjects:" + list[2])

I'd recommend not doing the parsing yourself, but using an existing library.  (For example, I found Apache Commons CSV easy to use from Kotlin.)

Although writing parsing code can be fun, and CSV seems simple enough, it has enough complications and variations that unless you created it yourself, you're likely to miss some cases.  (Not just escaped quotes, but other escaped characters, nested quotes, fields which include newlines, comment lines…  And my favourite gotcha: MS Excel uses the machine's list separator, which can be semicolon or another character instead of comma to separate fields!)

Trust me, I've been there…

Split by " and you get a list with size = 3
and what you need is at index = 1.
Then you need to split the item index = 0 by , for the name and the number:

val s = "John Doe, 13, \"Subject 1, Subject 2, Subject 3\""
val list: List<String> = s.split("\"")
val listStart = list[0].split(",")
val subjects = list[1].trim()
val name = listStart[0].trim()
val number = listStart[1].trim()

Another way if you don't really need the list but only the values:

val s = "John Doe, 13, \"Subject 1, Subject 2, Subject 3\""
val subjects = s.substringAfter("\"").substringBefore("\"").trim()
val name = s.substringBefore(",").trim()
val number = s.substringAfter(",").substringBefore(",").trim()

println("name: " + name)
println("number: " + number)
println("subjects: " + subjects)

will print:

name: John Doe
number: 13
subjects:Subject 1, Subject 2, Subject 3

Is this helpful? Not much of a test since I make no actual assertions, but maybe enough to put you onto the idea.

@Test
fun should_tolerate_quoted_commas_in_third_column() {
    val s = "John Doe, 13, \"Subject 1, Subject 2, Subject 3\""
    val pattern = Pattern.compile(",")
    s.split(regex=pattern, limit=3).forEach( System.out::println )
}

output:

John Doe
 13
 "Subject 1, Subject 2, Subject 3"

EDIT 1:

Similarly...

@Test
fun should_make_list_from_quoted_string_in_third_column() {
    val s = "John Doe, 13, \"Subject 1, Subject 2, Subject 3\""
    val outerList = s.split(regex = Pattern.compile(","), limit = 3)
    outerList[2].replace("\"","").split(",").forEach( System.out::println )
}
Related