///|
/// 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) -> (Int32, Int32) {
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[Int32]) -> Unit {
logger.write_string("+")
for width in column_widths {
logger.write_string("-".repeat(width + 2))
logger.write_string("+")
}
logger.write_string("\n")
}
///|
pub extend CSV with Show::{to_string, output}
///|
pub impl Show for CSV with fn output(self, logger) -> Unit {
if self.headers.is_empty() {
logger.write_string("")
} else {
let column_widths : Array[Int32] = 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()
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 fn to_string(self) -> String {
let result = StringBuilder()
// 添加表头
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] = []
let rows : Array[Array[String]] = []
{ 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()
let data =
#|header1,header2
#|value1,value2
#|
buffer.write_string_utf16le(data)
let csv = CSV::parse_buffer(buffer)
assert_true(csv.header() == ["header1", "header2"])
assert_true(csv.data() == [["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)
assert_true(csv.header() == ["header1", "header2"])
assert_true(csv.data() == [["value1", "value2"]])
}