Skip to main content

trailcache_core/models/
sorting.rs

1//! Requirement sorting utilities shared across all interfaces.
2//!
3//! Sorts requirements numerically by their number field, keeping
4//! sub-requirements (e.g., "a", "b") grouped under their parent.
5
6use super::{RankRequirement, MeritBadgeRequirement};
7
8/// Parse a requirement number into a sortable key.
9/// "3a" -> (3, "a"), "a" -> (0, "a"), "10" -> (10, "")
10pub fn req_number_sort_key(num: &str) -> (u32, String) {
11    let trimmed = num.trim_matches(|c: char| c == '(' || c == ')' || c.is_whitespace());
12    let numeric_end = trimmed.find(|c: char| !c.is_ascii_digit()).unwrap_or(trimmed.len());
13    if numeric_end > 0 {
14        let n: u32 = trimmed[..numeric_end].parse().unwrap_or(u32::MAX);
15        let suffix = trimmed[numeric_end..].to_lowercase();
16        (n, suffix)
17    } else {
18        // Purely alphabetic — sub-requirement, parent assigned during sort
19        (0, trimmed.to_lowercase())
20    }
21}
22
23/// Given requirement number strings, return indices sorted numerically
24/// with sub-requirements grouped under their preceding parent.
25pub fn sorted_indices_by_number(numbers: &[String]) -> Vec<usize> {
26    let keys: Vec<(u32, String)> = numbers.iter().map(|n| req_number_sort_key(n)).collect();
27
28    // Assign parent numbers to purely-alpha sub-requirements
29    let mut parent_map: Vec<u32> = Vec::with_capacity(keys.len());
30    let mut last_parent: u32 = 0;
31    for key in &keys {
32        if key.0 > 0 {
33            last_parent = key.0;
34            parent_map.push(key.0);
35        } else {
36            parent_map.push(last_parent);
37        }
38    }
39
40    let final_keys: Vec<(u32, String)> = keys
41        .iter()
42        .enumerate()
43        .map(|(i, key)| {
44            if key.0 == 0 {
45                (parent_map[i], key.1.clone())
46            } else {
47                key.clone()
48            }
49        })
50        .collect();
51
52    let mut indices: Vec<usize> = (0..numbers.len()).collect();
53    indices.sort_by(|&a, &b| final_keys[a].cmp(&final_keys[b]));
54    indices
55}
56
57/// Trait for types that have a requirement number string.
58pub trait HasRequirementNumber {
59    fn requirement_number_str(&self) -> String;
60}
61
62impl HasRequirementNumber for RankRequirement {
63    fn requirement_number_str(&self) -> String {
64        self.number()
65    }
66}
67
68impl HasRequirementNumber for MeritBadgeRequirement {
69    fn requirement_number_str(&self) -> String {
70        self.number()
71    }
72}
73
74/// Sort a Vec of any type with a requirement number, keeping
75/// sub-requirements grouped under their parent.
76pub fn sort_requirements<T: HasRequirementNumber + Clone>(reqs: &mut [T]) {
77    let numbers: Vec<String> = reqs.iter().map(|r| r.requirement_number_str()).collect();
78    let order = sorted_indices_by_number(&numbers);
79    let orig = reqs.to_vec();
80    for (i, &idx) in order.iter().enumerate() {
81        reqs[i] = orig[idx].clone();
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_req_number_sort_key() {
91        assert_eq!(req_number_sort_key("1"), (1, String::new()));
92        assert_eq!(req_number_sort_key("3a"), (3, "a".to_string()));
93        assert_eq!(req_number_sort_key("10"), (10, String::new()));
94        assert_eq!(req_number_sort_key("a"), (0, "a".to_string()));
95        assert_eq!(req_number_sort_key("(b)"), (0, "b".to_string()));
96    }
97
98    #[test]
99    fn test_sorted_indices_numeric() {
100        let nums: Vec<String> = vec!["3", "1", "2", "10"]
101            .into_iter()
102            .map(String::from)
103            .collect();
104        let order = sorted_indices_by_number(&nums);
105        let sorted: Vec<&str> = order.iter().map(|&i| nums[i].as_str()).collect();
106        assert_eq!(sorted, vec!["1", "2", "3", "10"]);
107    }
108
109    #[test]
110    fn test_sorted_indices_with_subreqs() {
111        let nums: Vec<String> = vec!["1", "a", "b", "2", "a", "3"]
112            .into_iter()
113            .map(String::from)
114            .collect();
115        let order = sorted_indices_by_number(&nums);
116        let sorted: Vec<&str> = order.iter().map(|&i| nums[i].as_str()).collect();
117        assert_eq!(sorted, vec!["1", "a", "b", "2", "a", "3"]);
118    }
119
120    #[test]
121    fn test_sorted_indices_mixed() {
122        let nums: Vec<String> = vec!["2", "1", "1a", "1b", "3", "2a"]
123            .into_iter()
124            .map(String::from)
125            .collect();
126        let order = sorted_indices_by_number(&nums);
127        let sorted: Vec<&str> = order.iter().map(|&i| nums[i].as_str()).collect();
128        assert_eq!(sorted, vec!["1", "1a", "1b", "2", "2a", "3"]);
129    }
130}