use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io;
use std::path::Path;
use csv;
use regex::Regex;
use crate::config::{Config, Delimiter};
use crate::select::SelectColumns;
use crate::util::FilenameTemplate;
use crate::CliResult;
use clap::Parser;
#[derive(Parser, Clone, Debug)]
pub struct Args {
#[arg()]
pub arg_column: SelectColumns,
#[arg()]
pub arg_outdir: String,
#[arg()]
pub arg_input: Option<String>,
#[arg(long = "filename", value_name = "arg", default_value = "{}.csv")]
pub flag_filename: FilenameTemplate,
#[arg(long = "prefix-length", value_name = "arg")]
pub flag_prefix_length: Option<usize>,
#[arg(long = "drop")]
pub flag_drop: bool,
#[arg(short = 'n', long = "no-headers")]
pub flag_no_headers: bool,
#[arg(short = 'd', long = "delimiter", value_name = "arg")]
pub flag_delimiter: Option<Delimiter>,
}
pub fn run(args: &Args) -> CliResult<()> {
fs::create_dir_all(&args.arg_outdir)?;
args.sequential_partition()
}
impl Args {
fn rconfig(&self) -> Config {
Config::new(&self.arg_input)
.delimiter(self.flag_delimiter)
.no_headers(self.flag_no_headers)
.select(self.arg_column.clone())
}
fn key_column(&self, rconfig: &Config, headers: &csv::ByteRecord) -> CliResult<usize> {
let select_cols = rconfig.selection(headers)?;
if select_cols.len() == 1 {
Ok(select_cols[0])
} else {
fail!("can only partition on one column")
}
}
fn sequential_partition(&self) -> CliResult<()> {
let rconfig = self.rconfig();
let mut rdr = rconfig.reader()?;
let headers = rdr.byte_headers()?.clone();
let key_col = self.key_column(&rconfig, &headers)?;
let mut gen = WriterGenerator::new(self.flag_filename.clone());
let mut writers: HashMap<Vec<u8>, BoxedWriter> = HashMap::new();
let mut row = csv::ByteRecord::new();
while rdr.read_byte_record(&mut row)? {
let column = &row[key_col];
let key = match self.flag_prefix_length {
Some(len) if len < column.len() => &column[0..len],
_ => column,
};
let mut entry = writers.entry(key.to_vec());
let wtr =
match entry {
Entry::Occupied(ref mut occupied) => occupied.get_mut(),
Entry::Vacant(vacant) => {
let mut wtr = gen.writer(&*self.arg_outdir, key)?;
if !rconfig.no_headers {
if self.flag_drop {
wtr.write_record(headers.iter().enumerate().filter_map(
|(i, e)| if i != key_col { Some(e) } else { None },
))?;
} else {
wtr.write_record(&headers)?;
}
}
vacant.insert(wtr)
}
};
if self.flag_drop {
wtr.write_record(row.iter().enumerate().filter_map(|(i, e)| {
if i != key_col {
Some(e)
} else {
None
}
}))?;
} else {
wtr.write_byte_record(&row)?;
}
}
Ok(())
}
}
type BoxedWriter = csv::Writer<Box<dyn io::Write + 'static>>;
struct WriterGenerator {
template: FilenameTemplate,
counter: usize,
used: HashSet<String>,
non_word_char: Regex,
}
impl WriterGenerator {
fn new(template: FilenameTemplate) -> WriterGenerator {
WriterGenerator {
template,
counter: 1,
used: HashSet::new(),
non_word_char: Regex::new(r"\W").unwrap(),
}
}
fn writer<P>(&mut self, path: P, key: &[u8]) -> io::Result<BoxedWriter>
where
P: AsRef<Path>,
{
let unique_value = self.unique_value(key);
self.template.writer(path.as_ref(), &unique_value)
}
fn unique_value(&mut self, key: &[u8]) -> String {
let utf8 = String::from_utf8_lossy(key);
let safe = self.non_word_char.replace_all(&utf8, "").into_owned();
let base = if safe.is_empty() {
"empty".to_owned()
} else {
safe
};
if !self.used.contains(&base) {
self.used.insert(base.clone());
base
} else {
loop {
let candidate = format!("{}_{}", base, self.counter);
self.counter = self.counter.checked_add(1).unwrap_or_else(|| {
panic!("Cannot generate unique value")
});
if !self.used.contains(&candidate) {
self.used.insert(candidate.clone());
return candidate;
}
}
}
}
}