///|
priv struct CSVMode {
format : String
separator : Char
comment : Char
header : Bool
}
///|
fn csv_mode(text : String, input : Bool) -> CSVMode? raise ParseError {
let chars = text.to_array()
let words = []
let mut first = 0
for i in 0..<=chars.length() {
if i == chars.length() || chars[i].is_whitespace() {
if first < i {
words.push(String::from_array(chars[first:i]))
}
first = i + 1
}
}
if words.is_empty() {
return None
}
let format = words[0]
let mut separator = match format {
"csv" => ','
"tsv" => '\t'
_ => raise Invalid("invalid CSV/TSV mode")
}
let mut comment = '\u0000'
let mut header = false
for word in words[1:] {
let parts = word.split("=").collect()
let key = parts[0].to_owned()
let value = if parts.length() > 1 {
word[key.length() + 1:].to_owned()
} else {
""
}
if key == "separator" || (input && key == "comment") {
let value = value.to_array()
if value.length() != 1 {
raise Invalid("CSV delimiter must be one character")
}
let c = value[0]
if ['\u0000', '\r', '\n', '"', '\ufffd'].contains(c) {
raise Invalid("invalid CSV delimiter")
}
if key == "separator" {
separator = c
} else {
comment = c
}
} else if input && key == "header" {
header = match value {
"" | "true" => true
"false" => false
_ => raise Invalid("invalid CSV header option")
}
} else {
raise Invalid("invalid CSV/TSV option")
}
}
if separator == comment {
raise Invalid("CSV separator equals comment delimiter")
}
Some({ format, separator, comment, header, })
}
///|
fn csv_mode_text(mode : CSVMode?) -> String {
match mode {
None => ""
Some(mode) => {
let out = StringBuilder()
out.write_string(mode.format)
if mode.separator != (if mode.format == "csv" { ',' } else { '\t' }) {
out.write_string(" separator=")
out.write_char(mode.separator)
}
if mode.comment != '\u0000' {
out.write_string(" comment=")
out.write_char(mode.comment)
}
if mode.header {
out.write_string(" header")
}
out.to_string()
}
}
}
///|
// Record parsing consumes chunks incrementally. Delimiter and header options
// belong to the opened stream; changing INPUTMODE configures future streams.
fn MainInput::csv_peek(self : MainInput, st : State) -> Char? raise ParseError {
while self.start == self.data.length() && !self.eof {
self.more(st)
}
self.data.get(self.start)
}
///|
fn MainInput::csv_row(
self : MainInput,
st : State,
mode : CSVMode,
) -> (String, Array[Value])? raise ParseError {
if !self.csv_started {
if self.csv_peek(st) == Some('\ufeff') {
self.start += 1
}
self.csv_started = true
}
// Empty physical lines and comments are skipped only before a record starts.
while true {
let c = match self.csv_peek(st) {
None => return None
Some(c) => c
}
if c == mode.comment && mode.comment != '\u0000' {
while self.csv_peek(st) is Some(c) {
self.start += 1
st.tick()
if c == '\n' {
break
}
}
} else if c == '\n' {
self.start += 1
} else if c == '\r' {
self.start += 1
if self.csv_peek(st) == Some('\n') {
self.start += 1
} else if self.csv_peek(st) is None {
return None
} else {
// A bare CR belongs to a nonempty record. Keep it in the prefix.
return Some(self.csv_body(st, mode, ['\r']))
}
} else {
break
}
}
Some(self.csv_body(st, mode, []))
}
///|
fn MainInput::csv_body(
self : MainInput,
st : State,
mode : CSVMode,
prefix : Array[Char],
) -> (String, Array[Value]) raise ParseError {
let raw = prefix.copy()
let field = prefix.copy()
let fields = []
let mut quoted = false
let mut at_start = prefix.is_empty()
let mut strip_cr = false
let mut units = prefix.length()
while self.csv_peek(st) is Some(c) {
st.tick()
self.start += 1
units += if c.to_int() > 65535 { 2 } else { 1 }
if units > 1000000 {
raise Invalid("record length budget")
}
if at_start && c == '"' {
raw.push(c)
quoted = true
at_start = false
continue
}
if c == '\r' && self.csv_peek(st) is None {
raw.push(c)
break
}
if quoted && c == '"' {
raw.push(c)
let next = self.csv_peek(st)
if next == Some('"') {
self.start += 1
raw.push('"')
field.push('"')
units += 1
continue
}
if next == Some(mode.separator) || next == Some('\n') || next is None {
quoted = false
} else if next == Some('\r') {
self.start += 1
let after = self.csv_peek(st)
if after == Some('\n') || after is None {
if after is None {
raw.push('\r')
}
break
}
raw.push('\r')
field.push('"')
field.push('\r')
units += 1
} else {
field.push('"')
}
continue
}
if c == '\n' || (c == '\r' && self.csv_peek(st) == Some('\n')) {
if c == '\r' {
self.start += 1
}
if !quoted {
break
}
if c == '\r' {
raw.push('\r')
strip_cr = true
}
raw.push('\n')
field.push('\n')
at_start = false
continue
}
raw.push(c)
if !quoted && c == mode.separator {
fields.push(input_value(String::from_array(field)))
if fields.length() >= 10000 {
raise Invalid("field count budget")
}
field.clear()
at_start = true
} else {
field.push(c)
at_start = false
}
}
fields.push(input_value(String::from_array(field)))
let text = if strip_cr {
String::from_array(raw.filter(c => c != '\r'))
} else {
String::from_array(raw)
}
(text, fields)
}
///|
fn State::csv_header(
self : State,
fields : Array[Value],
) -> Unit raise ParseError {
self.field_names = Some(fields.map(v => v.text()))
let dest = self.arrays["FIELDS"]
self.entries -= dest.length()
dest.clear()
for i, value in fields {
self.put_element("FIELDS", (i + 1).to_string(), Text(value.text()))
}
}
///|
fn State::named_field(self : State, name : String) -> Value raise ParseError {
let names = match self.field_names {
None => raise Invalid("named fields require a CSV header")
Some(names) => names
}
let mut index = 0
for i, field in names {
if field == name {
index = i + 1
}
}
if index == 0 {
Text("")
} else {
self.read(FieldSlot(index))
}
}
///|
fn csv_record(
text : String,
mode : CSVMode,
st : State,
) -> (String, Array[Value])? raise ParseError {
let source : MainInput = {
reader: _ => End,
data: text.to_array(),
start: 0,
eof: true,
opened: false,
failure: None,
index: 0,
had_file: true,
mode: 4,
separator: '\n',
csv: Some(mode),
csv_started: false,
csv_header_done: true,
}
source.csv_row(st, mode)
}
///|
fn csv_fields(
text : String,
mode : CSVMode,
st : State,
) -> Array[Value] raise ParseError {
match csv_record(text, mode, st) {
Some((_, fields)) => fields
None => []
}
}
///|
fn csv_write(fields : Array[String], separator : Char) -> String {
let result = StringBuilder()
for i, field in fields {
if i > 0 {
result.write_char(separator)
}
let chars = field.to_array()
let quote = field == "\\." ||
chars.any(c => c == separator || c == '"' || c == '\r' || c == '\n') ||
(chars.get(0) is Some(c) && c.is_whitespace())
if quote {
result.write_char('"')
}
for c in chars {
result.write_char(c)
if quote && c == '"' {
result.write_char('"')
}
}
if quote {
result.write_char('"')
}
}
result.to_string()
}