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 let min_part_len = min_part_len.max(1);
119 parts
120 .into_iter()
121 .map(|p| p.to_lowercase())
122 .filter(|p| p.chars().count() >= min_part_len)
123 .collect()
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
133 fn camel_case_is_identifier() {
134 assert!(is_identifier("camelCase"));
135 assert!(is_identifier("myVariableName"));
136 }
137
138 #[test]
139 fn snake_case_is_identifier() {
140 assert!(is_identifier("snake_case"));
141 assert!(is_identifier("MY_CONST"));
142 }
143
144 #[test]
145 fn hyphenated_is_identifier() {
146 assert!(is_identifier("kebab-case"));
147 assert!(is_identifier("GPT-4"));
148 }
149
150 #[test]
151 fn path_separators_are_identifier() {
152 assert!(is_identifier("a/b"));
153 assert!(is_identifier("pkg::Type"));
154 }
155
156 #[test]
157 fn plain_word_is_not_identifier() {
158 assert!(!is_identifier("plain"));
160 }
161
162 #[test]
163 fn has_whitespace_is_not_identifier() {
164 assert!(!is_identifier("hello world"));
165 assert!(!is_identifier("foo bar"));
166 }
167
168 #[test]
169 fn no_ascii_letter_not_identifier() {
170 assert!(!is_identifier("123"));
171 assert!(!is_identifier("_"));
172 }
173
174 #[test]
177 fn camel_case_split() {
178 assert_eq!(split_identifier("camelCase", 1), vec!["camel", "case"]);
179 }
180
181 #[test]
182 fn snake_case_split() {
183 assert_eq!(split_identifier("snake_case", 1), vec!["snake", "case"]);
184 }
185
186 #[test]
187 fn gpt4_split() {
188 assert_eq!(split_identifier("GPT-4", 1), vec!["gpt", "4"]);
189 }
190
191 #[test]
192 fn lora_split() {
193 assert_eq!(split_identifier("LoRA", 1), vec!["lo", "ra"]);
194 }
195
196 #[test]
197 fn plain_single_part() {
198 assert_eq!(split_identifier("plain", 1), vec!["plain"]);
199 }
200
201 #[test]
202 fn bm25f_split() {
203 let parts = split_identifier("BM25F", 1);
205 assert_eq!(parts, vec!["bm", "25", "f"]);
206 }
207
208 #[test]
209 fn double_colon_separator() {
210 assert_eq!(split_identifier("pkg::Type", 1), vec!["pkg", "type"]);
211 }
212
213 #[test]
214 fn dot_separator() {
215 assert_eq!(
216 split_identifier("com.example.Foo", 1),
217 vec!["com", "example", "foo"]
218 );
219 }
220
221 #[test]
222 fn slash_separator() {
223 assert_eq!(split_identifier("a/b/c", 1), vec!["a", "b", "c"]);
224 }
225
226 #[test]
227 fn min_part_len_filters_short() {
228 let parts = split_identifier("GPT-4", 2);
230 assert_eq!(parts, vec!["gpt"]);
231 }
232
233 #[test]
234 fn split_identifier_filters_unicode_min_part_len_by_chars() {
235 let parts = split_identifier("foo_\u{4F60}", 2);
238 assert_eq!(parts, vec!["foo"]);
239
240 let parts = split_identifier("foo_\u{4F60}\u{597D}", 2);
242 assert_eq!(parts, vec!["foo", "\u{4F60}\u{597D}"]);
243 }
244
245 #[test]
246 fn mixed_case_and_digits() {
247 let parts = split_identifier("v2Api", 2);
249 assert_eq!(parts, vec!["api"]);
250 }
251
252 #[test]
253 fn all_uppercase_run_then_lower() {
254 let parts = split_identifier("XMLParser", 1);
257 assert_eq!(parts, vec!["xml", "parser"]);
258 }
259}