1use std::borrow::Cow;
29use std::collections::VecDeque;
30use std::io::{BufRead, Read};
31
32use crate::{DataError, Row, RowSource, Rows, Value};
33
34pub const DELIMITERS: [char; 4] = [',', '\t', '|', ';'];
37
38pub const SAMPLE_CHARS: usize = 1024;
41
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub enum Header {
45 #[default]
48 Sniff,
49 Yes,
51 No,
53 UnlessNumeric,
58}
59
60#[derive(Clone, Copy, Debug, Default)]
62pub struct CsvReader {
63 dialect: Option<Dialect>,
64 fallback: Option<char>,
65 header: Header,
66}
67
68impl CsvReader {
69 pub fn new() -> Self {
73 CsvReader::default()
74 }
75
76 pub fn tsv() -> Self {
78 CsvReader::new().delimiter('\t')
79 }
80
81 pub fn delimiter(mut self, delimiter: char) -> Self {
83 self.dialect = Some(Dialect::excel(delimiter));
84 self
85 }
86
87 pub fn dialect(mut self, dialect: Dialect) -> Self {
89 self.dialect = Some(dialect);
90 self
91 }
92
93 pub fn fallback(mut self, delimiter: char) -> Self {
97 self.fallback = Some(delimiter);
98 self
99 }
100
101 pub fn header(mut self, header: bool) -> Self {
104 self.header = if header { Header::Yes } else { Header::No };
105 self
106 }
107
108 pub fn header_mode(mut self, header: Header) -> Self {
110 self.header = header;
111 self
112 }
113
114 pub fn detect(&self, content: &str) -> Option<(Dialect, bool)> {
118 let sample: String = content.chars().take(SAMPLE_CHARS).collect();
119 let dialect = match self.dialect {
120 Some(dialect) => dialect,
121 None => {
122 sniff(&sample, Some(&DELIMITERS)).or_else(|| self.fallback.map(Dialect::excel))?
123 }
124 };
125 let header = match self.header {
126 Header::Yes => true,
127 Header::No => false,
128 Header::Sniff => has_header(&sample).unwrap_or(true),
129 Header::UnlessNumeric => {
130 let first = read_rows(&sample, &dialect)
131 .into_iter()
132 .find(|row| !row.is_empty())
133 .unwrap_or_default();
134 first.iter().any(|cell| {
135 let cell = cell.trim();
136 !cell.is_empty() && cell.parse::<f64>().is_err()
137 })
138 }
139 };
140 Some((dialect, header))
141 }
142
143 pub fn read(&self, content: &str) -> Result<Rows, DataError> {
147 let content = universal_newlines(content);
148 let content = content.strip_prefix('\u{feff}').unwrap_or(&content);
150 let (dialect, header) = self
151 .detect(content)
152 .ok_or_else(|| DataError::new("Could not determine delimiter"))?;
153 let mut records = parse_rows(content, &dialect)
154 .into_iter()
155 .filter(|row| !row.is_empty());
156 let names = if header {
157 records.next().unwrap_or_default()
158 } else {
159 Vec::new()
160 };
161 let records: Vec<Vec<String>> = records.collect();
162 let width = records
163 .iter()
164 .map(Vec::len)
165 .max()
166 .unwrap_or(0)
167 .max(names.len());
168 let columns: Vec<String> = (0..width)
169 .map(|i| names.get(i).cloned().unwrap_or_else(|| (i + 1).to_string()))
170 .collect();
171 let mut rows = Rows::new(columns);
172 for record in records {
173 rows.push(record.into_iter().map(Value::Str));
174 }
175 Ok(rows)
176 }
177}
178
179impl CsvReader {
180 pub fn source<R: BufRead>(&self, reader: R) -> Result<CsvSource<R>, DataError> {
207 let mut source = CsvSource {
208 reader,
209 dialect: Dialect::excel(','),
210 columns: Vec::new(),
211 pending: VecDeque::new(),
212 record: String::new(),
213 state: State::StartRecord,
214 line: 0,
215 done: false,
216 longer: 0,
217 };
218 let mut prefix = String::new();
220 while prefix.chars().count() < SAMPLE_CHARS {
221 match source.read_line()? {
222 Some(line) => prefix.push_str(&line),
223 None => break,
224 }
225 }
226 let prefix = universal_newlines(&prefix);
227 let prefix = prefix.strip_prefix('\u{feff}').unwrap_or(&prefix);
228 let (dialect, header) = self
229 .detect(prefix)
230 .ok_or_else(|| DataError::new("Could not determine delimiter"))?;
231 source.dialect = dialect;
232 source.feed(prefix)?;
233 if !matches!(source.state, State::StartRecord) {
235 source.fill_record()?;
236 }
237 if source.done {
238 source.flush_record();
239 }
240 let names = if header {
241 source.pending.pop_front().unwrap_or_default()
242 } else {
243 Vec::new()
244 };
245 let width = source
246 .pending
247 .iter()
248 .map(Vec::len)
249 .max()
250 .unwrap_or(0)
251 .max(names.len());
252 source.columns = (0..width)
253 .map(|i| names.get(i).cloned().unwrap_or_else(|| (i + 1).to_string()))
254 .collect();
255 Ok(source)
256 }
257}
258
259pub const MAX_RECORD: usize = 64 << 20;
263
264#[derive(Debug)]
267pub struct CsvSource<R> {
268 reader: R,
269 dialect: Dialect,
270 columns: Vec<String>,
271 pending: VecDeque<Vec<String>>,
273 record: String,
275 state: State,
277 line: usize,
278 done: bool,
279 longer: usize,
280}
281
282impl<R: BufRead> CsvSource<R> {
283 pub fn dialect(&self) -> Dialect {
285 self.dialect
286 }
287
288 pub fn longer_rows(&self) -> usize {
291 self.longer
292 }
293
294 fn read_line(&mut self) -> Result<Option<String>, DataError> {
296 if self.done {
297 return Ok(None);
298 }
299 let mut buffer = Vec::new();
300 let read = (&mut self.reader)
301 .take(MAX_RECORD as u64 + 1)
302 .read_until(b'\n', &mut buffer)
303 .map_err(|e| {
304 self.done = true;
305 DataError::at(self.line + 1, e.to_string())
306 })?;
307 if read == 0 {
308 self.done = true;
309 return Ok(None);
310 }
311 self.line += 1;
312 if buffer.len() > MAX_RECORD {
313 self.done = true;
314 return Err(DataError::at(
315 self.line,
316 format!("longer than {MAX_RECORD} bytes"),
317 ));
318 }
319 Ok(Some(String::from_utf8_lossy(&buffer).into_owned()))
320 }
321
322 fn feed(&mut self, text: &str) -> Result<(), DataError> {
325 for c in text.chars() {
326 self.state = advance(self.state, c, &self.dialect);
327 self.record.push(c);
328 if matches!(self.state, State::StartRecord) {
329 self.flush_record();
330 }
331 }
332 if self.record.len() > MAX_RECORD {
333 self.done = true;
334 return Err(DataError::at(
335 self.line,
336 format!("a quoted field runs past {MAX_RECORD} bytes"),
337 ));
338 }
339 Ok(())
340 }
341
342 fn fill_record(&mut self) -> Result<(), DataError> {
344 while let Some(line) = self.read_line()? {
345 self.feed(&universal_newlines(&line))?;
346 if matches!(self.state, State::StartRecord) {
347 break;
348 }
349 }
350 Ok(())
351 }
352
353 fn flush_record(&mut self) {
355 if self.record.is_empty() {
356 return;
357 }
358 let text = std::mem::take(&mut self.record);
359 self.state = State::StartRecord;
360 self.pending.extend(
361 parse_rows(&text, &self.dialect)
362 .into_iter()
363 .filter(|row| !row.is_empty()),
364 );
365 }
366}
367
368impl<R: BufRead> RowSource for CsvSource<R> {
369 fn columns(&self) -> &[String] {
370 &self.columns
371 }
372
373 fn next_row(&mut self) -> Option<Result<Row, DataError>> {
374 while self.pending.is_empty() {
375 if self.done {
376 self.flush_record();
379 if self.pending.is_empty() {
380 return None;
381 }
382 break;
383 }
384 if let Err(error) = self.fill_record() {
385 return Some(Err(error));
386 }
387 }
388 let record = self.pending.pop_front()?;
389 let width = self.columns.len();
390 if record.len() > width {
391 self.longer += 1;
392 }
393 let mut row: Row = record.into_iter().take(width).map(Value::Str).collect();
394 row.resize(width, Value::Null);
395 Some(Ok(row))
396 }
397}
398
399fn advance(state: State, c: char, dialect: &Dialect) -> State {
402 match state {
403 State::StartRecord | State::StartField => {
404 if c == '\n' {
405 State::StartRecord
406 } else if c == dialect.quotechar {
407 State::InQuotedField
408 } else if c == dialect.delimiter || (c == ' ' && dialect.skipinitialspace) {
409 State::StartField
410 } else {
411 State::InField
412 }
413 }
414 State::InField => match c {
415 '\n' => State::StartRecord,
416 c if c == dialect.delimiter => State::StartField,
417 _ => State::InField,
418 },
419 State::InQuotedField => {
420 if c != dialect.quotechar {
421 State::InQuotedField
422 } else if dialect.doublequote {
423 State::QuoteInQuotedField
424 } else {
425 State::InField
426 }
427 }
428 State::QuoteInQuotedField => match c {
429 c if c == dialect.quotechar => State::InQuotedField,
430 c if c == dialect.delimiter => State::StartField,
431 '\n' => State::StartRecord,
432 _ => State::InField,
433 },
434 }
435}
436
437fn universal_newlines(content: &str) -> Cow<'_, str> {
439 if content.contains('\r') {
440 Cow::Owned(content.replace("\r\n", "\n").replace('\r', "\n"))
441 } else {
442 Cow::Borrowed(content)
443 }
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448pub struct Dialect {
449 pub delimiter: char,
451 pub quotechar: char,
453 pub doublequote: bool,
455 pub skipinitialspace: bool,
457}
458
459impl Dialect {
460 pub fn excel(delimiter: char) -> Self {
463 Dialect {
464 delimiter,
465 quotechar: '"',
466 doublequote: true,
467 skipinitialspace: false,
468 }
469 }
470}
471
472fn is_word_char(c: char) -> bool {
474 c.is_alphanumeric() || c == '_'
475}
476
477fn is_delimiter_char(c: char) -> bool {
479 !is_word_char(c) && c != '\n' && c != '"' && c != '\''
480}
481
482fn is_quote_char(c: char) -> bool {
484 c == '"' || c == '\''
485}
486
487struct QuoteHit {
491 quote: char,
492 delim: Option<char>,
493 space: bool,
494}
495
496pub fn sniff(sample: &str, delimiters: Option<&[char]>) -> Option<Dialect> {
509 let data: Vec<char> = sample.chars().collect();
510 let (quote, mut delimiter, mut skipinitialspace) = guess_quote_and_delimiter(&data, delimiters);
511 if delimiter.is_none() {
512 let (guessed, spaced) = guess_delimiter(sample, delimiters);
513 delimiter = guessed;
514 skipinitialspace = spaced;
515 }
516 Some(Dialect {
517 delimiter: delimiter?,
518 quotechar: quote.unwrap_or('"'),
521 doublequote: true,
522 skipinitialspace,
523 })
524}
525
526fn guess_quote_and_delimiter(
534 data: &[char],
535 delimiters: Option<&[char]>,
536) -> (Option<char>, Option<char>, bool) {
537 let hits = quote_hits(data);
538 if hits.is_empty() {
539 return (None, None, false);
540 }
541
542 let mut quotes: Vec<(char, usize)> = Vec::new();
546 let mut delims: Vec<(char, usize)> = Vec::new();
547 let mut spaces = 0usize;
548 fn bump(table: &mut Vec<(char, usize)>, key: char) {
549 match table.iter_mut().find(|(existing, _)| *existing == key) {
550 Some(entry) => entry.1 += 1,
551 None => table.push((key, 1)),
552 }
553 }
554 for hit in &hits {
555 bump(&mut quotes, hit.quote);
556 let Some(delim) = hit.delim else { continue };
559 if delimiters.is_none_or(|allowed| allowed.contains(&delim)) {
560 bump(&mut delims, delim);
561 }
562 if hit.space {
563 spaces += 1;
564 }
565 }
566
567 let first_max = |table: &[(char, usize)]| -> Option<(char, usize)> {
568 let mut best: Option<(char, usize)> = None;
569 for &(key, count) in table {
570 if best.is_none_or(|(_, seen)| count > seen) {
571 best = Some((key, count));
572 }
573 }
574 best
575 };
576 let quotechar = first_max("es).map(|(key, _)| key);
577 match first_max(&delims) {
578 Some((delim, count)) => (quotechar, Some(delim), count == spaces),
579 None => (quotechar, None, false),
581 }
582}
583
584type QuoteScan = fn(&[char]) -> Vec<QuoteHit>;
586
587fn quote_hits(data: &[char]) -> Vec<QuoteHit> {
590 let scans: [QuoteScan; 4] = [
591 scan_delim_quote_delim,
592 scan_line_quote_delim,
593 scan_delim_quote_line,
594 scan_line_quote_line,
595 ];
596 for scan in scans {
597 let hits = scan(data);
598 if !hits.is_empty() {
599 return hits;
600 }
601 }
602 Vec::new()
603}
604
605fn scan_delim_quote_delim(data: &[char]) -> Vec<QuoteHit> {
610 scan_delim_quote(data, |data, quote, delim, from| {
611 let mut k = from;
612 while k + 1 < data.len() {
613 if data[k] == quote && data[k + 1] == delim {
614 return Some(k + 2);
615 }
616 k += 1;
617 }
618 None
619 })
620}
621
622fn scan_delim_quote_line(data: &[char]) -> Vec<QuoteHit> {
625 scan_delim_quote(data, |data, quote, _delim, from| {
626 let mut k = from;
627 while k < data.len() {
628 if data[k] == quote && (k + 1 == data.len() || data[k + 1] == '\n') {
629 return Some(k + 1);
632 }
633 k += 1;
634 }
635 None
636 })
637}
638
639fn scan_delim_quote(
642 data: &[char],
643 close: fn(&[char], char, char, usize) -> Option<usize>,
644) -> Vec<QuoteHit> {
645 let mut hits = Vec::new();
646 let mut i = 0;
647 while i < data.len() {
648 if is_delimiter_char(data[i]) {
649 let delim = data[i];
650 let mut j = i + 1;
651 let mut space = false;
652 if j < data.len() && data[j] == ' ' {
653 space = true;
654 j += 1;
655 }
656 if j < data.len() && is_quote_char(data[j]) {
657 let quote = data[j];
658 if let Some(end) = close(data, quote, delim, j + 1) {
659 hits.push(QuoteHit {
660 quote,
661 delim: Some(delim),
662 space,
663 });
664 i = end;
665 continue;
666 }
667 }
668 }
669 i += 1;
670 }
671 hits
672}
673
674fn scan_line_quote_delim(data: &[char]) -> Vec<QuoteHit> {
677 scan_line_quote(data, |data, quote, from| {
678 let mut k = from;
679 while k + 1 < data.len() {
680 if data[k] == quote && is_delimiter_char(data[k + 1]) {
681 let space = k + 2 < data.len() && data[k + 2] == ' ';
682 return Some((Some(data[k + 1]), space, if space { k + 3 } else { k + 2 }));
683 }
684 k += 1;
685 }
686 None
687 })
688}
689
690fn scan_line_quote_line(data: &[char]) -> Vec<QuoteHit> {
693 scan_line_quote(data, |data, quote, from| {
694 let mut k = from;
695 while k < data.len() {
696 if data[k] == quote && (k + 1 == data.len() || data[k + 1] == '\n') {
697 return Some((None, false, k + 1));
698 }
699 k += 1;
700 }
701 None
702 })
703}
704
705#[allow(clippy::type_complexity)]
708fn scan_line_quote(
709 data: &[char],
710 close: fn(&[char], char, usize) -> Option<(Option<char>, bool, usize)>,
711) -> Vec<QuoteHit> {
712 let mut hits = Vec::new();
713 let mut i = 0;
714 while i < data.len() {
715 let mut starts: Vec<usize> = Vec::new();
719 if i == 0 || data[i - 1] == '\n' {
720 starts.push(i);
721 }
722 if data[i] == '\n' {
723 starts.push(i + 1);
724 }
725 let mut advanced = false;
726 for quote_at in starts {
727 if quote_at >= data.len() || !is_quote_char(data[quote_at]) {
728 continue;
729 }
730 let quote = data[quote_at];
731 if let Some((delim, space, end)) = close(data, quote, quote_at + 1) {
732 hits.push(QuoteHit {
733 quote,
734 delim,
735 space,
736 });
737 i = end;
738 advanced = true;
739 break;
740 }
741 }
742 if !advanced {
743 i += 1;
744 }
745 }
746 hits
747}
748
749fn guess_delimiter(sample: &str, delimiters: Option<&[char]>) -> (Option<char>, bool) {
752 let data: Vec<&str> = sample.split('\n').filter(|line| !line.is_empty()).collect();
754 if data.is_empty() {
755 return (None, false);
756 }
757 const ASCII: usize = 127;
759
760 let chunk_length = 10.min(data.len());
761 let mut iteration = 0usize;
762 let mut char_frequency: Vec<Vec<(usize, usize)>> = vec![Vec::new(); ASCII];
765 let mut modes: Vec<Option<(usize, isize)>> = vec![None; ASCII];
768 let mut delims: Vec<(char, (usize, isize))> = Vec::new();
769
770 let (mut start, mut end) = (0usize, chunk_length);
771 while start < data.len() {
772 iteration += 1;
773 for line in &data[start..end.min(data.len())] {
774 for (code, table) in char_frequency.iter_mut().enumerate() {
775 let c = code as u8 as char;
776 let freq = line.matches(c).count();
779 match table.iter_mut().find(|(seen, _)| *seen == freq) {
780 Some(entry) => entry.1 += 1,
781 None => table.push((freq, 1)),
782 }
783 }
784 }
785
786 for (code, items) in char_frequency.iter().enumerate() {
787 if items.len() == 1 && items[0].0 == 0 {
788 continue;
789 }
790 if items.len() > 1 {
791 let mut best = 0usize;
794 for (index, item) in items.iter().enumerate() {
795 if item.1 > items[best].1 {
796 best = index;
797 }
798 }
799 let others: usize = items
800 .iter()
801 .enumerate()
802 .filter(|(index, _)| *index != best)
803 .map(|(_, item)| item.1)
804 .sum();
805 modes[code] = Some((items[best].0, items[best].1 as isize - others as isize));
806 } else if let Some(&(freq, count)) = items.first() {
807 modes[code] = Some((freq, count as isize));
808 }
809 }
810
811 let total = (chunk_length * iteration).min(data.len()) as f64;
812 let mut consistency = 1.0f64;
813 let threshold = 0.9f64;
814 while delims.is_empty() && consistency >= threshold {
815 for (code, mode) in modes.iter().enumerate() {
816 let Some((freq, count)) = *mode else { continue };
817 let c = code as u8 as char;
818 if freq > 0
819 && count > 0
820 && (count as f64 / total) >= consistency
821 && delimiters.is_none_or(|allowed| allowed.contains(&c))
822 {
823 delims.push((c, (freq, count)));
824 }
825 }
826 consistency -= 0.01;
827 }
828
829 if delims.len() == 1 {
830 let delim = delims[0].0;
831 return (Some(delim), saw_space_after(data[0], delim));
832 }
833
834 start = end;
835 end += chunk_length;
836 }
837
838 if delims.is_empty() {
839 return (None, false);
840 }
841 if delims.len() > 1 {
842 for preferred in [',', '\t', ';', ' ', ':'] {
843 if delims.iter().any(|(c, _)| *c == preferred) {
844 return (Some(preferred), saw_space_after(data[0], preferred));
845 }
846 }
847 }
848 let best = delims
850 .iter()
851 .max_by_key(|(c, mode)| (*mode, *c))
852 .expect("delims is non-empty");
853 (Some(best.0), saw_space_after(data[0], best.0))
854}
855
856fn saw_space_after(line: &str, delimiter: char) -> bool {
859 line.matches(delimiter).count() == line.matches(&format!("{delimiter} ")).count()
860}
861
862#[derive(Debug, Clone, Copy, PartialEq, Eq)]
864enum ColumnType {
865 Number,
867 Length(usize),
869}
870
871#[derive(Debug, Clone, Copy)]
873enum ColumnVote {
874 Untyped,
876 Typed(ColumnType),
877 Dropped,
879}
880
881pub fn has_header(sample: &str) -> Option<bool> {
893 let dialect = sniff(sample, None)?;
896 let mut rows = read_rows(sample, &dialect).into_iter();
897 let header = rows.next()?;
898 let columns = header.len();
899 let mut votes = vec![ColumnVote::Untyped; columns];
900
901 for (checked, row) in rows.enumerate() {
902 if checked > 20 {
905 break;
906 }
907 if row.len() != columns {
908 continue;
909 }
910 for (col, vote) in votes.iter_mut().enumerate() {
911 if matches!(vote, ColumnVote::Dropped) {
912 continue;
913 }
914 let this = if parses_as_complex(&row[col]) {
915 ColumnType::Number
916 } else {
917 ColumnType::Length(row[col].chars().count())
918 };
919 match vote {
920 ColumnVote::Untyped => *vote = ColumnVote::Typed(this),
921 ColumnVote::Typed(known) if *known != this => *vote = ColumnVote::Dropped,
922 _ => {}
923 }
924 }
925 }
926
927 let mut tally = 0isize;
928 for (col, vote) in votes.iter().enumerate() {
929 match vote {
930 ColumnVote::Dropped => {}
931 ColumnVote::Untyped => tally += 1,
934 ColumnVote::Typed(ColumnType::Length(len)) => {
935 if header[col].chars().count() == *len {
936 tally -= 1;
937 } else {
938 tally += 1;
939 }
940 }
941 ColumnVote::Typed(ColumnType::Number) => {
942 if parses_as_complex(&header[col]) {
943 tally -= 1;
944 } else {
945 tally += 1;
946 }
947 }
948 }
949 }
950 Some(tally > 0)
951}
952
953fn parses_as_complex(value: &str) -> bool {
963 let trimmed = value.trim();
964 let body = match trimmed.strip_prefix('(') {
965 Some(inner) => match inner.strip_suffix(')') {
966 Some(inner) => inner.trim(),
967 None => return false,
968 },
969 None if trimmed.ends_with(')') => return false,
970 None => trimmed,
971 };
972 if body.is_empty() {
973 return false;
974 }
975 let chars: Vec<char> = body.chars().collect();
978 let split = (1..chars.len())
979 .find(|&i| matches!(chars[i], '+' | '-') && !matches!(chars[i - 1], 'e' | 'E'));
980 match split {
981 Some(index) => {
982 let at: usize = chars[..index].iter().map(|c| c.len_utf8()).sum();
983 let (real, imaginary) = body.split_at(at);
984 is_float_literal(real) && is_imaginary_literal(imaginary)
985 }
986 None => is_float_literal(body) || is_imaginary_literal(body),
987 }
988}
989
990fn is_float_literal(value: &str) -> bool {
993 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
994 if matches!(
995 body.to_ascii_lowercase().as_str(),
996 "inf" | "infinity" | "nan"
997 ) {
998 return true;
999 }
1000 let chars: Vec<char> = body.chars().collect();
1001 let mut index = 0usize;
1002 let mut digits = 0usize;
1003 let take_digits = |index: &mut usize, digits: &mut usize| {
1004 while *index < chars.len() && (chars[*index].is_ascii_digit() || chars[*index] == '_') {
1005 if chars[*index].is_ascii_digit() {
1006 *digits += 1;
1007 }
1008 *index += 1;
1009 }
1010 };
1011 take_digits(&mut index, &mut digits);
1012 if index < chars.len() && chars[index] == '.' {
1013 index += 1;
1014 take_digits(&mut index, &mut digits);
1015 }
1016 if digits == 0 {
1017 return false;
1018 }
1019 if index < chars.len() && (chars[index] == 'e' || chars[index] == 'E') {
1020 index += 1;
1021 if index < chars.len() && matches!(chars[index], '+' | '-') {
1022 index += 1;
1023 }
1024 let mut exponent = 0usize;
1025 take_digits(&mut index, &mut exponent);
1026 if exponent == 0 {
1027 return false;
1028 }
1029 }
1030 index == chars.len()
1031}
1032
1033fn is_imaginary_literal(value: &str) -> bool {
1035 let Some(body) = value.strip_suffix(['j', 'J']) else {
1036 return false;
1037 };
1038 matches!(body, "" | "+" | "-") || is_float_literal(body)
1039}
1040
1041#[derive(Clone, Copy, Debug)]
1044enum State {
1045 StartRecord,
1046 StartField,
1047 InField,
1048 InQuotedField,
1049 QuoteInQuotedField,
1050}
1051
1052pub fn read_rows(content: &str, dialect: &Dialect) -> Vec<Vec<String>> {
1061 parse_rows(content.strip_prefix('\u{feff}').unwrap_or(content), dialect)
1063}
1064
1065fn parse_rows(content: &str, dialect: &Dialect) -> Vec<Vec<String>> {
1068 let mut rows: Vec<Vec<String>> = Vec::new();
1069 let mut row: Vec<String> = Vec::new();
1070 let mut field = String::new();
1071 let mut state = State::StartRecord;
1072
1073 for c in content.chars() {
1074 match state {
1075 State::StartRecord => {
1076 if c == '\n' {
1077 rows.push(Vec::new());
1078 } else {
1079 state = State::StartField;
1080 read_csv_char(c, dialect, &mut state, &mut field, &mut row, &mut rows);
1082 }
1083 }
1084 _ => read_csv_char(c, dialect, &mut state, &mut field, &mut row, &mut rows),
1085 }
1086 }
1087 if !matches!(state, State::StartRecord) {
1088 row.push(std::mem::take(&mut field));
1089 row.shrink_to_fit();
1090 rows.push(std::mem::take(&mut row));
1091 }
1092 rows
1093}
1094
1095fn read_csv_char(
1098 c: char,
1099 dialect: &Dialect,
1100 state: &mut State,
1101 field: &mut String,
1102 row: &mut Vec<String>,
1103 rows: &mut Vec<Vec<String>>,
1104) {
1105 let end_record = |field: &mut String, row: &mut Vec<String>, rows: &mut Vec<Vec<String>>| {
1106 row.push(std::mem::take(field));
1107 row.shrink_to_fit();
1109 rows.push(std::mem::take(row));
1110 };
1111 match state {
1112 State::StartRecord => unreachable!("handled by the caller"),
1113 State::StartField => {
1114 if c == '\n' {
1115 end_record(field, row, rows);
1116 *state = State::StartRecord;
1117 } else if c == dialect.quotechar {
1118 *state = State::InQuotedField;
1119 } else if c == ' ' && dialect.skipinitialspace {
1120 } else if c == dialect.delimiter {
1122 row.push(std::mem::take(field));
1123 } else {
1124 field.push(c);
1125 *state = State::InField;
1126 }
1127 }
1128 State::InField => {
1129 if c == '\n' {
1130 end_record(field, row, rows);
1131 *state = State::StartRecord;
1132 } else if c == dialect.delimiter {
1133 row.push(std::mem::take(field));
1134 *state = State::StartField;
1135 } else {
1136 field.push(c);
1137 }
1138 }
1139 State::InQuotedField => {
1140 if c == dialect.quotechar {
1141 *state = if dialect.doublequote {
1142 State::QuoteInQuotedField
1143 } else {
1144 State::InField
1147 };
1148 } else {
1149 field.push(c);
1150 }
1151 }
1152 State::QuoteInQuotedField => {
1153 if c == dialect.quotechar {
1154 field.push(c);
1155 *state = State::InQuotedField;
1156 } else if c == dialect.delimiter {
1157 row.push(std::mem::take(field));
1158 *state = State::StartField;
1159 } else if c == '\n' {
1160 end_record(field, row, rows);
1161 *state = State::StartRecord;
1162 } else {
1163 field.push(c);
1164 *state = State::InField;
1165 }
1166 }
1167 }
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172 use super::*;
1173
1174 #[test]
1175 fn read_rows_handles_quotes_and_delimiters() {
1176 let comma = Dialect::excel(',');
1177 let rows = read_rows("a,\"b,c\",d\n\"he said \"\"hi\"\"\",2\n", &comma);
1179 assert_eq!(
1180 rows,
1181 vec![
1182 vec!["a".to_string(), "b,c".to_string(), "d".to_string()],
1183 vec!["he said \"hi\"".to_string(), "2".to_string()],
1184 ]
1185 );
1186 assert_eq!(read_rows("x\ny\n", &comma), vec![vec!["x"], vec!["y"]]);
1189 assert_eq!(
1191 read_rows("a\tb", &Dialect::excel('\t')),
1192 vec![vec!["a", "b"]]
1193 );
1194 assert_eq!(read_rows("\u{feff}a,b", &comma), vec![vec!["a", "b"]]);
1196 assert_eq!(
1199 read_rows("a,b\n\nc,d\n", &comma),
1200 vec![vec!["a", "b"], vec![], vec!["c", "d"]]
1201 );
1202 let padded = Dialect {
1204 skipinitialspace: true,
1205 ..Dialect::excel(',')
1206 };
1207 assert_eq!(read_rows("a, b , c", &padded), vec![vec!["a", "b ", "c"]]);
1208 }
1209
1210 #[test]
1211 fn the_sniffer_finds_the_delimiter_and_the_header() {
1212 let candidates = [',', '\t', '|', ';'];
1214 let sniffed =
1215 |sample: &str| sniff(sample, Some(&candidates)).map(|dialect| dialect.delimiter);
1216 assert_eq!(
1217 sniffed("name;age;city\nAlice;30;Paris\nBob;25;Lyon\n"),
1218 Some(';')
1219 );
1220 assert_eq!(sniffed("a|b|c\n1|2|3\n4|5|6\n"), Some('|'));
1221 assert_eq!(sniffed("a\tb\tc\n1\t2\t3\n4\t5\t6\n"), Some('\t'));
1222 assert_eq!(sniffed("a, b, c\n1, 2, 3\n4, 5, 6\n"), Some(','));
1223 assert_eq!(
1226 sniffed("name,bio\nAlice,\"line one\nline two\"\nBob,short\n"),
1227 Some(',')
1228 );
1229 assert_eq!(sniffed("a,b,c\n1,2\n3,4,5,6\n"), None);
1232 assert_eq!(sniffed("alpha\nbeta\ngamma\n"), None);
1233
1234 assert_eq!(has_header("name,age\nAlice,30\nBob,25\n"), Some(true));
1235 assert_eq!(has_header("price,note\n10,aa\n9.5,bbb\n"), Some(true));
1239 assert_eq!(has_header("1,2,3\n4,5,6\n7,8,9\n"), Some(false));
1240 }
1241 #[test]
1242 fn the_reader_sniffs_pads_and_names_columns() {
1243 let rows = CsvReader::new()
1244 .delimiter(',')
1245 .header(true)
1246 .read("a,b\r\n1,2,3\r\n\r\n4\r\n")
1247 .unwrap();
1248 assert_eq!(rows.columns(), ["a", "b", "3"]);
1249 assert_eq!(
1250 rows.rows()[0],
1251 [Value::from("1"), Value::from("2"), Value::from("3")]
1252 );
1253 assert_eq!(rows.rows()[1], [Value::from("4"), Value::Null, Value::Null]);
1254 let rows = CsvReader::new().read("1,2,3\n4,5,6\n7,8,9\n").unwrap();
1256 assert_eq!(rows.columns(), ["1", "2", "3"]);
1257 assert_eq!(rows.len(), 3);
1258 }
1259
1260 #[test]
1261 fn the_reader_fails_where_rich_csv_does_unless_given_a_fallback() {
1262 let error = CsvReader::new().read("alpha\nbeta\n").unwrap_err();
1263 assert_eq!(error.to_string(), "Could not determine delimiter");
1264 let rows = CsvReader::new()
1265 .fallback(',')
1266 .read("alpha\nbeta\n")
1267 .unwrap();
1268 assert_eq!(rows.columns(), ["alpha"]);
1269 assert_eq!(rows.len(), 1);
1270 let rows = CsvReader::tsv().read("x\ty\n1\t2\n").unwrap();
1271 assert_eq!(rows.columns(), ["x", "y"]);
1272 }
1273
1274 #[test]
1278 fn streaming_strips_only_a_leading_byte_order_mark() {
1279 let text = "\u{feff}a,b\n1,2\r\u{feff}x,y\n\u{feff}3,4\n";
1280 let read = CsvReader::new().header(true).read(text).unwrap();
1281 let streamed = CsvReader::new()
1282 .header(true)
1283 .source(text.as_bytes())
1284 .unwrap()
1285 .collect_rows()
1286 .unwrap();
1287 assert_eq!(streamed.columns(), ["a", "b"]);
1288 assert_eq!(streamed.rows()[1][0], Value::from("\u{feff}x"));
1289 assert_eq!(streamed.rows(), read.rows());
1290 }
1291
1292 #[test]
1296 fn read_and_source_sniff_past_a_byte_order_mark_alike() {
1297 for text in [
1298 "\u{feff}\r\nx|y;z\tw w\n",
1299 "\u{feff}\nname,age\nAda,36\n",
1300 "\u{feff}ab,c\nxy,1\nzw,2\n",
1301 "\u{feff}\u{feff}ab,c\nxy,1\n",
1303 ] {
1304 let read = CsvReader::new().fallback(',').read(text).unwrap();
1305 let streamed = CsvReader::new()
1306 .fallback(',')
1307 .source(text.as_bytes())
1308 .unwrap()
1309 .collect_rows()
1310 .unwrap();
1311 assert_eq!(streamed.columns(), read.columns(), "{text:?}");
1312 assert_eq!(streamed.rows(), read.rows(), "{text:?}");
1313 }
1314 }
1315}