use std::error::Error;
use std::fs::File;
use std::io::{BufWriter, Cursor, Read, Write};
use std::path::Path;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use polars::prelude::*;
use crate::cli::Format;
pub fn resolve_format(path: &Path, explicit: Format) -> Format {
if explicit != Format::Auto {
return explicit;
}
let name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_ascii_lowercase();
if name.ends_with(".parquet") || name.ends_with(".pq") {
Format::Parquet
} else if name.ends_with(".tsv.gz") {
Format::TsvGz
} else if name.ends_with(".csv.gz") {
Format::CsvGz
} else if name.ends_with(".tsv") {
Format::Tsv
} else if name.ends_with(".csv") {
Format::Csv
} else {
Format::Parquet
}
}
pub fn format_ext(fmt: Format) -> &'static str {
match fmt {
Format::Parquet | Format::Auto => "parquet",
Format::Csv => "csv",
Format::Tsv => "tsv",
Format::CsvGz => "csv.gz",
Format::TsvGz => "tsv.gz",
}
}
fn is_tab(fmt: Format) -> bool {
matches!(fmt, Format::Tsv | Format::TsvGz)
}
fn is_gz(fmt: Format) -> bool {
matches!(fmt, Format::CsvGz | Format::TsvGz)
}
pub fn read_frame(path: &Path, fmt: Format) -> Result<DataFrame, Box<dyn Error>> {
let fmt = resolve_format(path, fmt);
match fmt {
Format::Parquet => {
let f = File::open(path).map_err(|e| format!("cannot open {}: {e}", path.display()))?;
Ok(ParquetReader::new(f).finish()?)
}
Format::Csv | Format::Tsv | Format::CsvGz | Format::TsvGz => {
let sep = if is_tab(fmt) { b'\t' } else { b',' };
let cursor = read_bytes(path, is_gz(fmt))?;
let df = CsvReadOptions::default()
.with_has_header(true)
.with_parse_options(CsvParseOptions::default().with_separator(sep))
.into_reader_with_file_handle(cursor)
.finish()?;
Ok(df)
}
Format::Auto => unreachable!("resolve_format removes Auto"),
}
}
fn read_bytes(path: &Path, gz: bool) -> Result<Cursor<Vec<u8>>, Box<dyn Error>> {
let mut f = File::open(path).map_err(|e| format!("cannot open {}: {e}", path.display()))?;
let mut buf = Vec::new();
if gz {
GzDecoder::new(f).read_to_end(&mut buf)?;
} else {
f.read_to_end(&mut buf)?;
}
Ok(Cursor::new(buf))
}
pub fn write_frame(mut df: DataFrame, path: &Path, fmt: Format) -> Result<(), Box<dyn Error>> {
let fmt = resolve_format(path, fmt);
let f = File::create(path).map_err(|e| format!("cannot create {}: {e}", path.display()))?;
match fmt {
Format::Parquet => {
ParquetWriter::new(f).set_parallel(false).finish(&mut df)?;
}
Format::Csv | Format::Tsv | Format::CsvGz | Format::TsvGz => {
let sep = if is_tab(fmt) { b'\t' } else { b',' };
if is_gz(fmt) {
let mut w = GzEncoder::new(BufWriter::new(f), Compression::default());
CsvWriter::new(&mut w)
.with_separator(sep)
.include_header(true)
.finish(&mut df)?;
w.finish()?; } else {
let mut w = BufWriter::new(f);
CsvWriter::new(&mut w)
.with_separator(sep)
.include_header(true)
.finish(&mut df)?;
w.flush()?;
}
}
Format::Auto => unreachable!("resolve_format removes Auto"),
}
Ok(())
}