1use std::collections::HashMap;
14
15use crate::storage::ACRONYM;
16
17fn split_full_id(full: &str) -> (&str, Option<&str>) {
20 let without_prefix = full
21 .strip_prefix(&format!("{ACRONYM}-"))
22 .or_else(|| full.strip_prefix(&format!("{}-", ACRONYM.to_lowercase())))
23 .unwrap_or(full);
24 match without_prefix.split_once('-') {
25 Some((counter, suffix)) => (counter, Some(suffix)),
26 None => (without_prefix, None),
27 }
28}
29
30fn strip_leading_zeros(hex: &str) -> String {
32 let trimmed = hex.trim_start_matches('0');
33 if trimmed.is_empty() {
34 "0".to_string()
35 } else {
36 trimmed.to_string()
37 }
38}
39
40pub fn short_id(full: &str) -> String {
43 let (counter, _) = split_full_id(full);
44 format!("#{}", strip_leading_zeros(counter))
45}
46
47pub fn format_ids(full_ids: &[&str]) -> Vec<String> {
51 let mut counter_frequency: HashMap<String, usize> = HashMap::new();
52 let parsed: Vec<(String, Option<String>)> = full_ids
53 .iter()
54 .map(|id| {
55 let (c, s) = split_full_id(id);
56 (c.to_uppercase(), s.map(|s| s.to_uppercase()))
57 })
58 .collect();
59
60 for (counter, _) in &parsed {
61 *counter_frequency.entry(counter.clone()).or_insert(0) += 1;
62 }
63
64 parsed
65 .into_iter()
66 .map(|(counter, suffix)| {
67 let short_counter = strip_leading_zeros(&counter);
68 let ambiguous = counter_frequency.get(&counter).copied().unwrap_or(1) > 1;
69 match (ambiguous, suffix) {
70 (true, Some(sfx)) => format!("#{short_counter}-{sfx}"),
71 _ => format!("#{short_counter}"),
72 }
73 })
74 .collect()
75}
76
77pub fn normalize_id_input(raw: &str) -> String {
85 let trimmed = raw.trim().trim_start_matches('#');
86
87 if trimmed.to_uppercase().starts_with(&format!("{ACRONYM}-")) {
88 return trimmed.to_uppercase();
89 }
90
91 let (counter, suffix) = match trimmed.split_once('-') {
92 Some((c, s)) => (c, Some(s)),
93 None => (trimmed, None),
94 };
95
96 let counter_valid =
97 !counter.is_empty() && counter.len() <= 4 && counter.chars().all(|c| c.is_ascii_hexdigit());
98 if !counter_valid {
99 return raw.to_string();
100 }
101
102 let padded = format!("{:0>4}", counter.to_uppercase());
103 match suffix {
104 Some(sfx) if sfx.len() == 2 && sfx.chars().all(|c| c.is_ascii_hexdigit()) => {
105 format!("{ACRONYM}-{padded}-{}", sfx.to_uppercase())
106 }
107 _ => format!("{ACRONYM}-{padded}"),
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn short_id_strips_prefix_suffix_and_leading_zeros() {
117 assert_eq!(short_id("TODO-00A1-EA"), "#A1");
118 assert_eq!(short_id("TODO-0001-7F"), "#1");
119 assert_eq!(short_id("TODO-0110-B3"), "#110");
120 assert_eq!(short_id("TODO-FFFF-00"), "#FFFF");
121 }
122
123 #[test]
124 fn short_id_without_suffix_still_works() {
125 assert_eq!(short_id("TODO-0042"), "#42");
126 }
127
128 #[test]
129 fn format_ids_unique_counters_stay_short() {
130 let ids = vec!["TODO-0001-7F", "TODO-00A1-EA", "TODO-0110-B3"];
131 let out = format_ids(&ids);
132 assert_eq!(out, vec!["#1", "#A1", "#110"]);
133 }
134
135 #[test]
136 fn format_ids_expands_only_colliding_rows() {
137 let ids = vec![
138 "TODO-0001-7F",
139 "TODO-00A1-EA",
140 "TODO-00A1-7F",
141 "TODO-0110-B3",
142 ];
143 let out = format_ids(&ids);
144 assert_eq!(out, vec!["#1", "#A1-EA", "#A1-7F", "#110"]);
145 }
146
147 #[test]
148 fn normalize_accepts_short_form() {
149 assert_eq!(normalize_id_input("#A1"), "TODO-00A1");
150 assert_eq!(normalize_id_input("A1"), "TODO-00A1");
151 assert_eq!(normalize_id_input("a1"), "TODO-00A1");
152 assert_eq!(normalize_id_input("1"), "TODO-0001");
153 assert_eq!(normalize_id_input("110"), "TODO-0110");
154 assert_eq!(normalize_id_input("FFFF"), "TODO-FFFF");
155 }
156
157 #[test]
158 fn normalize_accepts_short_form_with_suffix() {
159 assert_eq!(normalize_id_input("#A1-EA"), "TODO-00A1-EA");
160 assert_eq!(normalize_id_input("a1-ea"), "TODO-00A1-EA");
161 }
162
163 #[test]
164 fn normalize_passes_through_full_form() {
165 assert_eq!(normalize_id_input("TODO-00A1"), "TODO-00A1");
166 assert_eq!(normalize_id_input("todo-00a1-ea"), "TODO-00A1-EA");
167 }
168
169 #[test]
170 fn normalize_passes_through_nonsense() {
171 assert_eq!(normalize_id_input("GGGGG"), "GGGGG");
174 assert_eq!(normalize_id_input("12345"), "12345");
175 }
176}