use crate::utils::typecheck::{is_boolean, is_date, is_number, parse_timestamp_to_seconds};
struct CsvSyntax {
delimiters: [char; 5],
quote_chars: [char; 2],
backslash_escapes: [&'static str; 3],
}
impl CsvSyntax {
const fn new() -> Self {
Self {
delimiters: [',', ';', '\t', '|', ':'],
quote_chars: ['"', '\''],
backslash_escapes: ["\\\"", "\\'", "\\\\"],
}
}
}
struct CsvValueTypes;
impl CsvValueTypes {
const NULL: &'static str = "null";
const BOOLEAN: &'static str = "boolean";
const TIMESTAMP: &'static str = "timestamp";
const NUMBER: &'static str = "number";
const DATE: &'static str = "date";
const STRING: &'static str = "string";
}
#[must_use]
pub fn dominant_delimiter_char(content: &str) -> Option<char> {
let syntax = CsvSyntax::new();
let delimiters = syntax.delimiters;
let sample_lines: Vec<&str> = content.lines().take(5).collect();
if sample_lines.is_empty() {
return None;
}
let mut delimiter_counts: Vec<(char, usize)> = delimiters
.iter()
.map(|&delim| {
let count: usize = sample_lines
.iter()
.map(|line| line.matches(delim).count())
.sum();
(delim, count)
})
.collect();
delimiter_counts.sort_by(|a, b| b.1.cmp(&a.1));
match delimiter_counts.first() {
Some((delim, count)) if *count > 0 => Some(*delim),
_ => None,
}
}
#[must_use]
pub fn detect_delimiter_byte(content: &str) -> u8 {
dominant_delimiter_char(content).map_or(b',', |c| u8::try_from(u32::from(c)).unwrap_or(b','))
}
#[must_use]
pub fn format_delimiter_for_metadata(byte: u8) -> String {
match char::from(byte) {
'\t' => "\\t".to_string(),
c => c.to_string(),
}
}
#[must_use]
pub fn delimiter_byte_for_reader(content: &str, file_path: &str) -> u8 {
use std::path::Path;
let ext = Path::new(file_path)
.extension()
.and_then(|e| e.to_str())
.map(str::to_lowercase);
match ext.as_deref() {
Some("tsv" | "tab") => b'\t',
Some("psv") => b'|',
_ => detect_delimiter_byte(content),
}
}
pub fn detect_quote_character(content: &str, field_separator: char) -> Option<String> {
let syntax = CsvSyntax::new();
let quote_chars = syntax.quote_chars;
let sample_lines: Vec<&str> = content.lines().take(10).collect();
if sample_lines.is_empty() {
return None;
}
let mut quote_scores: Vec<(char, usize)> = quote_chars
.iter()
.map(|"e| {
let mut score = 0;
for line in &sample_lines {
let quote_count = line.matches(quote).count();
if quote_count >= 2 && quote_count % 2 == 0 {
score += quote_count / 2;
}
if line.contains(&format!("{quote}{field_separator}"))
|| line.contains(&format!("{field_separator}{quote}"))
{
score += 1;
}
}
(quote, score)
})
.collect();
quote_scores.sort_by(|a, b| b.1.cmp(&a.1));
match quote_scores.first() {
Some((quote, score)) if *score > 0 => {
let display = match quote {
'"' => "\"".to_string(),
'\'' => "'".to_string(),
_ => quote.to_string(),
};
Some(display)
}
_ => None,
}
}
pub fn detect_escape_character(
content: &str,
_delimiter: Option<&str>,
quote: Option<&str>,
) -> Option<String> {
let sample_lines: Vec<&str> = content.lines().take(10).collect();
if sample_lines.is_empty() {
return None;
}
let syntax = CsvSyntax::new();
let mut backslash_escapes = 0;
let mut doubled_quote_escapes = 0;
for line in &sample_lines {
if syntax
.backslash_escapes
.iter()
.any(|pattern| line.contains(pattern))
{
backslash_escapes += 1;
}
if let Some(quote_char) = quote {
let doubled_pattern = format!("{quote_char}{quote_char}");
if line.contains(&doubled_pattern) {
doubled_quote_escapes += 1;
}
}
}
if backslash_escapes > 0 {
Some("\\".to_string())
} else if doubled_quote_escapes > 0 {
quote.map(std::string::ToString::to_string)
} else {
None
}
}
#[must_use]
pub fn infer_value_type(value: &str) -> String {
match () {
() if value.is_empty()
|| value.eq_ignore_ascii_case("null")
|| value.eq_ignore_ascii_case("nil") =>
{
CsvValueTypes::NULL.to_string()
}
() if is_boolean(value) => CsvValueTypes::BOOLEAN.to_string(),
() if parse_timestamp_to_seconds(value).is_some() => CsvValueTypes::TIMESTAMP.to_string(),
() if is_number(value) => CsvValueTypes::NUMBER.to_string(),
() if is_date(value) => CsvValueTypes::DATE.to_string(),
() => CsvValueTypes::STRING.to_string(),
}
}