pub fn is_identifier(text: &str) -> bool {
if text.chars().any(|c| c.is_whitespace()) {
return false;
}
if !text.chars().any(|c| c.is_ascii_alphabetic()) {
return false;
}
let chars: Vec<char> = text.chars().collect();
let n = chars.len();
if n < 2 {
return false;
}
for &c in &chars {
if matches!(c, '_' | '-' | '/' | '.' | ':') {
return true;
}
}
for i in 0..n - 1 {
let a = chars[i];
let b = chars[i + 1];
if (a.is_ascii_lowercase() && b.is_ascii_uppercase())
|| (a.is_ascii_alphabetic() && b.is_ascii_digit())
|| (a.is_ascii_digit() && b.is_ascii_alphabetic())
{
return true;
}
}
false
}
pub fn split_identifier(text: &str, min_part_len: usize) -> Vec<String> {
let chars: Vec<char> = text.chars().collect();
let mut parts: Vec<String> = Vec::new();
let mut current = String::new();
let n = chars.len();
let mut i = 0;
while i < n {
let c = chars[i];
if matches!(c, '_' | '-' | '/' | '.') {
if !current.is_empty() {
parts.push(current.clone());
current.clear();
}
i += 1;
continue;
}
if c == ':' && i + 1 < n && chars[i + 1] == ':' {
if !current.is_empty() {
parts.push(current.clone());
current.clear();
}
i += 2;
continue;
}
if c == ':' {
if !current.is_empty() {
parts.push(current.clone());
current.clear();
}
i += 1;
continue;
}
if !current.is_empty() && c.is_ascii_uppercase() {
let prev = chars[i - 1];
if prev.is_ascii_lowercase() {
parts.push(current.clone());
current.clear();
} else if prev.is_ascii_uppercase() && i + 1 < n && chars[i + 1].is_ascii_lowercase() {
parts.push(current.clone());
current.clear();
}
}
if !current.is_empty() {
let prev = chars[i - 1];
if (prev.is_ascii_digit() && c.is_ascii_alphabetic())
|| (prev.is_ascii_alphabetic() && c.is_ascii_digit())
{
parts.push(current.clone());
current.clear();
}
}
current.push(c);
i += 1;
}
if !current.is_empty() {
parts.push(current);
}
let min_part_len = min_part_len.max(1);
parts
.into_iter()
.map(|p| p.to_lowercase())
.filter(|p| p.chars().count() >= min_part_len)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn camel_case_is_identifier() {
assert!(is_identifier("camelCase"));
assert!(is_identifier("myVariableName"));
}
#[test]
fn snake_case_is_identifier() {
assert!(is_identifier("snake_case"));
assert!(is_identifier("MY_CONST"));
}
#[test]
fn hyphenated_is_identifier() {
assert!(is_identifier("kebab-case"));
assert!(is_identifier("GPT-4"));
}
#[test]
fn path_separators_are_identifier() {
assert!(is_identifier("a/b"));
assert!(is_identifier("pkg::Type"));
}
#[test]
fn plain_word_is_not_identifier() {
assert!(!is_identifier("plain"));
}
#[test]
fn has_whitespace_is_not_identifier() {
assert!(!is_identifier("hello world"));
assert!(!is_identifier("foo bar"));
}
#[test]
fn no_ascii_letter_not_identifier() {
assert!(!is_identifier("123"));
assert!(!is_identifier("_"));
}
#[test]
fn camel_case_split() {
assert_eq!(split_identifier("camelCase", 1), vec!["camel", "case"]);
}
#[test]
fn snake_case_split() {
assert_eq!(split_identifier("snake_case", 1), vec!["snake", "case"]);
}
#[test]
fn gpt4_split() {
assert_eq!(split_identifier("GPT-4", 1), vec!["gpt", "4"]);
}
#[test]
fn lora_split() {
assert_eq!(split_identifier("LoRA", 1), vec!["lo", "ra"]);
}
#[test]
fn plain_single_part() {
assert_eq!(split_identifier("plain", 1), vec!["plain"]);
}
#[test]
fn bm25f_split() {
let parts = split_identifier("BM25F", 1);
assert_eq!(parts, vec!["bm", "25", "f"]);
}
#[test]
fn double_colon_separator() {
assert_eq!(split_identifier("pkg::Type", 1), vec!["pkg", "type"]);
}
#[test]
fn dot_separator() {
assert_eq!(
split_identifier("com.example.Foo", 1),
vec!["com", "example", "foo"]
);
}
#[test]
fn slash_separator() {
assert_eq!(split_identifier("a/b/c", 1), vec!["a", "b", "c"]);
}
#[test]
fn min_part_len_filters_short() {
let parts = split_identifier("GPT-4", 2);
assert_eq!(parts, vec!["gpt"]);
}
#[test]
fn split_identifier_filters_unicode_min_part_len_by_chars() {
let parts = split_identifier("foo_\u{4F60}", 2);
assert_eq!(parts, vec!["foo"]);
let parts = split_identifier("foo_\u{4F60}\u{597D}", 2);
assert_eq!(parts, vec!["foo", "\u{4F60}\u{597D}"]);
}
#[test]
fn mixed_case_and_digits() {
let parts = split_identifier("v2Api", 2);
assert_eq!(parts, vec!["api"]);
}
#[test]
fn all_uppercase_run_then_lower() {
let parts = split_identifier("XMLParser", 1);
assert_eq!(parts, vec!["xml", "parser"]);
}
}