1pub fn is_identifier(text: &str) -> bool {
5 if text.chars().any(|c| c.is_whitespace()) {
6 return false;
7 }
8 if !text.chars().any(|c| c.is_ascii_alphabetic()) {
9 return false;
10 }
11
12 let chars: Vec<char> = text.chars().collect();
13 let n = chars.len();
14
15 if n < 2 {
17 return false;
18 }
19
20 for &c in &chars {
22 if matches!(c, '_' | '-' | '/' | '.' | ':') {
23 return true;
24 }
25 }
26
27 for i in 0..n - 1 {
29 let a = chars[i];
30 let b = chars[i + 1];
31 if (a.is_ascii_lowercase() && b.is_ascii_uppercase())
32 || (a.is_ascii_alphabetic() && b.is_ascii_digit())
33 || (a.is_ascii_digit() && b.is_ascii_alphabetic())
34 {
35 return true;
36 }
37 }
38
39 false
40}
41
42pub fn split_identifier(text: &str, min_part_len: usize) -> Vec<String> {
45 let chars: Vec<char> = text.chars().collect();
47 let mut parts: Vec<String> = Vec::new();
48 let mut current = String::new();
49
50 let n = chars.len();
51 let mut i = 0;
52 while i < n {
53 let c = chars[i];
54
55 if matches!(c, '_' | '-' | '/' | '.') {
57 if !current.is_empty() {
58 parts.push(current.clone());
59 current.clear();
60 }
61 i += 1;
62 continue;
63 }
64
65 if c == ':' && i + 1 < n && chars[i + 1] == ':' {
67 if !current.is_empty() {
68 parts.push(current.clone());
69 current.clear();
70 }
71 i += 2;
72 continue;
73 }
74 if c == ':' {
76 if !current.is_empty() {
77 parts.push(current.clone());
78 current.clear();
79 }
80 i += 1;
81 continue;
82 }
83
84 if !current.is_empty() && c.is_ascii_uppercase() {
86 let prev = chars[i - 1];
87 if prev.is_ascii_lowercase() {
88 parts.push(current.clone());
90 current.clear();
91 } else if prev.is_ascii_uppercase() && i + 1 < n && chars[i + 1].is_ascii_lowercase() {
92 parts.push(current.clone());
94 current.clear();
95 }
96 }
97
98 if !current.is_empty() {
100 let prev = chars[i - 1];
101 if (prev.is_ascii_digit() && c.is_ascii_alphabetic())
102 || (prev.is_ascii_alphabetic() && c.is_ascii_digit())
103 {
104 parts.push(current.clone());
105 current.clear();
106 }
107 }
108
109 current.push(c);
110 i += 1;
111 }
112
113 if !current.is_empty() {
114 parts.push(current);
115 }
116
117 parts
119 .into_iter()
120 .map(|p| p.to_lowercase())
121 .filter(|p| p.len() >= min_part_len.max(1))
122 .collect()
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
132 fn camel_case_is_identifier() {
133 assert!(is_identifier("camelCase"));
134 assert!(is_identifier("myVariableName"));
135 }
136
137 #[test]
138 fn snake_case_is_identifier() {
139 assert!(is_identifier("snake_case"));
140 assert!(is_identifier("MY_CONST"));
141 }
142
143 #[test]
144 fn hyphenated_is_identifier() {
145 assert!(is_identifier("kebab-case"));
146 assert!(is_identifier("GPT-4"));
147 }
148
149 #[test]
150 fn path_separators_are_identifier() {
151 assert!(is_identifier("a/b"));
152 assert!(is_identifier("pkg::Type"));
153 }
154
155 #[test]
156 fn plain_word_is_not_identifier() {
157 assert!(!is_identifier("plain"));
159 }
160
161 #[test]
162 fn has_whitespace_is_not_identifier() {
163 assert!(!is_identifier("hello world"));
164 assert!(!is_identifier("foo bar"));
165 }
166
167 #[test]
168 fn no_ascii_letter_not_identifier() {
169 assert!(!is_identifier("123"));
170 assert!(!is_identifier("_"));
171 }
172
173 #[test]
176 fn camel_case_split() {
177 assert_eq!(split_identifier("camelCase", 1), vec!["camel", "case"]);
178 }
179
180 #[test]
181 fn snake_case_split() {
182 assert_eq!(split_identifier("snake_case", 1), vec!["snake", "case"]);
183 }
184
185 #[test]
186 fn gpt4_split() {
187 assert_eq!(split_identifier("GPT-4", 1), vec!["gpt", "4"]);
188 }
189
190 #[test]
191 fn lora_split() {
192 assert_eq!(split_identifier("LoRA", 1), vec!["lo", "ra"]);
193 }
194
195 #[test]
196 fn plain_single_part() {
197 assert_eq!(split_identifier("plain", 1), vec!["plain"]);
198 }
199
200 #[test]
201 fn bm25f_split() {
202 let parts = split_identifier("BM25F", 1);
204 assert_eq!(parts, vec!["bm", "25", "f"]);
205 }
206
207 #[test]
208 fn double_colon_separator() {
209 assert_eq!(split_identifier("pkg::Type", 1), vec!["pkg", "type"]);
210 }
211
212 #[test]
213 fn dot_separator() {
214 assert_eq!(
215 split_identifier("com.example.Foo", 1),
216 vec!["com", "example", "foo"]
217 );
218 }
219
220 #[test]
221 fn slash_separator() {
222 assert_eq!(split_identifier("a/b/c", 1), vec!["a", "b", "c"]);
223 }
224
225 #[test]
226 fn min_part_len_filters_short() {
227 let parts = split_identifier("GPT-4", 2);
229 assert_eq!(parts, vec!["gpt"]);
230 }
231
232 #[test]
233 fn mixed_case_and_digits() {
234 let parts = split_identifier("v2Api", 2);
236 assert_eq!(parts, vec!["api"]);
237 }
238
239 #[test]
240 fn all_uppercase_run_then_lower() {
241 let parts = split_identifier("XMLParser", 1);
244 assert_eq!(parts, vec!["xml", "parser"]);
245 }
246}