1use std::num::ParseIntError;
2
3use thiserror::Error;
4
5#[derive(Debug, PartialEq)]
6pub struct Region {
7 pub name: String,
8 pub start: i64,
9 pub end: i64,
10}
11
12#[derive(Debug, Error, PartialEq)]
13pub enum RegionParseError {
14 #[error("Region start is less than region end")]
15 InvalidRange,
16 #[error("Parsing error: Region name not present")]
17 NoName,
18 #[error("Parsing error: Region coordinates not present")]
19 NoCoordinates,
20 #[error("Parsing error: Start coordinate not present")]
21 NoStartCoordinate,
22 #[error("Parsing error: End coordinate not present")]
23 NoEndCoordinate,
24 #[error("Parsing error: Invalid coordinate: {0}")]
25 InvalidCoordinate(#[from] ParseIntError),
26}
27
28impl Region {
29 pub fn parse(region_string: &str) -> Result<Self, RegionParseError> {
30 let mut parts = region_string.split(':');
32 let name = parts.next().ok_or(RegionParseError::NoName)?.to_string();
33 let interval = parts.next().ok_or(RegionParseError::NoCoordinates)?;
34 let mut bounds = interval.split('-');
35 let start = bounds
36 .next()
37 .ok_or(RegionParseError::NoStartCoordinate)?
38 .parse::<i64>()
39 .map_err(RegionParseError::InvalidCoordinate)?;
40 let end = bounds
41 .next()
42 .ok_or(RegionParseError::NoEndCoordinate)?
43 .parse::<i64>()
44 .map_err(RegionParseError::InvalidCoordinate)?;
45 if start > end {
46 return Err(RegionParseError::InvalidRange);
47 }
48
49 Ok(Self { name, start, end })
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn test_parse_valid_region() {
59 let region = Region::parse("chr1:100-200");
60 assert_eq!(
61 region,
62 Ok(Region {
63 name: "chr1".to_string(),
64 start: 100,
65 end: 200,
66 })
67 );
68 }
69
70 #[test]
71 fn test_parse_invalid_format() {
72 let region = Region::parse("chr1-100-200");
73 assert_eq!(region, Err(RegionParseError::NoCoordinates));
74 }
75
76 #[test]
77 fn test_parse_start_greater_than_end() {
78 let region = Region::parse("chr1:300-200");
79 assert_eq!(region, Err(RegionParseError::InvalidRange));
80 }
81
82 #[test]
83 fn test_region_applies_to_array_slice() {
84 let string = "foobarbaz";
85 let region = Region::parse("chr1:3-5").unwrap();
86 let slice = &string[region.start as usize..region.end as usize];
87 assert_eq!(slice, "ba");
88 }
89}