use std::collections::HashMap;
pub fn negate_char_class(chars: &[char]) -> Vec<char> {
(0x20u8..=0x7Eu8)
.map(|b| b as char)
.filter(|c| !chars.contains(c))
.collect()
}
pub fn is_user_symbol_name(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(first) if first.is_uppercase() => {
chars.all(|c| c.is_uppercase() || !c.is_alphabetic())
}
_ => false,
}
}
pub type SymbolTable = HashMap<String, Vec<char>>;
pub fn resolve_feature_bundle_chars<F, N, I>(
terms: &[(String, bool)],
symbols: Option<&SymbolTable>,
get_builtin: F,
negate_set: N,
intersect_sets: I,
) -> Result<Vec<char>, String>
where
F: Fn(&str) -> Option<Vec<char>>,
N: Fn(&[char]) -> Vec<char>,
I: Fn(&[Vec<char>]) -> Vec<char>,
{
let mut char_sets: Vec<Vec<char>> = Vec::new();
for (name, negated) in terms {
let chars = if let Some(symbol_table) = symbols {
if let Some(symbol_chars) = symbol_table.get(name) {
symbol_chars.clone()
} else if let Some(builtin_chars) = get_builtin(name) {
builtin_chars
} else {
return Err(format!("unknown class or symbol '{}'", name));
}
} else if let Some(builtin_chars) = get_builtin(name) {
builtin_chars
} else if is_user_symbol_name(name) {
return Err(format!("undefined symbol '{}'", name));
} else {
return Err(format!("unknown named class '{}'", name));
};
let final_chars = if *negated { negate_set(&chars) } else { chars };
char_sets.push(final_chars);
}
Ok(intersect_sets(&char_sets))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_negate_char_class() {
let vowels = vec!['a', 'e', 'i', 'o', 'u'];
let non_vowels = negate_char_class(&vowels);
assert!(!non_vowels.contains(&'a'));
assert!(!non_vowels.contains(&'e'));
assert!(!non_vowels.contains(&'i'));
assert!(!non_vowels.contains(&'o'));
assert!(!non_vowels.contains(&'u'));
assert!(non_vowels.contains(&'b'));
assert!(non_vowels.contains(&'c'));
assert!(non_vowels.contains(&'z'));
assert!(non_vowels.contains(&' '));
assert!(non_vowels.contains(&'!'));
assert!(non_vowels.contains(&'~'));
}
#[test]
fn test_negate_char_class_empty() {
let empty: Vec<char> = vec![];
let result = negate_char_class(&empty);
assert_eq!(result.len(), 95);
}
#[test]
fn test_is_user_symbol_name_valid() {
assert!(is_user_symbol_name("VOWEL"));
assert!(is_user_symbol_name("FRONT_VOWEL"));
assert!(is_user_symbol_name("V1"));
assert!(is_user_symbol_name("A"));
assert!(is_user_symbol_name("ABC123"));
}
#[test]
fn test_is_user_symbol_name_invalid() {
assert!(!is_user_symbol_name("vowel"));
assert!(!is_user_symbol_name("Vowel"));
assert!(!is_user_symbol_name("_FOO"));
assert!(!is_user_symbol_name("123"));
assert!(!is_user_symbol_name(""));
assert!(!is_user_symbol_name("foo_BAR"));
}
#[test]
fn test_resolve_feature_bundle_chars_builtin_only() {
let terms = vec![("vowel".to_string(), false)];
let result = resolve_feature_bundle_chars(
&terms,
None,
|name| {
if name == "vowel" {
Some(vec!['a', 'e', 'i', 'o', 'u'])
} else {
None
}
},
|chars| negate_char_class(chars),
|sets| {
if sets.is_empty() {
vec![]
} else {
sets[0].clone()
}
},
);
assert!(result.is_ok());
let chars = result.expect("test fixture: resolve must be Ok");
assert!(chars.contains(&'a'));
assert!(chars.contains(&'e'));
}
#[test]
fn test_resolve_feature_bundle_chars_negated() {
let terms = vec![("vowel".to_string(), true)];
let result = resolve_feature_bundle_chars(
&terms,
None,
|name| {
if name == "vowel" {
Some(vec!['a', 'e', 'i', 'o', 'u'])
} else {
None
}
},
|chars| negate_char_class(chars),
|sets| {
if sets.is_empty() {
vec![]
} else {
sets[0].clone()
}
},
);
assert!(result.is_ok());
let chars = result.expect("test fixture: resolve must be Ok");
assert!(!chars.contains(&'a'));
assert!(!chars.contains(&'e'));
assert!(chars.contains(&'b'));
}
#[test]
fn test_resolve_feature_bundle_chars_with_symbols() {
let mut symbols = SymbolTable::new();
symbols.insert("MY_CLASS".to_string(), vec!['x', 'y', 'z']);
let terms = vec![("MY_CLASS".to_string(), false)];
let result = resolve_feature_bundle_chars(
&terms,
Some(&symbols),
|_| None,
|chars| negate_char_class(chars),
|sets| {
if sets.is_empty() {
vec![]
} else {
sets[0].clone()
}
},
);
assert!(result.is_ok());
let chars = result.expect("test fixture: resolve must be Ok");
assert!(chars.contains(&'x'));
assert!(chars.contains(&'y'));
assert!(chars.contains(&'z'));
}
#[test]
fn test_resolve_feature_bundle_chars_unknown() {
let terms = vec![("unknown".to_string(), false)];
let result = resolve_feature_bundle_chars(
&terms,
None,
|_| None,
|chars| negate_char_class(chars),
|sets| {
if sets.is_empty() {
vec![]
} else {
sets[0].clone()
}
},
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("unknown named class"));
}
#[test]
fn test_resolve_feature_bundle_chars_undefined_symbol() {
let terms = vec![("UNDEFINED_SYMBOL".to_string(), false)];
let result = resolve_feature_bundle_chars(
&terms,
None,
|_| None,
|chars| negate_char_class(chars),
|sets| {
if sets.is_empty() {
vec![]
} else {
sets[0].clone()
}
},
);
assert!(result.is_err());
assert!(result.unwrap_err().contains("undefined symbol"));
}
}