1use std::collections::HashMap;
6
7use crate::model::Id;
8
9pub fn shortest_unique_prefixes(ids: &[Id]) -> HashMap<Id, String> {
12 let mut sorted: Vec<&Id> = ids.iter().collect();
13 sorted.sort();
14 sorted
15 .iter()
16 .enumerate()
17 .map(|(i, id)| {
18 let len = [i.checked_sub(1), (i + 1 < sorted.len()).then_some(i + 1)]
19 .into_iter()
20 .flatten()
21 .map(|j| common_prefix_len(id.as_str(), sorted[j].as_str()) + 1)
22 .max()
23 .unwrap_or(1)
24 .min(id.as_str().len());
25 ((*id).clone(), id.as_str()[..len].to_string())
26 })
27 .collect()
28}
29
30fn common_prefix_len(a: &str, b: &str) -> usize {
31 a.bytes().zip(b.bytes()).take_while(|(x, y)| x == y).count()
32}
33
34pub enum ResolvedId {
36 Found(Id),
37 NotFound,
38 Ambiguous(Vec<Id>),
39}
40
41pub fn resolve_id_prefix(ids: &[Id], typed: &str) -> ResolvedId {
47 let typed = typed.to_lowercase();
48 if let Some(id) = ids
49 .iter()
50 .find(|id| id.as_str().eq_ignore_ascii_case(&typed))
51 {
52 return ResolvedId::Found(id.clone());
53 }
54 let matches: Vec<Id> = ids
55 .iter()
56 .filter(|id| id.as_str().to_lowercase().starts_with(&typed))
57 .cloned()
58 .collect();
59 match matches.len() {
60 0 => ResolvedId::NotFound,
61 1 => ResolvedId::Found(matches.into_iter().next().unwrap_or_else(Id::root)),
62 _ => ResolvedId::Ambiguous(matches),
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn shortest_unique_prefixes_distinguishes_shared_start() {
72 let ids = vec![Id::new("abc123"), Id::new("abc456"), Id::new("xyz789")];
73 let short = shortest_unique_prefixes(&ids);
74 assert_eq!(short[&Id::new("abc123")], "abc1");
75 assert_eq!(short[&Id::new("abc456")], "abc4");
76 assert_eq!(short[&Id::new("xyz789")], "x");
77 }
78
79 #[test]
80 fn resolve_prefix_exact_match_wins_over_being_a_prefix() {
81 let ids = vec![Id::new("abc"), Id::new("abcdef")];
82 assert!(matches!(
83 resolve_id_prefix(&ids, "abc"),
84 ResolvedId::Found(id) if id == Id::new("abc")
85 ));
86 }
87
88 #[test]
89 fn resolve_prefix_ambiguous_when_multiple_match() {
90 let ids = vec![Id::new("abc123"), Id::new("abc456")];
91 assert!(matches!(
92 resolve_id_prefix(&ids, "abc"),
93 ResolvedId::Ambiguous(m) if m.len() == 2
94 ));
95 }
96
97 #[test]
98 fn resolve_prefix_not_found() {
99 let ids = vec![Id::new("abc123")];
100 assert!(matches!(
101 resolve_id_prefix(&ids, "zzz"),
102 ResolvedId::NotFound
103 ));
104 }
105
106 #[test]
107 fn resolve_prefix_is_case_insensitive() {
108 let ids = vec![Id::new("ABC123")];
111 assert!(matches!(
112 resolve_id_prefix(&ids, "abc"),
113 ResolvedId::Found(id) if id == Id::new("ABC123")
114 ));
115 assert!(matches!(
116 resolve_id_prefix(&ids, "ABC123"),
117 ResolvedId::Found(id) if id == Id::new("ABC123")
118 ));
119 }
120}