#[inline]
pub fn keyword_eq(raw: &str, upper: &str) -> bool {
debug_assert_upper(upper.as_bytes());
raw.eq_ignore_ascii_case(upper)
}
#[inline]
pub fn keyword_starts_with(raw: &str, upper: &str) -> bool {
debug_assert_upper(upper.as_bytes());
raw.len() >= upper.len() && raw.as_bytes()[..upper.len()].eq_ignore_ascii_case(upper.as_bytes())
}
#[inline]
pub fn keyword_ends_with(raw: &str, upper: &str) -> bool {
debug_assert_upper(upper.as_bytes());
raw.len() >= upper.len()
&& raw.as_bytes()[raw.len() - upper.len()..].eq_ignore_ascii_case(upper.as_bytes())
}
pub fn find_keyword(content: &[u8], upper: &[u8]) -> Option<usize> {
debug_assert!(!upper.is_empty(), "find_keyword needs a non-empty needle");
debug_assert_upper(upper);
let anchor = rarest_letter_offset(upper);
let anchor_upper = upper[anchor].to_ascii_uppercase();
let anchor_lower = anchor_upper.to_ascii_lowercase();
let mut search_from = 0usize;
loop {
let rel = memchr::memchr2(anchor_upper, anchor_lower, content.get(search_from..)?)?;
let hit = search_from + rel;
search_from = hit + 1;
let Some(candidate) = hit.checked_sub(anchor) else {
continue;
};
let end = candidate + upper.len();
if end <= content.len() && content[candidate..end].eq_ignore_ascii_case(upper) {
return Some(candidate);
}
}
}
const LETTERS_RAREST_FIRST: &[u8; 26] = b"JZKQWXVHMBGDYUSPLRNOFTACIE";
fn rarest_letter_offset(upper: &[u8]) -> usize {
let rank = |b: u8| LETTERS_RAREST_FIRST.iter().position(|&l| l == b);
upper
.iter()
.enumerate()
.filter_map(|(i, &b)| rank(b).map(|r| (r, i)))
.min()
.map_or(0, |(_, i)| i)
}
#[inline]
fn debug_assert_upper(upper: &[u8]) {
debug_assert!(
!upper.iter().any(u8::is_ascii_lowercase),
"STEP keyword literal must be uppercase: {}",
String::from_utf8_lossy(upper)
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eq_folds_only_the_raw_side() {
assert!(keyword_eq("IFCWALL", "IFCWALL"));
assert!(keyword_eq("IfcWall", "IFCWALL"));
assert!(keyword_eq("ifcwall", "IFCWALL"));
assert!(!keyword_eq("IFCWALLTYPE", "IFCWALL"));
assert!(!keyword_eq("IFCWAL", "IFCWALL"));
}
#[test]
fn prefix_and_suffix_fold_case() {
assert!(keyword_starts_with("IfcPropertySingleValue", "IFCPROPERTY"));
assert!(!keyword_starts_with("IfcProp", "IFCPROPERTY"));
assert!(keyword_ends_with("IfcWallType", "TYPE"));
assert!(keyword_ends_with("ifcdoorstyle", "STYLE"));
assert!(!keyword_ends_with("TYP", "TYPE"));
}
#[test]
fn find_keyword_hits_any_casing_and_misses_absent() {
let content = b"#1=IfcWall('a');\n#2=ifcindexedcolourmap($);\n#3=IFCDOOR('b');\n";
assert_eq!(find_keyword(content, b"IFCINDEXEDCOLOURMAP"), Some(20));
assert_eq!(find_keyword(content, b"IFCWALL"), Some(3));
assert_eq!(find_keyword(content, b"IFCDOOR"), Some(47));
assert_eq!(find_keyword(content, b"IFCWINDOW"), None);
assert_eq!(find_keyword(b"", b"IFCWALL"), None);
}
#[test]
fn find_keyword_handles_anchor_near_the_edges() {
assert_eq!(find_keyword(b"J", b"IFCPROJECT"), None);
assert_eq!(find_keyword(b"IFCPROJ", b"IFCPROJECT"), None);
assert_eq!(find_keyword(b"xxIFCPROJECT", b"IFCPROJECT"), Some(2));
}
#[test]
fn rarest_letter_prefers_j_over_i() {
assert_eq!(rarest_letter_offset(b"IFCPROJECT("), 6);
assert_eq!(rarest_letter_offset(b"123("), 0);
}
}