1const ROWS_PER_CHUNK: usize = 20;
13
14#[must_use]
16pub fn parse(input: &str, delimiter: char) -> Vec<Vec<String>> {
17 let mut rows = Vec::new();
18 let mut field = String::new();
19 let mut record: Vec<String> = Vec::new();
20 let mut in_quotes = false;
21 let mut chars = input.chars().peekable();
22
23 while let Some(ch) = chars.next() {
24 if in_quotes {
25 if ch == '"' {
26 if chars.peek() == Some(&'"') {
27 field.push('"');
28 chars.next();
29 } else {
30 in_quotes = false;
31 }
32 } else {
33 field.push(ch);
34 }
35 } else if ch == '"' {
36 in_quotes = true;
37 } else if ch == delimiter {
38 record.push(std::mem::take(&mut field));
39 } else if ch == '\n' || ch == '\r' {
40 if ch == '\r' && chars.peek() == Some(&'\n') {
42 chars.next();
43 }
44 record.push(std::mem::take(&mut field));
45 push_record(&mut rows, std::mem::take(&mut record));
46 } else {
47 field.push(ch);
48 }
49 }
50 if !field.is_empty() || !record.is_empty() {
52 record.push(field);
53 push_record(&mut rows, record);
54 }
55 rows
56}
57
58fn push_record(rows: &mut Vec<Vec<String>>, record: Vec<String>) {
60 if record.iter().any(|f| !f.trim().is_empty()) {
61 rows.push(record);
62 }
63}
64
65#[must_use]
67pub fn to_text(input: &str, delimiter: char) -> String {
68 let rows = parse(input, delimiter);
69 record_lines(&rows).join("\n")
70}
71
72#[must_use]
74pub fn chunks(input: &str, delimiter: char) -> Vec<String> {
75 let rows = parse(input, delimiter);
76 if rows.is_empty() {
77 return Vec::new();
78 }
79 let lines = record_lines(&rows);
80 if lines.is_empty() {
81 return vec![rows[0].join(&delimiter.to_string())];
83 }
84 let header_line = rows[0].join(&delimiter.to_string());
85 lines
86 .chunks(ROWS_PER_CHUNK)
87 .map(|group| format!("{}\n{}", header_line, group.join("\n")))
88 .collect()
89}
90
91fn record_lines(rows: &[Vec<String>]) -> Vec<String> {
93 if rows.len() < 2 {
94 return Vec::new();
95 }
96 let header = &rows[0];
97 rows[1..]
98 .iter()
99 .map(|row| {
100 row.iter()
101 .enumerate()
102 .map(|(i, val)| {
103 let col = header
104 .get(i)
105 .map_or_else(|| format!("col{i}"), |h| h.trim().to_string());
106 format!("{}: {}", col, val.trim())
107 })
108 .collect::<Vec<_>>()
109 .join(" | ")
110 })
111 .filter(|l| !l.trim().is_empty())
112 .collect()
113}
114
115#[cfg(test)]
116pub mod tests {
117 use super::*;
118
119 #[test]
120 fn parses_quoted_fields_with_embedded_delimiter() {
121 let rows = parse("a,b\n\"x,y\",\"he said \"\"hi\"\"\"\n", ',');
122 assert_eq!(rows.len(), 2);
123 assert_eq!(rows[1][0], "x,y");
124 assert_eq!(rows[1][1], "he said \"hi\"");
125 }
126
127 #[test]
128 fn renders_labeled_records() {
129 let text = to_text("name,age\nAlice,30\nBob,25", ',');
130 assert!(text.contains("name: Alice | age: 30"));
131 assert!(text.contains("name: Bob | age: 25"));
132 }
133
134 #[test]
135 fn chunks_repeat_header() {
136 let c = chunks("h1,h2\na,b\nc,d", ',');
137 assert_eq!(c.len(), 1);
138 assert!(c[0].starts_with("h1,h2\n"));
139 }
140
141 #[test]
142 fn header_only_input_still_chunks() {
143 let c = chunks("just,a,header", ',');
144 assert_eq!(c.len(), 1);
145 assert_eq!(c[0], "just,a,header");
146 }
147
148 #[test]
149 fn skips_blank_lines() {
150 let rows = parse("a\n\n\nb", ',');
151 assert_eq!(rows.len(), 2);
152 }
153}