use std::io::BufRead;
use std::path::Path;
use indexmap::IndexMap;
use crate::common::cli::canonicalize;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ContigDict {
names: Vec<String>,
lengths: Vec<u64>,
alias_to_id: IndexMap<String, u32>,
}
impl ContigDict {
pub const MAX_CONTIGS: usize = 1 << 24;
pub fn from_entries(
entries: impl IntoIterator<Item = (String, u64)>,
) -> Result<Self, anyhow::Error> {
let mut dict = Self::default();
for (name, length) in entries {
if dict.names.len() >= Self::MAX_CONTIGS {
anyhow::bail!(
"too many contigs: the compact key format supports at most {}",
Self::MAX_CONTIGS
);
}
let id = dict.names.len() as u32;
for alias in [name.clone(), canonicalize(&name)] {
match dict.alias_to_id.get(&alias) {
Some(&existing) if existing != id => anyhow::bail!(
"contig alias {:?} for {:?} collides with existing contig id {}",
alias,
name,
existing
),
Some(_) => {}
None => {
dict.alias_to_id.insert(alias, id);
}
}
}
dict.names.push(name);
dict.lengths.push(length);
}
Ok(dict)
}
pub fn from_fai(path: impl AsRef<Path>) -> Result<Self, anyhow::Error> {
let path = path.as_ref();
let file = std::fs::File::open(path)
.map_err(|e| anyhow::anyhow!("failed to open .fai {}: {}", path.display(), e))?;
let mut entries = Vec::new();
for line in std::io::BufReader::new(file).lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let mut cols = line.split('\t');
let name = cols
.next()
.ok_or_else(|| anyhow::anyhow!("malformed .fai line: {:?}", line))?
.to_string();
let length = cols
.next()
.ok_or_else(|| anyhow::anyhow!("missing length in .fai line: {:?}", line))?
.parse::<u64>()
.map_err(|e| anyhow::anyhow!("bad length in .fai line {:?}: {}", line, e))?;
entries.push((name, length));
}
if entries.is_empty() {
anyhow::bail!("no contigs found in .fai {}", path.display());
}
Self::from_entries(entries)
}
pub fn len(&self) -> usize {
self.names.len()
}
pub fn is_empty(&self) -> bool {
self.names.is_empty()
}
pub fn id_of(&self, name: &str) -> Option<u32> {
self.alias_to_id
.get(name)
.copied()
.or_else(|| self.alias_to_id.get(&canonicalize(name)).copied())
}
pub fn name_of(&self, id: u32) -> Option<&str> {
self.names.get(id as usize).map(String::as_str)
}
pub fn length_of(&self, id: u32) -> Option<u64> {
self.lengths.get(id as usize).copied()
}
pub fn id_to_name(&self) -> &[String] {
&self.names
}
pub fn to_meta_string(&self) -> String {
let mut out = String::new();
for (id, (name, length)) in self.names.iter().zip(self.lengths.iter()).enumerate() {
out.push_str(&format!("{}\t{}\t{}\n", id, name, length));
}
out
}
pub fn from_meta_string(s: &str) -> Result<Self, anyhow::Error> {
let mut entries: Vec<(u32, String, u64)> = Vec::new();
for line in s.lines() {
if line.trim().is_empty() {
continue;
}
let mut cols = line.split('\t');
let id = cols
.next()
.ok_or_else(|| anyhow::anyhow!("malformed contig meta line: {:?}", line))?
.parse::<u32>()?;
let name = cols
.next()
.ok_or_else(|| anyhow::anyhow!("missing name in contig meta line: {:?}", line))?
.to_string();
let length = cols
.next()
.ok_or_else(|| anyhow::anyhow!("missing length in contig meta line: {:?}", line))?
.parse::<u64>()?;
entries.push((id, name, length));
}
entries.sort_by_key(|(id, _, _)| *id);
for (expected, (id, _, _)) in entries.iter().enumerate() {
if *id as usize != expected {
anyhow::bail!("contig meta IDs are not contiguous from 0 (got {})", id);
}
}
Self::from_entries(entries.into_iter().map(|(_, name, length)| (name, length)))
}
}
#[cfg(test)]
mod test {
use super::*;
fn dict() -> ContigDict {
ContigDict::from_entries([
("1".to_string(), 249_250_621),
("2".to_string(), 243_199_373),
("X".to_string(), 155_270_560),
("MT".to_string(), 16_569),
])
.unwrap()
}
#[test]
fn colliding_alias_is_rejected() {
let err = ContigDict::from_entries([("MT".to_string(), 16_569), ("M".to_string(), 16_569)]);
assert!(err.is_err());
}
#[test]
fn ids_assigned_in_order() {
let d = dict();
assert_eq!(d.len(), 4);
assert_eq!(d.id_of("1"), Some(0));
assert_eq!(d.id_of("X"), Some(2));
assert_eq!(d.name_of(3), Some("MT"));
assert_eq!(d.length_of(0), Some(249_250_621));
}
#[test]
fn alias_resolution_via_canonicalize() {
let d = dict();
assert_eq!(d.id_of("chr1"), Some(0));
assert_eq!(d.id_of("chrM"), Some(3));
assert_eq!(d.id_of("M"), Some(3));
assert_eq!(d.id_of("nonexistent"), None);
}
#[test]
fn meta_string_roundtrip() {
let d = dict();
let parsed = ContigDict::from_meta_string(&d.to_meta_string()).unwrap();
assert_eq!(parsed, d);
}
}