///|
fn csv_push_field(
row : Array[String],
value : String,
limits : Limits,
) -> Unit raise SdmxError {
if row.length() >= limits.max_columns + 3 {
raise Limit(resource="CSV columns", maximum=limits.max_columns + 3)
}
check_field(value, limits, "CSV field")
row.push(value)
}
///|
fn csv_push_row(
rows : Array[Array[String]],
row : Array[String],
limits : Limits,
) -> Unit raise SdmxError {
if rows.length() >= limits.max_rows + 1 {
raise Limit(resource="CSV records", maximum=limits.max_rows)
}
rows.push(row)
}
///|
fn csv_rows(
text : String,
limits : Limits,
) -> Array[Array[String]] raise SdmxError {
check_text(text, limits, "CSV text")
let chars = text.to_array()
let rows : Array[Array[String]] = []
let mut row : Array[String] = []
let mut field = StringBuilder()
let mut units = 0
let mut state = 0 // start, unquoted, quoted, after closing quote
let mut i = if chars.length() > 0 && chars[0].to_int() == 65279 {
1
} else {
0
}
while i < chars.length() {
let ch = chars[i]
if state == 2 {
if ch == '"' {
if i + 1 < chars.length() && chars[i + 1] == '"' {
field.write_char('"')
units += 1
i += 1
} else {
state = 3
}
} else {
field.write_char(ch)
units += if ch.to_int() > 65535 { 2 } else { 1 }
}
} else if ch == ',' {
csv_push_field(row, field.to_string(), limits)
field = StringBuilder()
units = 0
state = 0
} else if ch == '\r' || ch == '\n' {
if ch == '\r' {
if i + 1 >= chars.length() || chars[i + 1] != '\n' {
raise Invalid(path="CSV", reason="bare CR outside quoted field")
}
i += 1
}
csv_push_field(row, field.to_string(), limits)
csv_push_row(rows, row, limits)
row = []
field = StringBuilder()
units = 0
state = 0
} else if ch == '"' && state == 0 {
state = 2
} else if ch == '"' || state == 3 {
raise Invalid(
path="CSV row \{rows.length() + 1}",
reason="invalid quotation",
)
} else {
state = 1
field.write_char(ch)
units += if ch.to_int() > 65535 { 2 } else { 1 }
}
if units > limits.max_field {
raise Limit(resource="CSV field", maximum=limits.max_field)
}
i += 1
}
if state == 2 {
raise Invalid(path="CSV", reason="unterminated quoted field")
}
if !row.is_empty() || units > 0 || state != 0 {
csv_push_field(row, field.to_string(), limits)
csv_push_row(rows, row, limits)
}
if rows.is_empty() {
raise Invalid(path="CSV", reason="missing header")
}
let width = rows[0].length()
for n, record in rows {
if record.length() != width {
raise Invalid(
path="CSV row \{n + 1}",
reason="record width differs from header",
)
}
}
rows
}
///|
fn csv_quote(value : String) -> String {
let out = StringBuilder()
let mut quote = false
for ch in value {
if ch == ',' || ch == '"' || ch == '\r' || ch == '\n' {
quote = true
}
}
if !quote {
return value
}
out.write_char('"')
for ch in value {
if ch == '"' {
out.write_char('"')
}
out.write_char(ch)
}
out.write_char('"')
out.to_string()
}
///|
fn csv_document(
rows : Array[Array[String]],
limits : Limits,
) -> String raise SdmxError {
let output = StringBuilder()
let mut units = 0
for row in rows {
for i, field in row {
check_field(field, limits, "CSV output field")
let escaped = csv_quote(field)
let extra = escaped.length() + (if i == 0 { 0 } else { 1 })
if units > limits.max_output - extra {
raise Limit(resource="CSV output", maximum=limits.max_output)
}
units += extra
if i > 0 {
output.write_char(',')
}
output.write_string(escaped)
}
if units > limits.max_output - 2 {
raise Limit(resource="CSV output", maximum=limits.max_output)
}
units += 2
output.write_string("\r\n")
}
output.to_string()
}