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 #[must_use]
83 pub fn delimiter(mut self, delimiter: char) -> Self {
84 self.dialect = Some(Dialect::excel(delimiter));
85 self
86 }
87
88 #[must_use]
90 pub fn dialect(mut self, dialect: Dialect) -> Self {
91 self.dialect = Some(dialect);
92 self
93 }
94
95 #[must_use]
99 pub fn fallback(mut self, delimiter: char) -> Self {
100 self.fallback = Some(delimiter);
101 self
102 }
103
104 #[must_use]
107 pub fn header(mut self, header: bool) -> Self {
108 self.header = if header { Header::Yes } else { Header::No };
109 self
110 }
111
112 #[must_use]
114 pub fn header_mode(mut self, header: Header) -> Self {
115 self.header = header;
116 self
117 }
118
119 pub fn detect(&self, content: &str) -> Option<(Dialect, bool)> {
123 let sample: String = content.chars().take(SAMPLE_CHARS).collect();
124 let dialect = match self.dialect {
125 Some(dialect) => dialect,
126 None => {
127 sniff(&sample, Some(&DELIMITERS)).or_else(|| self.fallback.map(Dialect::excel))?
128 }
129 };
130 let header = match self.header {
131 Header::Yes => true,
132 Header::No => false,
133 Header::Sniff => has_header(&sample).unwrap_or(true),
134 Header::UnlessNumeric => {
135 let first = read_rows(&sample, &dialect)
136 .into_iter()
137 .find(|row| !row.is_empty())
138 .unwrap_or_default();
139 first.iter().any(|cell| {
140 let cell = cell.trim();
141 !cell.is_empty() && cell.parse::<f64>().is_err()
142 })
143 }
144 };
145 Some((dialect, header))
146 }
147
148 pub fn read(&self, content: &str) -> Result<Rows, DataError> {
152 let content = universal_newlines(content);
153 let content = content.strip_prefix('\u{feff}').unwrap_or(&content);
155 let (dialect, header) = self
156 .detect(content)
157 .ok_or_else(|| DataError::new("Could not determine delimiter"))?;
158 let mut records = parse_rows(content, &dialect)
159 .into_iter()
160 .filter(|row| !row.is_empty());
161 let names = if header {
162 records.next().unwrap_or_default()
163 } else {
164 Vec::new()
165 };
166 let records: Vec<Vec<String>> = records.collect();
167 let width = records
168 .iter()
169 .map(Vec::len)
170 .max()
171 .unwrap_or(0)
172 .max(names.len());
173 let columns: Vec<String> = (0..width)
174 .map(|i| names.get(i).cloned().unwrap_or_else(|| (i + 1).to_string()))
175 .collect();
176 let mut rows = Rows::new(columns);
177 for record in records {
178 rows.push(record.into_iter().map(Value::Str));
179 }
180 Ok(rows)
181 }
182}
183
184impl CsvReader {
185 pub fn source<R: BufRead>(&self, reader: R) -> Result<CsvSource<R>, DataError> {
212 let mut source = CsvSource {
213 reader,
214 dialect: Dialect::excel(','),
215 columns: Vec::new(),
216 pending: VecDeque::new(),
217 record: String::new(),
218 state: State::StartRecord,
219 line: 0,
220 done: false,
221 longer: 0,
222 };
223 let mut prefix = String::new();
225 while prefix.chars().count() < SAMPLE_CHARS {
226 match source.read_line()? {
227 Some(line) => prefix.push_str(&line),
228 None => break,
229 }
230 }
231 let prefix = universal_newlines(&prefix);
232 let prefix = prefix.strip_prefix('\u{feff}').unwrap_or(&prefix);
233 let (dialect, header) = self
234 .detect(prefix)
235 .ok_or_else(|| DataError::new("Could not determine delimiter"))?;
236 source.dialect = dialect;
237 source.feed(prefix)?;
238 if !matches!(source.state, State::StartRecord) {
240 source.fill_record()?;
241 }
242 if source.done {
243 source.flush_record();
244 }
245 let names = if header {
246 source.pending.pop_front().unwrap_or_default()
247 } else {
248 Vec::new()
249 };
250 let width = source
251 .pending
252 .iter()
253 .map(Vec::len)
254 .max()
255 .unwrap_or(0)
256 .max(names.len());
257 source.columns = (0..width)
258 .map(|i| names.get(i).cloned().unwrap_or_else(|| (i + 1).to_string()))
259 .collect();
260 Ok(source)
261 }
262}
263
264pub const MAX_RECORD: usize = 64 << 20;
268
269#[derive(Debug)]
272pub struct CsvSource<R> {
273 reader: R,
274 dialect: Dialect,
275 columns: Vec<String>,
276 pending: VecDeque<Vec<String>>,
278 record: String,
280 state: State,
282 line: usize,
283 done: bool,
284 longer: usize,
285}
286
287impl<R: BufRead> CsvSource<R> {
288 pub fn dialect(&self) -> Dialect {
290 self.dialect
291 }
292
293 pub fn longer_rows(&self) -> usize {
296 self.longer
297 }
298
299 fn read_line(&mut self) -> Result<Option<String>, DataError> {
301 if self.done {
302 return Ok(None);
303 }
304 let mut buffer = Vec::new();
305 let read = (&mut self.reader)
306 .take(MAX_RECORD as u64 + 1)
307 .read_until(b'\n', &mut buffer)
308 .map_err(|e| {
309 self.done = true;
310 DataError::at(self.line + 1, e.to_string())
311 })?;
312 if read == 0 {
313 self.done = true;
314 return Ok(None);
315 }
316 self.line += 1;
317 if buffer.len() > MAX_RECORD {
318 self.done = true;
319 return Err(DataError::at(
320 self.line,
321 format!("longer than {MAX_RECORD} bytes"),
322 ));
323 }
324 Ok(Some(String::from_utf8_lossy(&buffer).into_owned()))
325 }
326
327 fn feed(&mut self, text: &str) -> Result<(), DataError> {
330 for c in text.chars() {
331 self.state = advance(self.state, c, &self.dialect);
332 self.record.push(c);
333 if matches!(self.state, State::StartRecord) {
334 self.flush_record();
335 }
336 }
337 if self.record.len() > MAX_RECORD {
338 self.done = true;
339 return Err(DataError::at(
340 self.line,
341 format!("a quoted field runs past {MAX_RECORD} bytes"),
342 ));
343 }
344 Ok(())
345 }
346
347 fn fill_record(&mut self) -> Result<(), DataError> {
349 while let Some(line) = self.read_line()? {
350 self.feed(&universal_newlines(&line))?;
351 if matches!(self.state, State::StartRecord) {
352 break;
353 }
354 }
355 Ok(())
356 }
357
358 fn flush_record(&mut self) {
360 if self.record.is_empty() {
361 return;
362 }
363 let text = std::mem::take(&mut self.record);
364 self.state = State::StartRecord;
365 self.pending.extend(
366 parse_rows(&text, &self.dialect)
367 .into_iter()
368 .filter(|row| !row.is_empty()),
369 );
370 }
371}
372
373impl<R: BufRead> RowSource for CsvSource<R> {
374 fn columns(&self) -> &[String] {
375 &self.columns
376 }
377
378 fn next_row(&mut self) -> Option<Result<Row, DataError>> {
379 while self.pending.is_empty() {
380 if self.done {
381 self.flush_record();
384 if self.pending.is_empty() {
385 return None;
386 }
387 break;
388 }
389 if let Err(error) = self.fill_record() {
390 return Some(Err(error));
391 }
392 }
393 let record = self.pending.pop_front()?;
394 let width = self.columns.len();
395 if record.len() > width {
396 self.longer += 1;
397 }
398 let mut row: Row = record.into_iter().take(width).map(Value::Str).collect();
399 row.resize(width, Value::Null);
400 Some(Ok(row))
401 }
402}
403
404fn advance(state: State, c: char, dialect: &Dialect) -> State {
407 match state {
408 State::StartRecord | State::StartField => {
409 if c == '\n' {
410 State::StartRecord
411 } else if c == dialect.quotechar {
412 State::InQuotedField
413 } else if c == dialect.delimiter || (c == ' ' && dialect.skipinitialspace) {
414 State::StartField
415 } else {
416 State::InField
417 }
418 }
419 State::InField => match c {
420 '\n' => State::StartRecord,
421 c if c == dialect.delimiter => State::StartField,
422 _ => State::InField,
423 },
424 State::InQuotedField => {
425 if c != dialect.quotechar {
426 State::InQuotedField
427 } else if dialect.doublequote {
428 State::QuoteInQuotedField
429 } else {
430 State::InField
431 }
432 }
433 State::QuoteInQuotedField => match c {
434 c if c == dialect.quotechar => State::InQuotedField,
435 c if c == dialect.delimiter => State::StartField,
436 '\n' => State::StartRecord,
437 _ => State::InField,
438 },
439 }
440}
441
442fn universal_newlines(content: &str) -> Cow<'_, str> {
444 if content.contains('\r') {
445 Cow::Owned(content.replace("\r\n", "\n").replace('\r', "\n"))
446 } else {
447 Cow::Borrowed(content)
448 }
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
453pub struct Dialect {
454 pub delimiter: char,
456 pub quotechar: char,
458 pub doublequote: bool,
460 pub skipinitialspace: bool,
462}
463
464impl Dialect {
465 pub fn excel(delimiter: char) -> Self {
468 Dialect {
469 delimiter,
470 quotechar: '"',
471 doublequote: true,
472 skipinitialspace: false,
473 }
474 }
475}
476
477fn is_word_char(c: char) -> bool {
479 c.is_alphanumeric() || c == '_'
480}
481
482fn is_delimiter_char(c: char) -> bool {
484 !is_word_char(c) && c != '\n' && c != '"' && c != '\''
485}
486
487fn is_quote_char(c: char) -> bool {
489 c == '"' || c == '\''
490}
491
492struct QuoteHit {
496 quote: char,
497 delim: Option<char>,
498 space: bool,
499}
500
501pub fn sniff(sample: &str, delimiters: Option<&[char]>) -> Option<Dialect> {
514 let data: Vec<char> = sample.chars().collect();
515 let (quote, mut delimiter, mut skipinitialspace) = guess_quote_and_delimiter(&data, delimiters);
516 if delimiter.is_none() {
517 let (guessed, spaced) = guess_delimiter(sample, delimiters);
518 delimiter = guessed;
519 skipinitialspace = spaced;
520 }
521 Some(Dialect {
522 delimiter: delimiter?,
523 quotechar: quote.unwrap_or('"'),
526 doublequote: true,
527 skipinitialspace,
528 })
529}
530
531fn guess_quote_and_delimiter(
539 data: &[char],
540 delimiters: Option<&[char]>,
541) -> (Option<char>, Option<char>, bool) {
542 let hits = quote_hits(data);
543 if hits.is_empty() {
544 return (None, None, false);
545 }
546
547 let mut quotes: Vec<(char, usize)> = Vec::new();
551 let mut delims: Vec<(char, usize)> = Vec::new();
552 let mut spaces = 0usize;
553 fn bump(table: &mut Vec<(char, usize)>, key: char) {
554 match table.iter_mut().find(|(existing, _)| *existing == key) {
555 Some(entry) => entry.1 += 1,
556 None => table.push((key, 1)),
557 }
558 }
559 for hit in &hits {
560 bump(&mut quotes, hit.quote);
561 let Some(delim) = hit.delim else { continue };
564 if delimiters.is_none_or(|allowed| allowed.contains(&delim)) {
565 bump(&mut delims, delim);
566 }
567 if hit.space {
568 spaces += 1;
569 }
570 }
571
572 let first_max = |table: &[(char, usize)]| -> Option<(char, usize)> {
573 let mut best: Option<(char, usize)> = None;
574 for &(key, count) in table {
575 if best.is_none_or(|(_, seen)| count > seen) {
576 best = Some((key, count));
577 }
578 }
579 best
580 };
581 let quotechar = first_max("es).map(|(key, _)| key);
582 match first_max(&delims) {
583 Some((delim, count)) => (quotechar, Some(delim), count == spaces),
584 None => (quotechar, None, false),
586 }
587}
588
589type QuoteScan = fn(&[char]) -> Vec<QuoteHit>;
591
592fn quote_hits(data: &[char]) -> Vec<QuoteHit> {
595 let scans: [QuoteScan; 4] = [
596 scan_delim_quote_delim,
597 scan_line_quote_delim,
598 scan_delim_quote_line,
599 scan_line_quote_line,
600 ];
601 for scan in scans {
602 let hits = scan(data);
603 if !hits.is_empty() {
604 return hits;
605 }
606 }
607 Vec::new()
608}
609
610fn scan_delim_quote_delim(data: &[char]) -> Vec<QuoteHit> {
615 scan_delim_quote(data, |data, quote, delim, from| {
616 let mut k = from;
617 while k + 1 < data.len() {
618 if data[k] == quote && data[k + 1] == delim {
619 return Some(k + 2);
620 }
621 k += 1;
622 }
623 None
624 })
625}
626
627fn scan_delim_quote_line(data: &[char]) -> Vec<QuoteHit> {
630 scan_delim_quote(data, |data, quote, _delim, from| {
631 let mut k = from;
632 while k < data.len() {
633 if data[k] == quote && (k + 1 == data.len() || data[k + 1] == '\n') {
634 return Some(k + 1);
637 }
638 k += 1;
639 }
640 None
641 })
642}
643
644fn scan_delim_quote(
647 data: &[char],
648 close: fn(&[char], char, char, usize) -> Option<usize>,
649) -> Vec<QuoteHit> {
650 let mut hits = Vec::new();
651 let mut i = 0;
652 while i < data.len() {
653 if is_delimiter_char(data[i]) {
654 let delim = data[i];
655 let mut j = i + 1;
656 let mut space = false;
657 if j < data.len() && data[j] == ' ' {
658 space = true;
659 j += 1;
660 }
661 if j < data.len() && is_quote_char(data[j]) {
662 let quote = data[j];
663 if let Some(end) = close(data, quote, delim, j + 1) {
664 hits.push(QuoteHit {
665 quote,
666 delim: Some(delim),
667 space,
668 });
669 i = end;
670 continue;
671 }
672 }
673 }
674 i += 1;
675 }
676 hits
677}
678
679fn scan_line_quote_delim(data: &[char]) -> Vec<QuoteHit> {
682 scan_line_quote(data, |data, quote, from| {
683 let mut k = from;
684 while k + 1 < data.len() {
685 if data[k] == quote && is_delimiter_char(data[k + 1]) {
686 let space = k + 2 < data.len() && data[k + 2] == ' ';
687 return Some((Some(data[k + 1]), space, if space { k + 3 } else { k + 2 }));
688 }
689 k += 1;
690 }
691 None
692 })
693}
694
695fn scan_line_quote_line(data: &[char]) -> Vec<QuoteHit> {
698 scan_line_quote(data, |data, quote, from| {
699 let mut k = from;
700 while k < data.len() {
701 if data[k] == quote && (k + 1 == data.len() || data[k + 1] == '\n') {
702 return Some((None, false, k + 1));
703 }
704 k += 1;
705 }
706 None
707 })
708}
709
710#[allow(clippy::type_complexity)]
713fn scan_line_quote(
714 data: &[char],
715 close: fn(&[char], char, usize) -> Option<(Option<char>, bool, usize)>,
716) -> Vec<QuoteHit> {
717 let mut hits = Vec::new();
718 let mut i = 0;
719 while i < data.len() {
720 let mut starts: Vec<usize> = Vec::new();
724 if i == 0 || data[i - 1] == '\n' {
725 starts.push(i);
726 }
727 if data[i] == '\n' {
728 starts.push(i + 1);
729 }
730 let mut advanced = false;
731 for quote_at in starts {
732 if quote_at >= data.len() || !is_quote_char(data[quote_at]) {
733 continue;
734 }
735 let quote = data[quote_at];
736 if let Some((delim, space, end)) = close(data, quote, quote_at + 1) {
737 hits.push(QuoteHit {
738 quote,
739 delim,
740 space,
741 });
742 i = end;
743 advanced = true;
744 break;
745 }
746 }
747 if !advanced {
748 i += 1;
749 }
750 }
751 hits
752}
753
754fn guess_delimiter(sample: &str, delimiters: Option<&[char]>) -> (Option<char>, bool) {
757 let data: Vec<&str> = sample.split('\n').filter(|line| !line.is_empty()).collect();
759 if data.is_empty() {
760 return (None, false);
761 }
762 const ASCII: usize = 127;
764
765 let chunk_length = 10.min(data.len());
766 let mut iteration = 0usize;
767 let mut char_frequency: Vec<Vec<(usize, usize)>> = vec![Vec::new(); ASCII];
770 let mut modes: Vec<Option<(usize, isize)>> = vec![None; ASCII];
773 let mut delims: Vec<(char, (usize, isize))> = Vec::new();
774
775 let (mut start, mut end) = (0usize, chunk_length);
776 while start < data.len() {
777 iteration += 1;
778 for line in &data[start..end.min(data.len())] {
779 for (code, table) in char_frequency.iter_mut().enumerate() {
780 let c = code as u8 as char;
781 let freq = line.matches(c).count();
784 match table.iter_mut().find(|(seen, _)| *seen == freq) {
785 Some(entry) => entry.1 += 1,
786 None => table.push((freq, 1)),
787 }
788 }
789 }
790
791 for (code, items) in char_frequency.iter().enumerate() {
792 if items.len() == 1 && items[0].0 == 0 {
793 continue;
794 }
795 if items.len() > 1 {
796 let mut best = 0usize;
799 for (index, item) in items.iter().enumerate() {
800 if item.1 > items[best].1 {
801 best = index;
802 }
803 }
804 let others: usize = items
805 .iter()
806 .enumerate()
807 .filter(|(index, _)| *index != best)
808 .map(|(_, item)| item.1)
809 .sum();
810 modes[code] = Some((items[best].0, items[best].1 as isize - others as isize));
811 } else if let Some(&(freq, count)) = items.first() {
812 modes[code] = Some((freq, count as isize));
813 }
814 }
815
816 let total = (chunk_length * iteration).min(data.len()) as f64;
817 let mut consistency = 1.0f64;
818 let threshold = 0.9f64;
819 while delims.is_empty() && consistency >= threshold {
820 for (code, mode) in modes.iter().enumerate() {
821 let Some((freq, count)) = *mode else { continue };
822 let c = code as u8 as char;
823 if freq > 0
824 && count > 0
825 && (count as f64 / total) >= consistency
826 && delimiters.is_none_or(|allowed| allowed.contains(&c))
827 {
828 delims.push((c, (freq, count)));
829 }
830 }
831 consistency -= 0.01;
832 }
833
834 if delims.len() == 1 {
835 let delim = delims[0].0;
836 return (Some(delim), saw_space_after(data[0], delim));
837 }
838
839 start = end;
840 end += chunk_length;
841 }
842
843 if delims.is_empty() {
844 return (None, false);
845 }
846 if delims.len() > 1 {
847 for preferred in [',', '\t', ';', ' ', ':'] {
848 if delims.iter().any(|(c, _)| *c == preferred) {
849 return (Some(preferred), saw_space_after(data[0], preferred));
850 }
851 }
852 }
853 let best = delims
855 .iter()
856 .max_by_key(|(c, mode)| (*mode, *c))
857 .expect("delims is non-empty");
858 (Some(best.0), saw_space_after(data[0], best.0))
859}
860
861fn saw_space_after(line: &str, delimiter: char) -> bool {
864 line.matches(delimiter).count() == line.matches(&format!("{delimiter} ")).count()
865}
866
867#[derive(Debug, Clone, Copy, PartialEq, Eq)]
869enum ColumnType {
870 Number,
872 Length(usize),
874}
875
876#[derive(Debug, Clone, Copy)]
878enum ColumnVote {
879 Untyped,
881 Typed(ColumnType),
882 Dropped,
884}
885
886pub fn has_header(sample: &str) -> Option<bool> {
898 let dialect = sniff(sample, None)?;
901 let mut rows = read_rows(sample, &dialect).into_iter();
902 let header = rows.next()?;
903 let columns = header.len();
904 let mut votes = vec![ColumnVote::Untyped; columns];
905
906 for (checked, row) in rows.enumerate() {
907 if checked > 20 {
910 break;
911 }
912 if row.len() != columns {
913 continue;
914 }
915 for (col, vote) in votes.iter_mut().enumerate() {
916 if matches!(vote, ColumnVote::Dropped) {
917 continue;
918 }
919 let this = if parses_as_complex(&row[col]) {
920 ColumnType::Number
921 } else {
922 ColumnType::Length(row[col].chars().count())
923 };
924 match vote {
925 ColumnVote::Untyped => *vote = ColumnVote::Typed(this),
926 ColumnVote::Typed(known) if *known != this => *vote = ColumnVote::Dropped,
927 _ => {}
928 }
929 }
930 }
931
932 let mut tally = 0isize;
933 for (col, vote) in votes.iter().enumerate() {
934 match vote {
935 ColumnVote::Dropped => {}
936 ColumnVote::Untyped => tally += 1,
939 ColumnVote::Typed(ColumnType::Length(len)) => {
940 if header[col].chars().count() == *len {
941 tally -= 1;
942 } else {
943 tally += 1;
944 }
945 }
946 ColumnVote::Typed(ColumnType::Number) => {
947 if parses_as_complex(&header[col]) {
948 tally -= 1;
949 } else {
950 tally += 1;
951 }
952 }
953 }
954 }
955 Some(tally > 0)
956}
957
958fn parses_as_complex(value: &str) -> bool {
968 let trimmed = value.trim();
969 let body = match trimmed.strip_prefix('(') {
970 Some(inner) => match inner.strip_suffix(')') {
971 Some(inner) => inner.trim(),
972 None => return false,
973 },
974 None if trimmed.ends_with(')') => return false,
975 None => trimmed,
976 };
977 if body.is_empty() {
978 return false;
979 }
980 let chars: Vec<char> = body.chars().collect();
983 let split = (1..chars.len())
984 .find(|&i| matches!(chars[i], '+' | '-') && !matches!(chars[i - 1], 'e' | 'E'));
985 match split {
986 Some(index) => {
987 let at: usize = chars[..index].iter().map(|c| c.len_utf8()).sum();
988 let (real, imaginary) = body.split_at(at);
989 is_float_literal(real) && is_imaginary_literal(imaginary)
990 }
991 None => is_float_literal(body) || is_imaginary_literal(body),
992 }
993}
994
995fn is_float_literal(value: &str) -> bool {
998 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
999 if matches!(
1000 body.to_ascii_lowercase().as_str(),
1001 "inf" | "infinity" | "nan"
1002 ) {
1003 return true;
1004 }
1005 let chars: Vec<char> = body.chars().collect();
1006 let mut index = 0usize;
1007 let mut digits = 0usize;
1008 let take_digits = |index: &mut usize, digits: &mut usize| {
1009 while *index < chars.len() && (chars[*index].is_ascii_digit() || chars[*index] == '_') {
1010 if chars[*index].is_ascii_digit() {
1011 *digits += 1;
1012 }
1013 *index += 1;
1014 }
1015 };
1016 take_digits(&mut index, &mut digits);
1017 if index < chars.len() && chars[index] == '.' {
1018 index += 1;
1019 take_digits(&mut index, &mut digits);
1020 }
1021 if digits == 0 {
1022 return false;
1023 }
1024 if index < chars.len() && (chars[index] == 'e' || chars[index] == 'E') {
1025 index += 1;
1026 if index < chars.len() && matches!(chars[index], '+' | '-') {
1027 index += 1;
1028 }
1029 let mut exponent = 0usize;
1030 take_digits(&mut index, &mut exponent);
1031 if exponent == 0 {
1032 return false;
1033 }
1034 }
1035 index == chars.len()
1036}
1037
1038fn is_imaginary_literal(value: &str) -> bool {
1040 let Some(body) = value.strip_suffix(['j', 'J']) else {
1041 return false;
1042 };
1043 matches!(body, "" | "+" | "-") || is_float_literal(body)
1044}
1045
1046#[derive(Clone, Copy, Debug)]
1049enum State {
1050 StartRecord,
1051 StartField,
1052 InField,
1053 InQuotedField,
1054 QuoteInQuotedField,
1055}
1056
1057pub fn read_rows(content: &str, dialect: &Dialect) -> Vec<Vec<String>> {
1066 parse_rows(content.strip_prefix('\u{feff}').unwrap_or(content), dialect)
1068}
1069
1070fn parse_rows(content: &str, dialect: &Dialect) -> Vec<Vec<String>> {
1073 let mut rows: Vec<Vec<String>> = Vec::new();
1074 let mut row: Vec<String> = Vec::new();
1075 let mut field = String::new();
1076 let mut state = State::StartRecord;
1077
1078 for c in content.chars() {
1079 match state {
1080 State::StartRecord => {
1081 if c == '\n' {
1082 rows.push(Vec::new());
1083 } else {
1084 state = State::StartField;
1085 read_csv_char(c, dialect, &mut state, &mut field, &mut row, &mut rows);
1087 }
1088 }
1089 _ => read_csv_char(c, dialect, &mut state, &mut field, &mut row, &mut rows),
1090 }
1091 }
1092 if !matches!(state, State::StartRecord) {
1093 row.push(std::mem::take(&mut field));
1094 row.shrink_to_fit();
1095 rows.push(std::mem::take(&mut row));
1096 }
1097 rows
1098}
1099
1100fn read_csv_char(
1103 c: char,
1104 dialect: &Dialect,
1105 state: &mut State,
1106 field: &mut String,
1107 row: &mut Vec<String>,
1108 rows: &mut Vec<Vec<String>>,
1109) {
1110 let end_record = |field: &mut String, row: &mut Vec<String>, rows: &mut Vec<Vec<String>>| {
1111 row.push(std::mem::take(field));
1112 row.shrink_to_fit();
1114 rows.push(std::mem::take(row));
1115 };
1116 match state {
1117 State::StartRecord => unreachable!("handled by the caller"),
1118 State::StartField => {
1119 if c == '\n' {
1120 end_record(field, row, rows);
1121 *state = State::StartRecord;
1122 } else if c == dialect.quotechar {
1123 *state = State::InQuotedField;
1124 } else if c == ' ' && dialect.skipinitialspace {
1125 } else if c == dialect.delimiter {
1127 row.push(std::mem::take(field));
1128 } else {
1129 field.push(c);
1130 *state = State::InField;
1131 }
1132 }
1133 State::InField => {
1134 if c == '\n' {
1135 end_record(field, row, rows);
1136 *state = State::StartRecord;
1137 } else if c == dialect.delimiter {
1138 row.push(std::mem::take(field));
1139 *state = State::StartField;
1140 } else {
1141 field.push(c);
1142 }
1143 }
1144 State::InQuotedField => {
1145 if c == dialect.quotechar {
1146 *state = if dialect.doublequote {
1147 State::QuoteInQuotedField
1148 } else {
1149 State::InField
1152 };
1153 } else {
1154 field.push(c);
1155 }
1156 }
1157 State::QuoteInQuotedField => {
1158 if c == dialect.quotechar {
1159 field.push(c);
1160 *state = State::InQuotedField;
1161 } else if c == dialect.delimiter {
1162 row.push(std::mem::take(field));
1163 *state = State::StartField;
1164 } else if c == '\n' {
1165 end_record(field, row, rows);
1166 *state = State::StartRecord;
1167 } else {
1168 field.push(c);
1169 *state = State::InField;
1170 }
1171 }
1172 }
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177 use super::*;
1178
1179 #[test]
1180 fn read_rows_handles_quotes_and_delimiters() {
1181 let comma = Dialect::excel(',');
1182 let rows = read_rows("a,\"b,c\",d\n\"he said \"\"hi\"\"\",2\n", &comma);
1184 assert_eq!(
1185 rows,
1186 vec![
1187 vec!["a".to_string(), "b,c".to_string(), "d".to_string()],
1188 vec!["he said \"hi\"".to_string(), "2".to_string()],
1189 ]
1190 );
1191 assert_eq!(read_rows("x\ny\n", &comma), vec![vec!["x"], vec!["y"]]);
1194 assert_eq!(
1196 read_rows("a\tb", &Dialect::excel('\t')),
1197 vec![vec!["a", "b"]]
1198 );
1199 assert_eq!(read_rows("\u{feff}a,b", &comma), vec![vec!["a", "b"]]);
1201 assert_eq!(
1204 read_rows("a,b\n\nc,d\n", &comma),
1205 vec![vec!["a", "b"], vec![], vec!["c", "d"]]
1206 );
1207 let padded = Dialect {
1209 skipinitialspace: true,
1210 ..Dialect::excel(',')
1211 };
1212 assert_eq!(read_rows("a, b , c", &padded), vec![vec!["a", "b ", "c"]]);
1213 }
1214
1215 #[test]
1216 fn the_sniffer_finds_the_delimiter_and_the_header() {
1217 let candidates = [',', '\t', '|', ';'];
1219 let sniffed =
1220 |sample: &str| sniff(sample, Some(&candidates)).map(|dialect| dialect.delimiter);
1221 assert_eq!(
1222 sniffed("name;age;city\nAlice;30;Paris\nBob;25;Lyon\n"),
1223 Some(';')
1224 );
1225 assert_eq!(sniffed("a|b|c\n1|2|3\n4|5|6\n"), Some('|'));
1226 assert_eq!(sniffed("a\tb\tc\n1\t2\t3\n4\t5\t6\n"), Some('\t'));
1227 assert_eq!(sniffed("a, b, c\n1, 2, 3\n4, 5, 6\n"), Some(','));
1228 assert_eq!(
1231 sniffed("name,bio\nAlice,\"line one\nline two\"\nBob,short\n"),
1232 Some(',')
1233 );
1234 assert_eq!(sniffed("a,b,c\n1,2\n3,4,5,6\n"), None);
1237 assert_eq!(sniffed("alpha\nbeta\ngamma\n"), None);
1238
1239 assert_eq!(has_header("name,age\nAlice,30\nBob,25\n"), Some(true));
1240 assert_eq!(has_header("price,note\n10,aa\n9.5,bbb\n"), Some(true));
1244 assert_eq!(has_header("1,2,3\n4,5,6\n7,8,9\n"), Some(false));
1245 }
1246 #[test]
1247 fn the_reader_sniffs_pads_and_names_columns() {
1248 let rows = CsvReader::new()
1249 .delimiter(',')
1250 .header(true)
1251 .read("a,b\r\n1,2,3\r\n\r\n4\r\n")
1252 .unwrap();
1253 assert_eq!(rows.columns(), ["a", "b", "3"]);
1254 assert_eq!(
1255 rows.rows()[0],
1256 [Value::from("1"), Value::from("2"), Value::from("3")]
1257 );
1258 assert_eq!(rows.rows()[1], [Value::from("4"), Value::Null, Value::Null]);
1259 let rows = CsvReader::new().read("1,2,3\n4,5,6\n7,8,9\n").unwrap();
1261 assert_eq!(rows.columns(), ["1", "2", "3"]);
1262 assert_eq!(rows.len(), 3);
1263 }
1264
1265 #[test]
1266 fn the_reader_fails_where_rich_csv_does_unless_given_a_fallback() {
1267 let error = CsvReader::new().read("alpha\nbeta\n").unwrap_err();
1268 assert_eq!(error.to_string(), "Could not determine delimiter");
1269 let rows = CsvReader::new()
1270 .fallback(',')
1271 .read("alpha\nbeta\n")
1272 .unwrap();
1273 assert_eq!(rows.columns(), ["alpha"]);
1274 assert_eq!(rows.len(), 1);
1275 let rows = CsvReader::tsv().read("x\ty\n1\t2\n").unwrap();
1276 assert_eq!(rows.columns(), ["x", "y"]);
1277 }
1278
1279 #[test]
1283 fn streaming_strips_only_a_leading_byte_order_mark() {
1284 let text = "\u{feff}a,b\n1,2\r\u{feff}x,y\n\u{feff}3,4\n";
1285 let read = CsvReader::new().header(true).read(text).unwrap();
1286 let streamed = CsvReader::new()
1287 .header(true)
1288 .source(text.as_bytes())
1289 .unwrap()
1290 .collect_rows()
1291 .unwrap();
1292 assert_eq!(streamed.columns(), ["a", "b"]);
1293 assert_eq!(streamed.rows()[1][0], Value::from("\u{feff}x"));
1294 assert_eq!(streamed.rows(), read.rows());
1295 }
1296
1297 #[test]
1301 fn read_and_source_sniff_past_a_byte_order_mark_alike() {
1302 for text in [
1303 "\u{feff}\r\nx|y;z\tw w\n",
1304 "\u{feff}\nname,age\nAda,36\n",
1305 "\u{feff}ab,c\nxy,1\nzw,2\n",
1306 "\u{feff}\u{feff}ab,c\nxy,1\n",
1308 ] {
1309 let read = CsvReader::new().fallback(',').read(text).unwrap();
1310 let streamed = CsvReader::new()
1311 .fallback(',')
1312 .source(text.as_bytes())
1313 .unwrap()
1314 .collect_rows()
1315 .unwrap();
1316 assert_eq!(streamed.columns(), read.columns(), "{text:?}");
1317 assert_eq!(streamed.rows(), read.rows(), "{text:?}");
1318 }
1319 }
1320}