///|
/// Represents a CSV (Comma-Separated Values) data structure with headers and
/// rows.
///
/// Parameters:
///
/// * `headers` : An array of strings representing the column names in the CSV.
/// * `rows` : A two-dimensional array where each inner array represents a row of
/// data, with each element corresponding to a column defined in the headers.
///
struct CSV {
headers : Array[String]
rows : Array[Array[String]]
}
///|
/// header of the CSV.
pub fn CSV::header(self : CSV) -> Array[String] {
self.headers
}
///|
/// data of the CSV.
pub fn CSV::data(self : CSV) -> Array[Array[String]] {
self.rows
}
///|
/// shape of the CSV.
pub fn CSV::shape(self : CSV) -> (Int, Int) {
let row_count = self.rows.length()
let column_count = if self.headers.length() > 0 {
self.headers.length()
} else if row_count > 0 {
self.rows[0].length()
} else {
0
}
(row_count, column_count)
}
///|
fn draw_border(logger : &Logger, column_widths : Array[Int]) -> Unit {
logger.write_string("+")
for width in column_widths {
logger.write_string("-".repeat(width + 2))
logger.write_string("+")
}
logger.write_string("\n")
}
///|
pub impl Show for CSV with output(self, logger) -> Unit {
if self.headers.is_empty() {
logger.write_string("")
} else {
let column_widths : Array[Int] = Array::make(self.headers.length(), 0)
// 计算表头宽度
for i in 0.. String {
if value.contains_any(chars=",\"\n\r") {
let escaped = value.replace_all(old="\"", new="\"\"")
let result = StringBuilder::new()
result.write_char('"')
result.write_string(escaped)
result.write_char('"')
result.to_string()
} else {
value
}
}
///|
fn write_csv_row(result : StringBuilder, row : Array[String]) -> Unit {
for i in 0.. 0 {
result.write_char(',')
}
result.write_string(escape_csv_field(row[i]))
}
result.write_char('\n')
}
///|
pub impl Show for CSV with to_string(self) -> String {
let result = StringBuilder::new()
// 添加表头
write_csv_row(result, self.headers)
// 添加数据行
for row in self.rows {
write_csv_row(result, row)
}
result.to_string()
}
///|
test "CSV::to_string/single_header_no_rows" {
let csv = { headers: ["Name"], rows: [] }
let expected =
#|Name
#|
inspect(csv.to_string(), content=expected)
}
///|
test "CSV::to_string/headers_and_rows" {
let csv = {
headers: ["Name", "Age", "City"],
rows: [["John", "25", "New York"], ["Jane", "30", "San Francisco"]],
}
let expected =
#|Name,Age,City
#|John,25,New York
#|Jane,30,San Francisco
#|
inspect(csv.to_string(), content=expected)
}
///|
/// Creates a new empty CSV structure with no headers and no rows.
///
/// Returns a new CSV instance with empty headers and rows arrays.
///
pub fn CSV::new() -> CSV {
let headers : Array[String] = Array::new()
let rows : Array[Array[String]] = Array::new()
{ headers, rows }
}
///|
pub fn CSV::from_array(
data : Array[Array[String]],
has_header? : Bool = true,
generate_headers? : Bool = true,
) -> CSV {
if data.is_empty() {
CSV::new()
} else if has_header {
let headers = data[0]
let rows = data[1:data.length()].iter().collect()
{ headers, rows }
} else if generate_headers {
let column_count = data[0].length()
let headers = Array::makei(column_count, fn(i) { "column\{i+1}" })
{ headers, rows: data }
} else {
let column_count = data[0].length()
let headers = Array::make(column_count, "")
{ headers, rows: data }
}
}
///|
/// Parses a CSV (Comma-Separated Values) string into a structured CSV object.
///
/// Parameters:
///
/// * `csv_string` : A string containing CSV data to be parsed.
/// * `options` : A configuration object of type `CSVOptions` that controls the
/// parsing behavior with the following fields:
/// * `delimiter` : The character used to separate fields (default: ',')
/// * `allow_newlines_in_quotes` : Whether to allow newlines within quoted
/// fields (default: true)
/// * `quote_char` : The character used for quoting fields (default: '"')
/// * `skip_empty_lines` : Whether to ignore empty lines in the input (default:
/// true)
/// * `trim_spaces` : Whether to remove leading and trailing whitespace from
/// fields (default: false)
///
/// Returns a `CSV` object containing the parsed data, with headers from the
/// first row and subsequent rows as data.
pub fn CSV::parse_string(
data : String,
options? : CSVOptions = {
delimiter: ',',
allow_newlines_in_quotes: true,
quote_char: '"',
skip_empty_lines: true,
trim_spaces: false,
},
) -> CSV {
let reader : StringReader = { source: data, pos: 0 }
CSV::from_array(parse(reader, options~))
}
///|
test "CSV::parse_buffer/basic" {
let buffer = @buffer.new()
let data =
#|header1,header2
#|value1,value2
#|
buffer.write_string(data)
let csv = CSV::parse_buffer(buffer)
inspect(csv.header(), content="[\"header1\", \"header2\"]")
inspect(csv.data(), content="[[\"value1\", \"value2\"]]")
}
///|
/// Parses a buffer containing CSV data into a structured CSV object.
///
/// Parameters:
///
/// * `buffer` : A buffer containing the CSV data to be parsed.
/// * `options` : A configuration object of type `CSVOptions` that controls
/// parsing behavior with the following fields:
/// * `delimiter` : The character used to separate fields (default: ',')
/// * `allow_newlines_in_quotes` : Whether to allow newlines within quoted
/// fields (default: true)
/// * `quote_char` : The character used for quoting fields (default: '"')
/// * `skip_empty_lines` : Whether to ignore empty lines in the input (default:
/// true)
/// * `trim_spaces` : Whether to remove leading and trailing whitespace from
/// fields (default: false)
///
/// Returns a `CSV` object containing the parsed data, with headers from the
/// first row and subsequent rows as data.
pub fn CSV::parse_buffer(
data : @buffer.Buffer,
options? : CSVOptions = {
delimiter: ',',
allow_newlines_in_quotes: true,
quote_char: '"',
skip_empty_lines: true,
trim_spaces: false,
},
) -> CSV {
let reader : StringReader = {
source: data.contents().to_unchecked_string(),
pos: 0,
}
CSV::from_array(parse(reader, options~))
}
///|
/// Parses binary data containing CSV content into a structured CSV object.
///
/// Parameters:
///
/// * `data` : A sequence of bytes containing the CSV content to be parsed.
/// * `options` : A configuration object that controls parsing behavior with the
/// following fields:
/// * `delimiter` : The character used to separate fields (default: ',')
/// * `allow_newlines_in_quotes` : Whether to allow newlines within quoted
/// fields (default: true)
/// * `quote_char` : The character used for quoting fields (default: '"')
/// * `skip_empty_lines` : Whether to ignore empty lines in the input (default:
/// true)
/// * `trim_spaces` : Whether to remove leading and trailing whitespace from
/// fields (default: false)
///
/// Returns a `CSV` object containing the parsed data, with headers from the
/// first row and subsequent rows as data.
pub fn CSV::parse_bytes(
data : Bytes,
options? : CSVOptions = {
delimiter: ',',
allow_newlines_in_quotes: true,
quote_char: '"',
skip_empty_lines: true,
trim_spaces: false,
},
) -> CSV {
let source = @encoding/utf8.decode_lossy(data)
let reader : StringReader = { source, pos: 0 }
CSV::from_array(parse(reader, options~))
}
///|
test "CSV::parse_bytes/basic_csv" {
let source =
#|header1,header2
#|value1,value2
#|
let data = @encoding/utf8.encode(source)
let csv = CSV::parse_bytes(data)
inspect(csv.header(), content="[\"header1\", \"header2\"]")
inspect(csv.data(), content="[[\"value1\", \"value2\"]]")
}