use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct Contig {
pub id: u32,
pub name: Arc<str>,
pub length: u32,
pub global_offset: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Position(
pub u32,
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Locus {
pub contig: u32,
pub pos: Position,
}
#[derive(Debug, Clone, Default)]
pub struct ContigSet {
contigs: Vec<Contig>,
}
impl ContigSet {
pub fn new() -> Self {
Self {
contigs: Vec::new(),
}
}
pub fn push(&mut self, name: impl Into<Arc<str>>, length: u32) -> u32 {
let id = u32::try_from(self.contigs.len()).expect("contig count exceeds u32::MAX");
let global_offset = self
.contigs
.last()
.map_or(0, |c| c.global_offset + c.length as u64);
self.contigs.push(Contig {
id,
name: name.into(),
length,
global_offset,
});
id
}
pub fn by_id(&self, id: u32) -> Option<&Contig> {
self.contigs.get(id as usize)
}
pub fn by_name(&self, name: &str) -> Option<&Contig> {
self.contigs.iter().find(|c| c.name.as_ref() == name)
}
pub fn len(&self) -> usize {
self.contigs.len()
}
pub fn is_empty(&self) -> bool {
self.contigs.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Contig> {
self.contigs.iter()
}
pub fn total_length(&self) -> u64 {
self.contigs
.last()
.map_or(0, |c| c.global_offset + c.length as u64)
}
pub fn resolve(&self, global: u64) -> Option<Locus> {
let after = self.contigs.partition_point(|c| c.global_offset <= global);
let contig = self.contigs.get(after.checked_sub(1)?)?;
let pos = global - contig.global_offset;
if pos < contig.length as u64 {
Some(Locus {
contig: contig.id,
pos: Position(pos as u32),
})
} else {
None
}
}
pub fn resolve_span(&self, global: u64, len: u32) -> Option<Locus> {
let start = self.resolve(global)?;
let contig = self.by_id(start.contig)?;
let end = global.checked_add(len as u64)?;
if end <= contig.global_offset + contig.length as u64 {
Some(start)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contig_set_assigns_ids_and_global_offsets() {
let mut set = ContigSet::new();
let chr1 = set.push("chr1", 1_000);
let chr2 = set.push("chr2", 500);
assert_eq!(chr1, 0);
assert_eq!(chr2, 1);
assert_eq!(set.by_id(chr1).unwrap().global_offset, 0);
assert_eq!(set.by_id(chr2).unwrap().global_offset, 1_000);
assert_eq!(set.by_name("chr2").unwrap().id, 1);
assert_eq!(set.total_length(), 1_500);
}
#[test]
fn loci_order_by_contig_then_position() {
let a = Locus {
contig: 0,
pos: Position(100),
};
let b = Locus {
contig: 0,
pos: Position(200),
};
let c = Locus {
contig: 1,
pos: Position(0),
};
assert!(a < b && b < c);
}
#[test]
fn by_id_out_of_range_is_none() {
let mut set = ContigSet::new();
set.push("chr1", 1_000);
assert!(set.by_id(99).is_none());
}
#[test]
fn empty_contig_set_is_empty_with_zero_total_length() {
let set = ContigSet::new();
assert!(set.is_empty());
assert_eq!(set.len(), 0);
assert_eq!(set.total_length(), 0);
}
#[test]
fn resolve_maps_global_offset_to_locus() {
let mut set = ContigSet::new();
set.push("chr1", 10); set.push("chr2", 5); assert_eq!(
set.resolve(0),
Some(Locus {
contig: 0,
pos: Position(0)
})
);
assert_eq!(
set.resolve(9),
Some(Locus {
contig: 0,
pos: Position(9)
})
);
assert_eq!(
set.resolve(10),
Some(Locus {
contig: 1,
pos: Position(0)
})
);
assert_eq!(
set.resolve(14),
Some(Locus {
contig: 1,
pos: Position(4)
})
);
}
#[test]
fn resolve_out_of_range_is_none() {
let mut set = ContigSet::new();
set.push("chr1", 10);
assert_eq!(set.resolve(10), None); assert_eq!(set.resolve(100), None);
assert_eq!(ContigSet::new().resolve(0), None); }
#[test]
fn resolve_span_within_a_contig_returns_start() {
let mut set = ContigSet::new();
set.push("chr1", 10);
set.push("chr2", 5);
assert_eq!(
set.resolve_span(8, 2),
Some(Locus {
contig: 0,
pos: Position(8)
})
); assert_eq!(
set.resolve_span(10, 5),
Some(Locus {
contig: 1,
pos: Position(0)
})
); }
#[test]
fn resolve_span_crossing_a_boundary_is_rejected() {
let mut set = ContigSet::new();
set.push("chr1", 10);
set.push("chr2", 5);
assert_eq!(set.resolve_span(8, 4), None); assert_eq!(set.resolve_span(12, 4), None); }
}