relay-knowledge 1.1.16

Graph-database-based knowledge graph project.
Documentation
use std::collections::BTreeSet;

pub(crate) fn normalized_terms(text: &str, min_len: usize) -> BTreeSet<String> {
    let mut terms = BTreeSet::new();
    extend_normalized_terms(text, min_len, &mut terms);

    terms
}

pub(crate) fn extend_normalized_terms(text: &str, min_len: usize, terms: &mut BTreeSet<String>) {
    let mut token = String::new();
    for character in text.chars() {
        if character.is_alphanumeric() || character == '_' {
            token.push(character);
        } else {
            insert_identifier_terms(&token, min_len, terms);
            token.clear();
        }
    }
    insert_identifier_terms(&token, min_len, terms);
}

fn insert_identifier_terms(token: &str, min_len: usize, terms: &mut BTreeSet<String>) {
    if token.is_empty() {
        return;
    }
    insert_term(&token.to_lowercase(), min_len, terms);

    let mut parts = Vec::new();
    for chunk in token.split('_').filter(|part| !part.is_empty()) {
        split_identifier_chunk(chunk, &mut parts);
    }
    for part in &parts {
        insert_term(part, min_len, terms);
    }
    if let Some(acronym) = acronym(&parts) {
        insert_term(&acronym, min_len, terms);
    }
}

fn split_identifier_chunk(chunk: &str, parts: &mut Vec<String>) {
    let mut current = String::new();
    let mut previous = CharacterClass::Boundary;
    let characters = chunk.chars().collect::<Vec<_>>();
    for (index, character) in characters.iter().enumerate() {
        let class = CharacterClass::from(*character);
        let next = characters
            .get(index + 1)
            .map(|character| CharacterClass::from(*character))
            .unwrap_or(CharacterClass::Boundary);
        if class == CharacterClass::Boundary {
            push_part(parts, &mut current);
            previous = CharacterClass::Boundary;
            continue;
        }
        if should_split(previous, class, next) {
            push_part(parts, &mut current);
        }
        current.extend(character.to_lowercase());
        previous = class;
    }
    push_part(parts, &mut current);
}

fn push_part(parts: &mut Vec<String>, current: &mut String) {
    if !current.is_empty() {
        parts.push(std::mem::take(current));
    }
}

fn acronym(parts: &[String]) -> Option<String> {
    if parts.len() < 2 {
        return None;
    }
    let acronym = parts
        .iter()
        .filter_map(|part| part.chars().next())
        .collect::<String>();
    (!acronym.is_empty()).then_some(acronym)
}

fn insert_term(term: &str, min_len: usize, terms: &mut BTreeSet<String>) {
    if term.chars().count() >= min_len {
        terms.insert(term.to_owned());
    }
}

fn should_split(previous: CharacterClass, current: CharacterClass, next: CharacterClass) -> bool {
    matches!(
        (previous, current),
        (CharacterClass::Lower, CharacterClass::Upper)
            | (CharacterClass::Digit, CharacterClass::Upper)
            | (CharacterClass::Digit, CharacterClass::Lower)
            | (CharacterClass::Lower, CharacterClass::Digit)
            | (CharacterClass::Upper, CharacterClass::Digit)
    ) || matches!(
        (previous, current, next),
        (
            CharacterClass::Upper,
            CharacterClass::Upper,
            CharacterClass::Lower
        )
    )
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum CharacterClass {
    Upper,
    Lower,
    Digit,
    Boundary,
}

impl From<char> for CharacterClass {
    fn from(value: char) -> Self {
        if value.is_ascii_uppercase() {
            Self::Upper
        } else if value.is_ascii_lowercase() {
            Self::Lower
        } else if value.is_ascii_digit() {
            Self::Digit
        } else if value.is_alphanumeric() {
            Self::Lower
        } else {
            Self::Boundary
        }
    }
}

#[cfg(test)]
#[path = "terms_tests.rs"]
mod tests;