1use anyhow::{Result, bail};
2
3#[derive(Debug, Clone)]
5struct PloidyEntry {
6 contig: Option<String>,
8 range: Option<(u32, u32)>,
11 ploidy: u8,
13}
14
15#[derive(Debug, Clone)]
29pub struct PloidyMap {
30 default_ploidy: u8,
32 overrides: Vec<PloidyEntry>,
34}
35
36impl PloidyMap {
37 pub fn new(default_ploidy: u8, override_specs: &[String]) -> Result<Self> {
46 let mut overrides = Vec::with_capacity(override_specs.len());
47
48 for spec in override_specs {
49 let entry = parse_ploidy_override(spec)?;
50 overrides.push(entry);
51 }
52
53 Ok(Self { default_ploidy, overrides })
54 }
55
56 #[must_use]
61 pub fn ploidy_at(&self, contig: &str, position: u32) -> u8 {
62 let mut result = self.default_ploidy;
63
64 for entry in &self.overrides {
65 let contig_matches = entry.contig.as_ref().is_none_or(|c| c == contig);
66
67 if !contig_matches {
68 continue;
69 }
70
71 let range_matches = entry
72 .range
73 .as_ref()
74 .is_none_or(|&(start, end)| position >= start && position < end);
75
76 if range_matches {
77 result = entry.ploidy;
78 }
79 }
80
81 result
82 }
83
84 #[must_use]
86 pub fn default_ploidy(&self) -> u8 {
87 self.default_ploidy
88 }
89}
90
91fn parse_ploidy_override(spec: &str) -> Result<PloidyEntry> {
97 let (location, ploidy_str) = spec
98 .rsplit_once('=')
99 .ok_or_else(|| anyhow::anyhow!("Invalid ploidy override format: '{spec}'. Expected CONTIG=PLOIDY or CONTIG:START-END=PLOIDY"))?;
100
101 let ploidy: u8 = ploidy_str
102 .parse()
103 .map_err(|_| anyhow::anyhow!("Invalid ploidy value in override: '{ploidy_str}'"))?;
104
105 if let Some((contig, range_str)) = location.split_once(':') {
106 let (start_str, end_str) = range_str.split_once('-').ok_or_else(|| {
108 anyhow::anyhow!(
109 "Invalid range format in ploidy override: '{range_str}'. Expected START-END"
110 )
111 })?;
112
113 let start_1based: u32 = start_str.parse().map_err(|_| {
114 anyhow::anyhow!("Invalid start position in ploidy override: '{start_str}'")
115 })?;
116 let end_1based: u32 = end_str
117 .parse()
118 .map_err(|_| anyhow::anyhow!("Invalid end position in ploidy override: '{end_str}'"))?;
119
120 if start_1based == 0 {
121 bail!("Ploidy override start position must be >= 1: '{spec}'");
122 }
123 if end_1based < start_1based {
124 bail!("Ploidy override end < start: '{spec}'");
125 }
126
127 let start_0based = start_1based - 1;
129 let end_0based = end_1based; Ok(PloidyEntry {
132 contig: Some(contig.to_string()),
133 range: Some((start_0based, end_0based)),
134 ploidy,
135 })
136 } else {
137 Ok(PloidyEntry { contig: Some(location.to_string()), range: None, ploidy })
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_default_ploidy() {
148 let map = PloidyMap::new(2, &[]).unwrap();
149 assert_eq!(map.ploidy_at("chr1", 0), 2);
150 assert_eq!(map.ploidy_at("chrX", 1000), 2);
151 }
152
153 #[test]
154 fn test_whole_contig_override() {
155 let map = PloidyMap::new(2, &["chrX=1".to_string()]).unwrap();
156 assert_eq!(map.ploidy_at("chr1", 0), 2);
157 assert_eq!(map.ploidy_at("chrX", 0), 1);
158 assert_eq!(map.ploidy_at("chrX", 999_999), 1);
159 }
160
161 #[test]
162 fn test_range_override() {
163 let overrides = vec![
164 "chrX=1".to_string(),
165 "chrX:10001-2781479=2".to_string(), ];
167 let map = PloidyMap::new(2, &overrides).unwrap();
168
169 assert_eq!(map.ploidy_at("chr1", 100), 2);
171 assert_eq!(map.ploidy_at("chrX", 5000), 1);
173 assert_eq!(map.ploidy_at("chrX", 10000), 2);
175 assert_eq!(map.ploidy_at("chrX", 2_781_478), 2);
176 assert_eq!(map.ploidy_at("chrX", 2_781_479), 1);
178 }
179
180 #[test]
181 fn test_last_writer_wins() {
182 let overrides = vec!["chrX=1".to_string(), "chrX=3".to_string()];
184 let map = PloidyMap::new(2, &overrides).unwrap();
185 assert_eq!(map.ploidy_at("chrX", 0), 3);
187 }
188
189 #[test]
190 fn test_parse_errors() {
191 assert!(PloidyMap::new(2, &["chrX".to_string()]).is_err());
193 assert!(PloidyMap::new(2, &["chrX=abc".to_string()]).is_err());
195 assert!(PloidyMap::new(2, &["chrX:100=1".to_string()]).is_err());
197 assert!(PloidyMap::new(2, &["chrX:0-100=1".to_string()]).is_err());
199 assert!(PloidyMap::new(2, &["chrX:200-100=1".to_string()]).is_err());
201 }
202
203 #[test]
204 fn test_multiple_contigs_and_ranges() {
205 let overrides = vec![
206 "chrX=1".to_string(),
207 "chrY=1".to_string(),
208 "chrX:10001-2781479=2".to_string(),
209 "chrX:155701383-156030895=2".to_string(),
210 ];
211 let map = PloidyMap::new(2, &overrides).unwrap();
212
213 assert_eq!(map.ploidy_at("chr1", 100), 2);
214 assert_eq!(map.ploidy_at("chrX", 5000), 1);
215 assert_eq!(map.ploidy_at("chrX", 50000), 2); assert_eq!(map.ploidy_at("chrX", 100_000_000), 1); assert_eq!(map.ploidy_at("chrX", 155_800_000), 2); assert_eq!(map.ploidy_at("chrY", 100), 1);
219 }
220}