use md5::{Digest, Md5};
use std::fmt::{self, Display};
use super::coords::CoordinateMode;
#[derive(Eq, PartialEq, Hash, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Region {
pub chr: String,
pub start: u32,
pub end: u32,
pub rest: Option<String>,
}
impl Region {
pub fn width(&self) -> u32 {
self.end - self.start
}
pub fn as_string(&self) -> String {
format!(
"{}\t{}\t{}{}",
self.chr,
self.start,
self.end,
self.rest
.as_deref()
.map_or(String::new(), |s| format!("\t{}", s)),
)
}
pub fn digest(&self) -> String {
let digest_string = format!("{},{},{}", self.chr, self.start, self.end);
let mut hasher = Md5::new();
hasher.update(digest_string);
let chrom_hash = hasher.finalize();
format!("{:x}", chrom_hash)
}
pub fn mid_point(&self) -> u32 {
self.start + self.width() / 2
}
pub fn mid_point_with_mode(&self, mode: CoordinateMode) -> u32 {
match mode {
CoordinateMode::Bed => self.start + self.width() / 2,
CoordinateMode::GRanges => {
let w = self.width();
if w % 4 == 2 {
self.start + w / 2 - 1
} else {
self.start + w / 2
}
}
}
}
pub fn distance_to(&self, other: &Region) -> i64 {
if self.start < other.end && other.start < self.end {
0i64
} else if other.end <= self.start {
(self.start as i64) - (other.end as i64)
} else {
(other.start as i64) - (self.end as i64)
}
}
}
impl Display for Region {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
fn region(start: u32, end: u32) -> Region {
Region {
chr: "chr1".to_string(),
start,
end,
rest: None,
}
}
#[rstest]
#[case::overlap((100, 200), (150, 250), 0)]
#[case::contained((100, 300), (150, 200), 0)]
#[case::adjacent((100, 200), (200, 300), 0)]
#[case::gap((100, 200), (250, 300), 50)]
fn test_distance_to(#[case] a: (u32, u32), #[case] b: (u32, u32), #[case] expected: i64) {
let (a, b) = (region(a.0, a.1), region(b.0, b.1));
assert_eq!(a.distance_to(&b), expected);
assert_eq!(b.distance_to(&a), expected);
}
}