use arrow::datatypes::DataType;
use clap::{Parser, ValueEnum};
pub const MANDATORY_HEADERS: [(&str, DataType); 11] = [
("QNAME", DataType::Utf8),
("FLAG", DataType::Int32),
("RNAME", DataType::Utf8),
("POS", DataType::Int32),
("MAPQ", DataType::Int32),
("CIGAR", DataType::Utf8),
("RNEXT", DataType::Utf8),
("PNEXT", DataType::Int32),
("TLEN", DataType::Int32),
("SEQ", DataType::Utf8),
("QUAL", DataType::Utf8),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Csv,
Tsv,
Psv,
Parquet,
Custom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum InputFormat {
Auto,
Sam,
Bam,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Sampler {
Bernoulli(f64),
Reservoir(usize),
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None, allow_negative_numbers = true)]
pub struct Args {
#[arg(default_value = "-")]
pub input: String,
#[arg(short = 'o', long)]
pub output: Option<String>,
#[arg(short = 'n', long, default_value_t = 1_000_000)]
pub limit: usize,
#[arg(long, default_value_t = 100_000)]
pub detect_limit: usize,
#[arg(short = 'f', long, value_enum)]
pub format: Option<OutputFormat>,
#[arg(long, value_enum, default_value_t = InputFormat::Auto)]
pub input_format: InputFormat,
#[arg(short = 'd', long, default_value = ",")]
pub delimiter: String,
#[arg(long)]
pub keep_tag_prefix: bool,
#[arg(long)]
pub no_quotes: bool,
#[arg(long)]
pub downsample: Option<f64>,
#[arg(long)]
pub seed: Option<u64>,
}
impl Args {
pub fn sampler(&self) -> Result<Option<Sampler>, &'static str> {
let Some(value) = self.downsample else {
return Ok(None);
};
if !value.is_finite() || value <= 0.0 {
return Err("--downsample must be a positive number");
}
if value < 1.0 {
Ok(Some(Sampler::Bernoulli(value)))
} else if value.fract() == 0.0 {
Ok(Some(Sampler::Reservoir(value as usize)))
} else {
Err("--downsample >= 1 must be an integer count; use a fraction < 1 for a percent")
}
}
}
pub fn resolve_output_format(args: &Args) -> OutputFormat {
if let Some(format) = args.format {
return format;
}
let extension = args
.output
.as_deref()
.map(std::path::Path::new)
.and_then(std::path::Path::extension)
.and_then(std::ffi::OsStr::to_str);
match extension {
Some("tsv") => OutputFormat::Tsv,
Some("psv") => OutputFormat::Psv,
Some("parquet") | Some("parq") => OutputFormat::Parquet,
_ => OutputFormat::Csv,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args(downsample: Option<f64>) -> Args {
Args {
input: "-".into(),
output: None,
limit: 1_000_000,
detect_limit: 100_000,
format: None,
input_format: InputFormat::Auto,
delimiter: ",".into(),
keep_tag_prefix: false,
no_quotes: false,
downsample,
seed: None,
}
}
#[test]
fn sampler_classifies_fraction_and_count() {
assert_eq!(
args(Some(0.1)).sampler().unwrap(),
Some(Sampler::Bernoulli(0.1))
);
assert_eq!(
args(Some(0.5)).sampler().unwrap(),
Some(Sampler::Bernoulli(0.5))
);
assert_eq!(
args(Some(100.0)).sampler().unwrap(),
Some(Sampler::Reservoir(100))
);
assert_eq!(
args(Some(1.0)).sampler().unwrap(),
Some(Sampler::Reservoir(1))
);
assert_eq!(args(None).sampler().unwrap(), None);
}
#[test]
fn sampler_rejects_invalid_values() {
assert!(args(Some(0.0)).sampler().is_err());
assert!(args(Some(-0.5)).sampler().is_err());
assert!(args(Some(2.5)).sampler().is_err());
}
}