///|
priv struct CsvReader {
mut field : StringBuilder
mut row : Array[String]
rows : Array[Array[String]]
mut units : Int32
budget : Meter
}
///|
fn CsvReader::append(self : CsvReader, c : Char) -> Unit raise DesignError {
self.field.write_char(c)
self.units += 1
self.budget.charge(1)
if self.units > 4096 {
raise Invalid("CSV cell exceeds 4096 characters")
}
}
///|
fn CsvReader::finish_field(self : CsvReader) -> Unit raise DesignError {
if self.row.length() >= 16 {
raise Invalid("CSV width exceeds 16")
}
self.row.push(self.field.to_string())
self.field = StringBuilder()
self.units = 0
}
///|
fn CsvReader::finish_row(self : CsvReader) -> Unit raise DesignError {
self.finish_field()
if self.rows.length() >= self.budget.limits.max_rows + 1 {
raise Budget("CSV row allowance")
}
self.rows.push(self.row)
self.row = []
}
///|
fn csv_rows(
text : String,
budget : Meter,
) -> Array[Array[String]] raise DesignError {
if text.length() > 1048576 {
raise Budget("CSV text allowance")
}
let reader : CsvReader = {
field: StringBuilder(),
row: [],
rows: [],
units: 0,
budget,
}
// States: 0 empty, 1 unquoted, 2 quoted, 3 after closing quote.
let mut state = 0
let mut after_cr = false
let mut touched = false
for c in text {
budget.charge(1)
if after_cr {
after_cr = false
if c == '\n' {
continue
}
}
touched = true
if state == 2 {
if c == '"' {
state = 3
} else {
reader.append(c)
}
continue
}
if state == 3 {
if c == '"' {
reader.append(c)
state = 2
continue
}
if c != ',' && c != '\n' && c != '\r' {
raise Invalid("characters after CSV closing quote")
}
}
if c == ',' {
reader.finish_field()
state = 0
} else if c == '\n' || c == '\r' {
reader.finish_row()
state = 0
touched = false
after_cr = c == '\r'
} else if c == '"' {
if state != 0 {
raise Invalid("quote inside unquoted CSV field")
}
state = 2
} else {
reader.append(c)
state = 1
}
}
if state == 2 {
raise Invalid("unterminated CSV quoted field")
}
if touched || !reader.row.is_empty() {
reader.finish_row()
}
reader.rows
}
///|
pub fn parse_table_csv(
owner : Schema,
text : String,
limits~ : Limits = default_limits(),
) -> Table raise DesignError {
let budget = meter(limits)
let parsed = csv_rows(text, budget)
if parsed.is_empty() {
raise Invalid("CSV requires an attribute header")
}
let columns = owner.resolve(parsed[0])
if columns.length() != parsed[0].length() {
raise Invalid("duplicate CSV header")
}
if columns.map(i => owner.labels[i]) != parsed[0] {
raise Invalid("CSV header must follow schema order")
}
let rows = []
for i in 1.. String {
let mut quoted = false
for c in value {
if c == ',' || c == '"' || c == '\r' || c == '\n' {
quoted = true
}
}
if !quoted {
return value
}
let out = StringBuilder()
out.write_char('"')
for c in value {
if c == '"' {
out.write_char('"')
}
out.write_char(c)
}
out.write_char('"')
out.to_string()
}
///|
pub fn Table::to_csv(self : Table) -> String {
let out = StringBuilder()
out.write_string(self.column_names().map(csv_field).join(",") + "\n")
for row in self.data {
out.write_string(row.map(csv_field).join(",") + "\n")
}
out.to_string()
}