use rsomics_common::{Result, RsomicsError};
#[derive(Clone, Copy)]
pub struct Region {
pub start: i64,
pub end: i64,
}
impl Region {
pub fn parse(s: &str) -> Result<Self> {
let (left, right) = s.split_once(':').ok_or_else(|| {
RsomicsError::InvalidInput(format!("invalid region `{s}` — expect start:end"))
})?;
let start: i64 = left
.parse()
.map_err(|_| RsomicsError::InvalidInput(format!("invalid region start `{left}`")))?;
let end: i64 = right
.parse()
.map_err(|_| RsomicsError::InvalidInput(format!("invalid region end `{right}`")))?;
if start == 0 || end == 0 {
return Err(RsomicsError::InvalidInput(
"both start and end should not be 0".to_string(),
));
}
if start < 0 && end > 0 {
return Err(RsomicsError::InvalidInput(
"when start < 0, end should not > 0".to_string(),
));
}
Ok(Self { start, end })
}
#[must_use]
pub fn resolve(&self, seq_len: usize) -> (usize, usize) {
#[allow(clippy::cast_possible_wrap)]
let len = seq_len as i64;
let start0 = if self.start > 0 {
self.start - 1
} else {
(len + self.start).max(0)
};
let end0_incl = if self.end > 0 {
self.end.min(len) - 1
} else {
len + self.end
};
if end0_incl < 0 || start0 > end0_incl {
return (0, 0);
}
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let s = start0.clamp(0, len) as usize;
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let e = (end0_incl + 1).clamp(0, len) as usize;
(s, e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_twelve() {
let r = Region::parse("1:12").unwrap();
assert_eq!(r.resolve(20), (0, 12));
}
#[test]
fn last_one() {
let r = Region::parse("-1:-1").unwrap();
assert_eq!(r.resolve(10), (9, 10));
}
#[test]
fn out_of_bounds_is_empty() {
let r = Region::parse("100:200").unwrap();
assert_eq!(r.resolve(10), (0, 0));
}
#[test]
fn rejects_zero() {
assert!(Region::parse("0:5").is_err());
}
#[test]
fn rejects_mixed_sign() {
assert!(Region::parse("-5:5").is_err());
}
}