use std::path::Path;
use anyhow::{Context, Result};
use log::debug;
use super::bam_stat::BamStatResult;
fn fmt_line(count: u64, description: &str) -> String {
format!("{count} + 0 {description}")
}
fn fmt_pct(numerator: u64, denominator: u64) -> String {
if denominator == 0 {
"(N/A : N/A)".to_string()
} else {
let pct = 100.0 * numerator as f64 / denominator as f64;
format!("({pct:.2}% : N/A)")
}
}
pub fn write_flagstat(result: &BamStatResult, output_path: &Path) -> Result<()> {
use std::io::Write;
let mut out = std::fs::File::create(output_path)
.with_context(|| format!("Failed to create flagstat file: {}", output_path.display()))?;
let total = result.total_records;
let primary = result.primary_count;
writeln!(
out,
"{}",
fmt_line(total, "in total (QC-passed reads + QC-failed reads)")
)?;
writeln!(out, "{}", fmt_line(primary, "primary"))?;
writeln!(out, "{}", fmt_line(result.secondary, "secondary"))?;
writeln!(out, "{}", fmt_line(result.supplementary, "supplementary"))?;
writeln!(out, "{}", fmt_line(result.duplicates, "duplicates"))?;
writeln!(
out,
"{}",
fmt_line(result.primary_duplicates, "primary duplicates")
)?;
writeln!(
out,
"{} {}",
fmt_line(result.mapped, "mapped"),
fmt_pct(result.mapped, total)
)?;
writeln!(
out,
"{} {}",
fmt_line(result.primary_mapped, "primary mapped"),
fmt_pct(result.primary_mapped, primary)
)?;
writeln!(
out,
"{}",
fmt_line(result.paired_flagstat, "paired in sequencing")
)?;
writeln!(out, "{}", fmt_line(result.read1_flagstat, "read1"))?;
writeln!(out, "{}", fmt_line(result.read2_flagstat, "read2"))?;
writeln!(
out,
"{} {}",
fmt_line(result.properly_paired, "properly paired"),
fmt_pct(result.properly_paired, result.paired_flagstat)
)?;
writeln!(
out,
"{}",
fmt_line(result.both_mapped, "with itself and mate mapped")
)?;
writeln!(
out,
"{} {}",
fmt_line(result.singletons, "singletons"),
fmt_pct(result.singletons, result.paired_flagstat)
)?;
writeln!(
out,
"{}",
fmt_line(result.mate_diff_chr, "with mate mapped to a different chr")
)?;
writeln!(
out,
"{}",
fmt_line(
result.mate_diff_chr_mapq5,
"with mate mapped to a different chr (mapQ>=5)"
)
)?;
debug!("Wrote flagstat output to {}", output_path.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rna::rseqc::accumulators::BamStatAccum;
use rust_htslib::bam::{self, Read as BamRead};
use std::io::Read;
#[test]
fn test_flagstat_format() {
let mut reader =
bam::Reader::from_path("tests/data/test.bam").expect("Failed to open test.bam");
let mut accum = BamStatAccum::default();
let mut record = bam::Record::new();
while let Some(res) = reader.read(&mut record) {
res.expect("Error reading BAM record");
accum.process_read(&record, 30);
}
let result = accum.into_result();
let tmp_path = std::env::temp_dir().join("rustqc_test_flagstat.txt");
write_flagstat(&result, &tmp_path).expect("Failed to write flagstat");
let mut contents = String::new();
std::fs::File::open(&tmp_path)
.unwrap()
.read_to_string(&mut contents)
.unwrap();
let _ = std::fs::remove_file(&tmp_path);
assert!(
contents.contains("in total (QC-passed reads + QC-failed reads)"),
"Missing total line in flagstat output"
);
assert!(
contents.contains("mapped"),
"Missing mapped line in flagstat output"
);
}
}