rsomics_seq_grep/
region.rs1use rsomics_common::{Result, RsomicsError};
2
3#[derive(Clone, Copy)]
5pub struct Region {
6 pub start: i64,
7 pub end: i64,
8}
9
10impl Region {
11 pub fn parse(s: &str) -> Result<Self> {
13 let (left, right) = s.split_once(':').ok_or_else(|| {
14 RsomicsError::InvalidInput(format!("invalid region `{s}` — expect start:end"))
15 })?;
16
17 let start: i64 = left
18 .parse()
19 .map_err(|_| RsomicsError::InvalidInput(format!("invalid region start `{left}`")))?;
20 let end: i64 = right
21 .parse()
22 .map_err(|_| RsomicsError::InvalidInput(format!("invalid region end `{right}`")))?;
23
24 if start == 0 || end == 0 {
25 return Err(RsomicsError::InvalidInput(
26 "both start and end should not be 0".to_string(),
27 ));
28 }
29 if start < 0 && end > 0 {
30 return Err(RsomicsError::InvalidInput(
31 "when start < 0, end should not > 0".to_string(),
32 ));
33 }
34
35 Ok(Self { start, end })
36 }
37
38 #[must_use]
41 pub fn resolve(&self, seq_len: usize) -> (usize, usize) {
42 #[allow(clippy::cast_possible_wrap)]
43 let len = seq_len as i64;
44
45 let start0 = if self.start > 0 {
46 self.start - 1
47 } else {
48 (len + self.start).max(0)
49 };
50 let end0_incl = if self.end > 0 {
51 self.end.min(len) - 1
52 } else {
53 len + self.end
54 };
55
56 if end0_incl < 0 || start0 > end0_incl {
57 return (0, 0);
58 }
59
60 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
61 let s = start0.clamp(0, len) as usize;
62 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
63 let e = (end0_incl + 1).clamp(0, len) as usize;
64 (s, e)
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn first_twelve() {
74 let r = Region::parse("1:12").unwrap();
75 assert_eq!(r.resolve(20), (0, 12));
76 }
77
78 #[test]
79 fn last_one() {
80 let r = Region::parse("-1:-1").unwrap();
81 assert_eq!(r.resolve(10), (9, 10));
82 }
83
84 #[test]
85 fn out_of_bounds_is_empty() {
86 let r = Region::parse("100:200").unwrap();
87 assert_eq!(r.resolve(10), (0, 0));
88 }
89
90 #[test]
91 fn rejects_zero() {
92 assert!(Region::parse("0:5").is_err());
93 }
94
95 #[test]
96 fn rejects_mixed_sign() {
97 assert!(Region::parse("-5:5").is_err());
98 }
99}