dsv 0.13.2

dsv (Data Swiss Knife) — a high-performance CSV, Parquet, and JSONL CLI toolkit. Modern xsv fork with all 33 community PRs merged.
use csv;

use crate::config::{Config, Delimiter};
use crate::CliResult;
use clap::Parser;

#[derive(Parser, Debug)]
pub struct Args {
    #[arg()]
    pub arg_input: Option<String>,
    #[arg(short = 'o', long = "output", value_name = "file")]
    pub flag_output: Option<String>,
    #[arg(short = 'n', long = "no-headers")]
    pub flag_no_headers: bool,
    #[arg(short = 'd', long = "delimiter", value_name = "arg")]
    pub flag_delimiter: Option<Delimiter>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
enum ColType {
    Null,
    Unicode,
    Float,
    Integer,
}

impl ColType {
    fn from_sample(sample: &[u8]) -> ColType {
        if sample.is_empty() {
            return ColType::Null;
        }
        let string = match std::str::from_utf8(sample) {
            Err(_) => return ColType::Unicode,
            Ok(s) => s,
        };
        if string.parse::<i64>().is_ok() {
            ColType::Integer
        } else if string.parse::<f64>().is_ok() {
            ColType::Float
        } else {
            ColType::Unicode
        }
    }

    fn merge(&mut self, other: ColType) {
        *self = match (*self, other) {
            (ColType::Null, any) | (any, ColType::Null) => any,
            (a, b) if a == b => a,
            (ColType::Integer, ColType::Float) | (ColType::Float, ColType::Integer) => {
                ColType::Float
            }
            _ => ColType::Unicode,
        };
    }
}

impl std::fmt::Display for ColType {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match *self {
            ColType::Null => write!(f, "null"),
            ColType::Unicode => write!(f, "string"),
            ColType::Float => write!(f, "float"),
            ColType::Integer => write!(f, "integer"),
        }
    }
}

pub fn run(args: &Args) -> CliResult<()> {
    let conf = Config::new(&args.arg_input)
        .delimiter(args.flag_delimiter)
        .no_headers(args.flag_no_headers);

    let mut rdr = conf.reader()?;
    let headers = rdr.byte_headers()?.clone();
    let mut types: Vec<ColType> = (0..headers.len()).map(|_| ColType::Null).collect();

    let mut record = csv::ByteRecord::new();
    while rdr.read_byte_record(&mut record)? {
        for (i, ty) in types.iter_mut().enumerate() {
            let sample = if i < record.len() { &record[i] } else { b"" };
            let t = ColType::from_sample(sample);
            ty.merge(t);
        }
    }

    let mut wtr = Config::new(&args.flag_output).writer()?;
    for (i, header) in headers.iter().enumerate() {
        let name = if conf.no_headers {
            format!("column_{}", i + 1).into_bytes()
        } else {
            header.to_vec()
        };
        wtr.write_record([name.as_slice(), types[i].to_string().as_bytes()])?;
    }
    wtr.flush()?;
    Ok(())
}