1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Cell<S> {
24 pub text: S,
26 pub row_span: u32,
28 pub col_span: u32,
30}
31
32impl<S> Cell<S> {
33 pub fn new(text: S, row_span: u32, col_span: u32) -> Self {
35 Self {
36 text,
37 row_span,
38 col_span,
39 }
40 }
41
42 pub fn single(text: S) -> Self {
44 Self::new(text, 1, 1)
45 }
46}
47
48pub fn to_csv<R, C, S>(rows: R) -> String
61where
62 R: IntoIterator<Item = C>,
63 C: IntoIterator<Item = Cell<S>>,
64 S: AsRef<str>,
65{
66 to_delimited(rows, ',')
67}
68
69pub fn to_delimited<R, C, S>(rows: R, delimiter: char) -> String
96where
97 R: IntoIterator<Item = C>,
98 C: IntoIterator<Item = Cell<S>>,
99 S: AsRef<str>,
100{
101 let mut out = String::new();
102 for record in grid(rows) {
103 for (i, field) in record.iter().enumerate() {
104 if i > 0 {
105 out.push(delimiter);
106 }
107 push_field(&mut out, field, delimiter);
108 }
109 out.push_str("\r\n");
110 }
111 out
112}
113
114fn push_field(out: &mut String, field: &str, delimiter: char) {
115 let quote = field.contains(delimiter)
116 || field.contains('"')
117 || field.contains('\n')
118 || field.contains('\r');
119 if quote {
120 out.push('"');
121 out.push_str(&field.replace('"', "\"\""));
122 out.push('"');
123 } else {
124 out.push_str(field);
125 }
126}
127
128fn grid<R, C, S>(rows: R) -> Vec<Vec<String>>
131where
132 R: IntoIterator<Item = C>,
133 C: IntoIterator<Item = Cell<S>>,
134 S: AsRef<str>,
135{
136 let rows: Vec<Vec<Cell<S>>> = rows.into_iter().map(|r| r.into_iter().collect()).collect();
137 let placement = crate::grid::place(rows.iter().map(|row| {
138 row.iter()
139 .map(|cell| crate::grid::Span::new(cell.row_span, cell.col_span))
140 }));
141 let mut grid = vec![vec![String::new(); placement.width]; rows.len()];
142 for ((record, row), columns) in grid.iter_mut().zip(&rows).zip(&placement.columns) {
143 for (cell, &col) in row.iter().zip(columns) {
144 record[col] = cell.text.as_ref().to_string();
145 }
146 }
147 grid
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn csv(rows: &[&[Cell<&str>]]) -> String {
155 to_csv(rows.iter().map(|row| row.iter().copied()))
156 }
157
158 #[test]
159 fn plain_fields_are_written_as_they_are() {
160 assert_eq!(
161 csv(&[&[Cell::single("a"), Cell::single("b c")]]),
162 "a,b c\r\n"
163 );
164 }
165
166 #[test]
167 fn a_field_with_a_delimiter_quote_or_line_break_is_quoted() {
168 assert_eq!(
169 csv(&[&[
170 Cell::single("1,5"),
171 Cell::single("say \"hi\""),
172 Cell::single("two\nlines"),
173 Cell::single("cr\rhere"),
174 ]]),
175 "\"1,5\",\"say \"\"hi\"\"\",\"two\nlines\",\"cr\rhere\"\r\n"
176 );
177 }
178
179 #[test]
180 fn the_delimiter_decides_what_needs_quoting() {
181 let row = [Cell::single("a,b"), Cell::single("c\td")];
182 assert_eq!(to_delimited([row], '\t'), "a,b\t\"c\td\"\r\n");
183 }
184
185 #[test]
186 fn a_horizontal_span_leaves_the_positions_it_covers_empty() {
187 assert_eq!(
188 csv(&[
189 &[Cell::new("wide", 1, 3)],
190 &[Cell::single("a"), Cell::single("b"), Cell::single("c")],
191 ]),
192 "wide,,\r\na,b,c\r\n"
193 );
194 }
195
196 #[test]
197 fn a_vertical_span_pushes_the_rows_below_rightward() {
198 assert_eq!(
200 csv(&[
201 &[Cell::new("tall", 3, 1), Cell::single("a")],
202 &[Cell::single("b")],
203 &[Cell::single("c")],
204 &[Cell::single("d"), Cell::single("e")],
205 ]),
206 "tall,a\r\n,b\r\n,c\r\nd,e\r\n"
207 );
208 }
209
210 #[test]
211 fn a_cell_merged_down_at_the_row_end_leaves_the_next_row_aligned() {
212 assert_eq!(
213 csv(&[
214 &[Cell::single("a"), Cell::new("tall", 2, 1)],
215 &[Cell::single("b")],
216 &[Cell::single("c"), Cell::single("d")],
217 ]),
218 "a,tall\r\nb,\r\nc,d\r\n"
219 );
220 }
221
222 #[test]
223 fn a_block_merge_covers_rows_and_columns() {
224 assert_eq!(
225 csv(&[
226 &[Cell::new("block", 2, 2), Cell::single("x")],
227 &[Cell::single("y")],
228 &[Cell::single("p"), Cell::single("q"), Cell::single("r")],
229 ]),
230 "block,,x\r\n,,y\r\np,q,r\r\n"
231 );
232 }
233
234 #[test]
235 fn short_rows_are_padded_to_the_widest() {
236 assert_eq!(
237 csv(&[
238 &[Cell::single("a")],
239 &[Cell::single("b"), Cell::single("c")]
240 ]),
241 "a,\r\nb,c\r\n"
242 );
243 }
244
245 #[test]
246 fn a_zero_span_is_read_as_one() {
247 assert_eq!(
248 csv(&[
249 &[Cell::new("z", 0, 0), Cell::single("a")],
250 &[Cell::single("b"), Cell::single("c")]
251 ]),
252 "z,a\r\nb,c\r\n"
253 );
254 }
255
256 #[test]
257 fn an_empty_table_is_empty_text() {
258 assert_eq!(csv(&[]), "");
259 }
260
261 #[test]
262 fn owned_text_works_as_well_as_borrowed() {
263 let rows = vec![vec![Cell::single(String::from("own"))]];
264 assert_eq!(to_csv(rows), "own\r\n");
265 }
266}