#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cell<S> {
pub text: S,
pub row_span: u32,
pub col_span: u32,
}
impl<S> Cell<S> {
pub fn new(text: S, row_span: u32, col_span: u32) -> Self {
Self {
text,
row_span,
col_span,
}
}
pub fn single(text: S) -> Self {
Self::new(text, 1, 1)
}
}
pub fn to_csv<R, C, S>(rows: R) -> String
where
R: IntoIterator<Item = C>,
C: IntoIterator<Item = Cell<S>>,
S: AsRef<str>,
{
to_delimited(rows, ',')
}
pub fn to_delimited<R, C, S>(rows: R, delimiter: char) -> String
where
R: IntoIterator<Item = C>,
C: IntoIterator<Item = Cell<S>>,
S: AsRef<str>,
{
let mut out = String::new();
for record in grid(rows) {
for (i, field) in record.iter().enumerate() {
if i > 0 {
out.push(delimiter);
}
push_field(&mut out, field, delimiter);
}
out.push_str("\r\n");
}
out
}
fn push_field(out: &mut String, field: &str, delimiter: char) {
let quote = field.contains(delimiter)
|| field.contains('"')
|| field.contains('\n')
|| field.contains('\r');
if quote {
out.push('"');
out.push_str(&field.replace('"', "\"\""));
out.push('"');
} else {
out.push_str(field);
}
}
fn grid<R, C, S>(rows: R) -> Vec<Vec<String>>
where
R: IntoIterator<Item = C>,
C: IntoIterator<Item = Cell<S>>,
S: AsRef<str>,
{
let mut covered: Vec<usize> = Vec::new();
let mut grid: Vec<Vec<String>> = Vec::new();
for row in rows {
let mut record: Vec<String> = Vec::new();
let mut col = 0;
for cell in row {
skip_covered(&mut covered, &mut record, &mut col);
record.push(cell.text.as_ref().to_string());
let span = cell.col_span.max(1) as usize;
record.extend(std::iter::repeat_n(String::new(), span - 1));
if covered.len() < col + span {
covered.resize(col + span, 0);
}
for c in &mut covered[col..col + span] {
*c = cell.row_span.max(1) as usize - 1;
}
col += span;
}
while col < covered.len() {
if covered[col] > 0 {
covered[col] -= 1;
}
record.push(String::new());
col += 1;
}
grid.push(record);
}
let width = grid.iter().map(Vec::len).max().unwrap_or(0);
for record in &mut grid {
record.resize(width, String::new());
}
grid
}
fn skip_covered(covered: &mut [usize], record: &mut Vec<String>, col: &mut usize) {
while covered.get(*col).is_some_and(|&n| n > 0) {
covered[*col] -= 1;
record.push(String::new());
*col += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn csv(rows: &[&[Cell<&str>]]) -> String {
to_csv(rows.iter().map(|row| row.iter().copied()))
}
#[test]
fn plain_fields_are_written_as_they_are() {
assert_eq!(
csv(&[&[Cell::single("a"), Cell::single("b c")]]),
"a,b c\r\n"
);
}
#[test]
fn a_field_with_a_delimiter_quote_or_line_break_is_quoted() {
assert_eq!(
csv(&[&[
Cell::single("1,5"),
Cell::single("say \"hi\""),
Cell::single("two\nlines"),
Cell::single("cr\rhere"),
]]),
"\"1,5\",\"say \"\"hi\"\"\",\"two\nlines\",\"cr\rhere\"\r\n"
);
}
#[test]
fn the_delimiter_decides_what_needs_quoting() {
let row = [Cell::single("a,b"), Cell::single("c\td")];
assert_eq!(to_delimited([row], '\t'), "a,b\t\"c\td\"\r\n");
}
#[test]
fn a_horizontal_span_leaves_the_positions_it_covers_empty() {
assert_eq!(
csv(&[
&[Cell::new("wide", 1, 3)],
&[Cell::single("a"), Cell::single("b"), Cell::single("c")],
]),
"wide,,\r\na,b,c\r\n"
);
}
#[test]
fn a_vertical_span_pushes_the_rows_below_rightward() {
assert_eq!(
csv(&[
&[Cell::new("tall", 3, 1), Cell::single("a")],
&[Cell::single("b")],
&[Cell::single("c")],
&[Cell::single("d"), Cell::single("e")],
]),
"tall,a\r\n,b\r\n,c\r\nd,e\r\n"
);
}
#[test]
fn a_cell_merged_down_at_the_row_end_leaves_the_next_row_aligned() {
assert_eq!(
csv(&[
&[Cell::single("a"), Cell::new("tall", 2, 1)],
&[Cell::single("b")],
&[Cell::single("c"), Cell::single("d")],
]),
"a,tall\r\nb,\r\nc,d\r\n"
);
}
#[test]
fn a_block_merge_covers_rows_and_columns() {
assert_eq!(
csv(&[
&[Cell::new("block", 2, 2), Cell::single("x")],
&[Cell::single("y")],
&[Cell::single("p"), Cell::single("q"), Cell::single("r")],
]),
"block,,x\r\n,,y\r\np,q,r\r\n"
);
}
#[test]
fn short_rows_are_padded_to_the_widest() {
assert_eq!(
csv(&[
&[Cell::single("a")],
&[Cell::single("b"), Cell::single("c")]
]),
"a,\r\nb,c\r\n"
);
}
#[test]
fn a_zero_span_is_read_as_one() {
assert_eq!(
csv(&[
&[Cell::new("z", 0, 0), Cell::single("a")],
&[Cell::single("b"), Cell::single("c")]
]),
"z,a\r\nb,c\r\n"
);
}
#[test]
fn an_empty_table_is_empty_text() {
assert_eq!(csv(&[]), "");
}
#[test]
fn owned_text_works_as_well_as_borrowed() {
let rows = vec![vec![Cell::single(String::from("own"))]];
assert_eq!(to_csv(rows), "own\r\n");
}
}