1use indexmap::IndexMap;
34
35use crate::error::{Error, Result};
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ChrEntry {
39 pub id: String,
40 pub size: i64,
41 pub index: usize,
43}
44
45#[derive(Debug, Clone, Default)]
50pub struct ChrMap {
51 entries: IndexMap<String, ChrEntry>,
52 declared_key_size: Option<usize>,
55}
56
57impl ChrMap {
58 pub fn from_entries(entries: impl IntoIterator<Item = (String, i64)>) -> Self {
65 let mut map: IndexMap<String, ChrEntry> = IndexMap::new();
66 for (id, size) in entries {
67 let index = map.get(&id).map_or(map.len(), |e| e.index);
68 map.insert(id.clone(), ChrEntry { id, size, index });
69 }
70 Self {
71 entries: map,
72 declared_key_size: None,
73 }
74 }
75
76 pub fn from_indexed_entries(entries: impl IntoIterator<Item = (String, i64, usize)>) -> Self {
83 let mut list: Vec<ChrEntry> = entries
84 .into_iter()
85 .map(|(id, size, index)| ChrEntry { id, size, index })
86 .collect();
87 list.sort_by_key(|e| e.index);
88 let mut map = IndexMap::with_capacity(list.len());
89 for entry in list {
90 map.insert(entry.id.clone(), entry);
91 }
92 Self {
93 entries: map,
94 declared_key_size: None,
95 }
96 }
97
98 pub fn with_key_size(mut self, key_size: usize) -> Self {
100 self.declared_key_size = Some(key_size);
101 self
102 }
103
104 pub fn declared_key_size(&self) -> Option<usize> {
105 self.declared_key_size
106 }
107
108 pub fn len(&self) -> usize {
109 self.entries.len()
110 }
111
112 pub fn is_empty(&self) -> bool {
113 self.entries.is_empty()
114 }
115
116 pub fn iter(&self) -> impl Iterator<Item = &ChrEntry> {
117 self.entries.values()
118 }
119
120 pub fn names(&self) -> Vec<String> {
121 self.entries.keys().cloned().collect()
122 }
123
124 pub fn by_index(&self, index: usize) -> Option<&ChrEntry> {
132 if let Some((_, entry)) = self.entries.get_index(index) {
133 if entry.index == index {
134 return Some(entry);
135 }
136 }
137 self.entries.values().find(|e| e.index == index)
138 }
139
140 pub fn genome_size(&self) -> i64 {
142 self.entries.values().map(|e| e.size).sum()
143 }
144
145 fn try_lookup(&self, id: &str) -> Option<&ChrEntry> {
148 if let Some(entry) = self.entries.get(id) {
149 return Some(entry);
150 }
151 if let Some(entry) = self.entries.get(&id.to_ascii_lowercase()) {
152 return Some(entry);
153 }
154 if let Some(entry) = self.entries.get(&id.to_ascii_uppercase()) {
155 return Some(entry);
156 }
157 self.entries
158 .iter()
159 .find(|(key, _)| key.len() == id.len() && key.eq_ignore_ascii_case(id))
160 .map(|(_, entry)| entry)
161 }
162
163 fn alt_id(id: &str) -> String {
168 match id.get(..3) {
169 Some(prefix) if prefix.eq_ignore_ascii_case("chr") => id[3..].to_string(),
170 _ => format!("chr{id}"),
171 }
172 }
173
174 pub fn get(&self, id: &str) -> Option<&ChrEntry> {
175 if let Some(entry) = self.try_lookup(id) {
176 return Some(entry);
177 }
178 self.try_lookup(&Self::alt_id(id))
179 }
180
181 pub fn contains(&self, id: &str) -> bool {
182 self.get(id).is_some()
183 }
184
185 pub fn resolve(&self, id: &str) -> Result<&ChrEntry> {
187 match self.get(id) {
188 Some(entry) => Ok(entry),
189 None => Err(self.not_found(id)),
190 }
191 }
192
193 fn not_found(&self, id: &str) -> Error {
202 let alt = Self::alt_id(id);
203 let mut key_size = None;
204 let mut truncated_match = None;
205 if let Some(declared) = self.declared_key_size.filter(|&k| k > 0) {
206 if id.chars().count() > declared && alt.chars().count() > declared {
207 key_size = Some(declared);
208 truncated_match = [id, alt.as_str()]
209 .into_iter()
210 .filter_map(|candidate| candidate.get(..declared))
211 .find_map(|prefix| self.try_lookup(prefix))
212 .map(|entry| entry.id.clone());
213 }
214 }
215 Error::UnknownChromosome {
216 id: id.to_string(),
217 key_size,
218 truncated_match,
219 available: self.entries.keys().cloned().collect(),
220 }
221 }
222
223 pub fn select(&self, requested: &[String]) -> Result<Vec<ChrEntry>> {
227 if requested.is_empty() {
228 return Ok(self.entries.values().cloned().collect());
229 }
230 requested
231 .iter()
232 .map(|id| self.resolve(id).cloned())
233 .collect()
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn map(names: &[(&str, i64)]) -> ChrMap {
242 ChrMap::from_entries(names.iter().map(|(n, s)| (n.to_string(), *s)))
243 }
244
245 #[test]
246 fn resolves_exact_case_and_prefix_toggle() {
247 let m = map(&[("chr1", 100), ("chr2", 200)]);
248 for asked in ["chr1", "CHR1", "Chr1", "1"] {
249 assert_eq!(m.get(asked).unwrap().id, "chr1", "asking for {asked}");
250 }
251 let m = map(&[("1", 100), ("2", 200)]);
252 for asked in ["1", "chr1", "CHR1"] {
253 assert_eq!(m.get(asked).unwrap().id, "1", "asking for {asked}");
254 }
255 }
256
257 #[test]
258 fn mixed_case_id_reaches_a_uniform_case_key() {
259 let m = map(&[("chrX", 10)]);
261 assert_eq!(m.get("ChrX").unwrap().id, "chrX");
262 assert_eq!(m.get("X").unwrap().id, "chrX");
263 }
264
265 #[test]
266 fn the_uppercase_form_is_tried_before_the_walk() {
267 let m = map(&[("chrX", 1), ("CHRX", 2)]);
270 assert_eq!(m.get("ChrX").unwrap().id, "CHRX");
271 }
272
273 #[test]
274 fn ties_in_the_walk_go_to_file_order() {
275 let m = map(&[("chrX", 1), ("cHRx", 2)]);
278 assert_eq!(m.get("ChrX").unwrap().id, "chrX");
279 let m = map(&[("cHRx", 1), ("chrX", 2)]);
280 assert_eq!(m.get("ChrX").unwrap().id, "cHRx");
281 }
282
283 #[test]
284 fn ids_are_compared_whole() {
285 let m = map(&[("chr1", 100)]);
287 assert!(m.get("chr12").is_none());
288 assert!(m.get("12").is_none());
289 let m = map(&[("chrXVI", 100)]);
290 assert!(m.get("chrXVII").is_none());
291 }
292
293 #[test]
294 fn not_found_message_lists_what_is_available() {
295 let m = map(&[("chr1", 1), ("chr2", 2)]);
296 let message = m.resolve("chrZ").unwrap_err().to_string();
297 assert!(message.contains("not found"), "{message}");
298 assert!(message.contains("(available: chr1, chr2)"), "{message}");
299 assert!(!message.contains("field"), "{message}");
300 }
301
302 #[test]
303 fn an_over_long_id_is_told_it_cannot_fit() {
304 let m = map(&[("chr1_GL456210_random", 1)]).with_key_size(20);
305 let message = m
308 .resolve("chrX_GL456210_random_ext")
309 .unwrap_err()
310 .to_string();
311 assert!(message.contains("20-character field"), "{message}");
312 assert!(message.contains("24 characters long"), "{message}");
313 }
314
315 #[test]
316 fn an_over_long_id_names_the_chromosome_its_prefix_spells() {
317 let m = map(&[("chr1_GL456210_random", 1)]).with_key_size(20);
318 let message = m
321 .resolve("chr1_GL456210_random_v234")
322 .unwrap_err()
323 .to_string();
324 assert!(
325 message.contains("does hold chr1_GL456210_random"),
326 "{message}"
327 );
328 }
329
330 #[test]
331 fn an_id_that_fits_once_stripped_gets_no_field_clause() {
332 let m = map(&[("chr1_GL456210_random", 1)]).with_key_size(20);
335 let message = m
336 .resolve("chrX_GL456210_random_ab")
337 .unwrap_err()
338 .to_string();
339 assert!(!message.contains("field"), "{message}");
340 assert!(message.contains("not found"), "{message}");
341 }
342
343 #[test]
344 fn select_takes_request_order_and_defaults_to_file_order() {
345 let m = map(&[("chr1", 1), ("chr2", 2), ("chr3", 3)]);
346 let all: Vec<_> = m
347 .select(&[])
348 .unwrap()
349 .iter()
350 .map(|e| e.id.clone())
351 .collect();
352 assert_eq!(all, ["chr1", "chr2", "chr3"]);
353 let some: Vec<_> = m
354 .select(&["chr3".into(), "1".into()])
355 .unwrap()
356 .iter()
357 .map(|e| e.id.clone())
358 .collect();
359 assert_eq!(some, ["chr3", "chr1"]);
360 }
361
362 #[test]
363 fn index_follows_insertion_order() {
364 let m = map(&[("chr1", 1), ("chr2", 2)]);
365 assert_eq!(m.get("chr2").unwrap().index, 1);
366 assert_eq!(m.by_index(0).unwrap().id, "chr1");
367 assert_eq!(m.genome_size(), 3);
368 }
369
370 #[test]
371 fn file_indices_are_kept_and_sorted_by() {
372 let m = ChrMap::from_indexed_entries([
375 ("chrB".to_string(), 20, 7),
376 ("chrA".to_string(), 10, 3),
377 ]);
378 assert_eq!(m.names(), ["chrA", "chrB"]);
379 assert_eq!(m.get("chrA").unwrap().index, 3);
380 assert_eq!(m.by_index(3).unwrap().id, "chrA");
381 assert_eq!(m.by_index(7).unwrap().id, "chrB");
382 assert!(m.by_index(0).is_none());
383 }
384}