#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssemblyReportEntry {
pub sequence_name: String,
pub sequence_role: String,
pub assigned_molecule: String,
pub genbank_accession: String,
pub refseq_accession: String,
pub ucsc_name: String,
}
impl AssemblyReportEntry {
pub fn is_assembled_molecule(&self) -> bool {
self.sequence_role == "assembled-molecule"
}
pub fn has_refseq_accession(&self) -> bool {
!self.refseq_accession.is_empty() && self.refseq_accession != "na"
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AssemblyReport {
pub assembly_name: String,
pub entries: Vec<AssemblyReportEntry>,
}
impl AssemblyReport {
pub fn assembled_molecules_with_refseq(&self) -> impl Iterator<Item = &AssemblyReportEntry> {
self.entries
.iter()
.filter(|e| e.is_assembled_molecule() && e.has_refseq_accession())
}
}
const COMMENT_PREFIX: &str = "#";
const ASSEMBLY_NAME_KEY: &str = "# Assembly name:";
const MIN_COLUMNS: usize = 10;
pub fn parse_assembly_report(text: &str) -> AssemblyReport {
let mut assembly_name = String::new();
let mut entries = Vec::new();
for line in text.lines() {
if line.starts_with(COMMENT_PREFIX) {
if let Some(name) = line.strip_prefix(ASSEMBLY_NAME_KEY) {
let name = name.trim();
if !name.is_empty() {
assembly_name = name.to_string();
}
}
continue;
}
if line.trim().is_empty() {
continue;
}
let cols: Vec<&str> = line.split('\t').collect();
if cols.len() < MIN_COLUMNS {
continue;
}
entries.push(AssemblyReportEntry {
sequence_name: cols[0].to_string(),
sequence_role: cols[1].to_string(),
assigned_molecule: cols[2].to_string(),
genbank_accession: cols[4].to_string(),
refseq_accession: cols[6].to_string(),
ucsc_name: cols[9].to_string(),
});
}
AssemblyReport {
assembly_name,
entries,
}
}
#[cfg(test)]
mod tests {
use super::*;
const GRCH38_SAMPLE: &str = "\
# Assembly name: GRCh38.p14
# Organism name: Homo sapiens (human)
# Assembly level: Chromosome
# RefSeq assembly accession: GCF_000001405.40
#
## Assembly-Units:
## GenBank Unit Accession\tRefSeq Unit Accession\tAssembly-Unit name
## GCA_000001305.2\tGCF_000001305.15\tPrimary Assembly
# Sequence-Name\tSequence-Role\tAssigned-Molecule\tAssigned-Molecule-Location/Type\tGenBank-Accn\tRelationship\tRefSeq-Accn\tAssembly-Unit\tSequence-Length\tUCSC-style-name
1\tassembled-molecule\t1\tChromosome\tCM000663.2\t=\tNC_000001.11\tPrimary Assembly\t248956422\tchr1
17\tassembled-molecule\t17\tChromosome\tCM000679.2\t=\tNC_000017.11\tPrimary Assembly\t83257441\tchr17
X\tassembled-molecule\tX\tChromosome\tCM000685.2\t=\tNC_000023.11\tPrimary Assembly\t156040895\tchrX
MT\tassembled-molecule\tMT\tMitochondrion\tJ01415.2\t=\tNC_012920.1\tnon-nuclear\t16569\tchrM
HSCHR1_CTG1\talt-scaffold\t1\tChromosome\tKI270762.1\t=\tNT_187515.1\tALT_REF_LOCI_1\t354444\tchr1_KI270762v1_alt
";
#[test]
fn parses_assembly_name_from_header() {
let report = parse_assembly_report(GRCH38_SAMPLE);
assert_eq!(report.assembly_name, "GRCh38.p14");
}
#[test]
fn parses_assembled_molecule_rows() {
let report = parse_assembly_report(GRCH38_SAMPLE);
assert_eq!(report.entries.len(), 5);
let chr1 = &report.entries[0];
assert_eq!(chr1.sequence_name, "1");
assert_eq!(chr1.sequence_role, "assembled-molecule");
assert_eq!(chr1.refseq_accession, "NC_000001.11");
assert_eq!(chr1.genbank_accession, "CM000663.2");
assert_eq!(chr1.ucsc_name, "chr1");
assert!(chr1.is_assembled_molecule());
assert!(chr1.has_refseq_accession());
}
#[test]
fn assembled_molecules_with_refseq_excludes_alt_scaffolds() {
let report = parse_assembly_report(GRCH38_SAMPLE);
let refseqs: Vec<&str> = report
.assembled_molecules_with_refseq()
.map(|e| e.refseq_accession.as_str())
.collect();
assert_eq!(
refseqs,
vec![
"NC_000001.11",
"NC_000017.11",
"NC_000023.11",
"NC_012920.1"
],
"alt-scaffold NT_187515.1 must be excluded from inference rows"
);
}
#[test]
fn skips_comment_and_short_rows() {
let text = "\
# some banner
1\tassembled-molecule\t1\tChromosome\tCM000663.2\t=\tNC_000001.11\tPrimary Assembly\t248956422\tchr1
short\trow\twith\tfew\tcols
";
let report = parse_assembly_report(text);
assert_eq!(report.assembly_name, "");
assert_eq!(report.entries.len(), 1);
assert_eq!(report.entries[0].refseq_accession, "NC_000001.11");
}
#[test]
fn treats_na_refseq_as_absent() {
let text = "\
# Assembly name: GRCh38.p14
HSCHR1_RANDOM\tunplaced-scaffold\tna\tna\tGL000008.2\t<>\tna\tPrimary Assembly\t209709\tchr1_GL000008v2_random
";
let report = parse_assembly_report(text);
assert_eq!(report.entries.len(), 1);
assert!(!report.entries[0].has_refseq_accession());
assert_eq!(report.assembled_molecules_with_refseq().count(), 0);
}
#[test]
fn empty_input_yields_empty_report() {
let report = parse_assembly_report("");
assert_eq!(report, AssemblyReport::default());
}
}