///|
pub fn read_csv(
source : String,
delimiter? : Char = ',',
) -> Array[Array[String]] raise InputError {
let rows : Array[Array[String]] = []
let mut row : Array[String] = []
let mut field = ""
let mut quoted = false
let mut closed = false
let mut pending_cr = false
for c in source {
if pending_cr && c == '\n' {
pending_cr = false
continue
}
pending_cr = false
if quoted {
if c == '"' {
quoted = false
closed = true
} else {
field += c.to_string()
}
} else if closed && c == '"' {
field += "\""
quoted = true
closed = false
} else if c == delimiter {
row.push(field)
field = ""
closed = false
} else if c == '\n' || c == '\r' {
row.push(field)
rows.push(row)
row = []
field = ""
closed = false
pending_cr = c == '\r'
} else if c == '"' {
if field != "" {
raise InputError("quote in unquoted CSV field")
}
quoted = true
} else {
if closed {
raise InputError("characters after closing CSV quote")
}
field += c.to_string()
}
}
if quoted {
raise InputError("unterminated CSV field")
}
if field != "" || !row.is_empty() || closed {
row.push(field)
rows.push(row)
}
rows
}
///|
pub fn write_csv(
rows : Array[Array[String]],
delimiter? : String = ",",
) -> String {
rows
.map(fn(row) {
row
.map(fn(cell) {
if cell.contains(delimiter) ||
cell.contains("\"") ||
cell.contains("\n") ||
cell.contains("\r") {
"\"" + cell.replace_all(old="\"", new="\"\"") + "\""
} else {
cell
}
})
.join(delimiter)
})
.join("\r\n")
}
///|
fn csv_records(source : String) -> Array[Value] raise InputError {
let matrix = read_csv(source)
if matrix.is_empty() {
return []
}
let headers = matrix[0]
let seen : Map[String, Bool] = {}
for header in headers {
if header == "" || seen.contains(header) {
raise InputError("empty or duplicate CSV header")
}
seen[header] = true
}
let records : Array[Value] = []
for i = 1; i < matrix.length(); i = i + 1 {
let row = matrix[i]
if row.length() != headers.length() {
raise InputError("CSV width mismatch at row " + (i + 1).to_string())
}
let values : Map[String, Value] = {}
for j = 0; j < headers.length(); j = j + 1 {
values[headers[j]] = String(row[j])
}
records.push(Object(values))
}
records
}