#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CsvDialect {
pub delimiter: char,
pub quote: char,
pub has_header: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CsvRow {
pub fields: Vec<String>,
pub line: usize,
}
pub fn looks_like_csv(input: &str) -> bool {
detect_delimiter(input).is_some()
}
pub fn detect_delimiter(input: &str) -> Option<char> {
let lines: Vec<&str> = input
.lines()
.filter(|line| !line.trim().is_empty())
.take(5)
.collect();
if lines.is_empty() {
return None;
}
let mut best: Option<(char, usize, usize, usize)> = None;
for delimiter in [',', '\t', ';', '|'] {
let mut matching_lines = 0;
let mut total_columns = 0;
let mut expected_columns = None;
let mut consistent = true;
for line in &lines {
let columns = split_csv_line_with_delimiter(line, delimiter).len();
if columns > 1 {
matching_lines += 1;
total_columns += columns;
if let Some(expected) = expected_columns {
if expected != columns {
consistent = false;
}
} else {
expected_columns = Some(columns);
}
}
}
if matching_lines == 0 {
continue;
}
let score = (
delimiter,
usize::from(consistent),
matching_lines,
total_columns,
);
match best {
Some((_, best_consistent, best_matches, best_columns))
if (score.1, score.2, score.3) <= (best_consistent, best_matches, best_columns) => {
}
_ => best = Some(score),
}
}
best.map(|(delimiter, _, _, _)| delimiter)
}
pub fn split_csv_line_basic(line: &str) -> Vec<String> {
if line.is_empty() {
return Vec::new();
}
let delimiter = detect_delimiter(line).unwrap_or(',');
split_csv_line_with_delimiter(line, delimiter)
}
pub fn parse_csv_basic(input: &str) -> Vec<CsvRow> {
if input.trim().is_empty() {
return Vec::new();
}
let delimiter = detect_delimiter(input).unwrap_or(',');
let mut rows = Vec::new();
for (line_index, line) in input.lines().enumerate() {
if line.trim().is_empty() {
continue;
}
rows.push(CsvRow {
fields: split_csv_line_with_delimiter(line, delimiter),
line: line_index + 1,
});
}
rows
}
pub fn csv_escape_field(input: &str) -> String {
if input.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", input.replace('"', "\"\""))
} else {
input.to_string()
}
}
pub fn csv_join_row(fields: &[&str]) -> String {
fields
.iter()
.map(|field| csv_escape_field(field))
.collect::<Vec<_>>()
.join(",")
}
pub fn count_csv_columns(line: &str) -> usize {
split_csv_line_basic(line).len()
}
fn split_csv_line_with_delimiter(line: &str, delimiter: char) -> Vec<String> {
let mut fields = Vec::new();
let mut current = String::new();
let mut chars = line.chars().peekable();
let mut in_quotes = false;
while let Some(ch) = chars.next() {
if ch == '"' {
if in_quotes && chars.peek() == Some(&'"') {
current.push('"');
chars.next();
} else {
in_quotes = !in_quotes;
}
continue;
}
if ch == delimiter && !in_quotes {
fields.push(current);
current = String::new();
continue;
}
current.push(ch);
}
fields.push(current);
fields
}