obadh_engine 0.5.1

A linguistically accurate Roman to Bengali transliteration engine
Documentation
//! Definitions for Bengali conjuncts
//!
//! This module provides a comprehensive set of Bengali conjunct definitions
//! based on phonetic components. The engine uses this compiled Rust rule table
//! directly; source CSV data is not parsed or shipped on the runtime path.

mod canonical;
mod rules;
mod trie;

use crate::definitions::consonant_value;
use canonical::{
    canonical_conjunct_part, is_derived_aspirated_ya_phola, is_derived_aspirated_ya_phola_prefix,
    is_special_form_key,
};
use rules::CONJUNCT_RULES;
use trie::ConjunctTrie;

use std::collections::BTreeSet;
use std::sync::OnceLock;

/// Structure to store and manage Bengali conjunct definitions.
#[derive(Debug)]
pub struct ConjunctDefinitions {
    /// Trie for allocation-free conjunct lookup from component parts.
    conjunct_trie: ConjunctTrie,
}

impl ConjunctDefinitions {
    /// Create a new instance of conjunct definitions
    pub fn new() -> Self {
        let conjunct_trie = ConjunctTrie::with_capacity(conjunct_trie_node_capacity());

        let mut instance = ConjunctDefinitions { conjunct_trie };

        for rule in CONJUNCT_RULES {
            instance.add_conjunct(rule.key(), rule.value());
        }

        instance.conjunct_trie.sort_edges();

        instance
    }

    /// Add a conjunct mapping
    fn add_conjunct(&mut self, key: &'static str, value: &'static str) {
        self.conjunct_trie.insert(key, value);
    }

    /// Check if a sequence can form a valid conjunct
    pub fn can_form_conjunct(&self, key: &str) -> bool {
        self.create_conjunct(key).is_some()
    }

    /// Create a conjunct from a sequence of consonants
    pub fn create_conjunct(&self, key: &str) -> Option<&'static str> {
        let node = self.conjunct_trie.advance(self.conjunct_trie.root(), key)?;
        self.conjunct_trie.value(node)
    }

    /// Create a conjunct from already-tokenized component parts without
    /// allocating a joined key.
    pub fn create_conjunct_from_parts(&self, parts: &[&str]) -> Option<&'static str> {
        if parts.len() < 2 {
            return None;
        }

        let mut node = self.conjunct_trie.root();
        for part in parts {
            node = self
                .conjunct_trie
                .advance(node, canonical_conjunct_part(part))?;
        }

        self.conjunct_trie.value(node)
    }

    /// Check component parts without allocating a joined key.
    pub fn can_form_conjunct_from_parts(&self, parts: &[&str]) -> bool {
        self.create_conjunct_from_parts(parts).is_some() || is_derived_aspirated_ya_phola(parts)
    }

    /// Check derived conjunct forms that are intentionally rule-generated
    /// instead of listed in the static source-owned conjunct table.
    pub(crate) fn can_form_derived_conjunct_from_parts(&self, parts: &[&str]) -> bool {
        is_derived_aspirated_ya_phola(parts)
    }

    /// Check whether the current parts can still become a derived conjunct if
    /// the tokenizer consumes one more component.
    pub(crate) fn can_match_derived_conjunct_prefix(&self, parts: &[&str]) -> bool {
        is_derived_aspirated_ya_phola_prefix(parts)
    }

    /// Return the root trie cursor for incremental conjunct matching.
    pub(crate) fn conjunct_match_root(&self) -> usize {
        self.conjunct_trie.root()
    }

    /// Advance an incremental conjunct match by one romanized component.
    pub(crate) fn advance_conjunct_match(&self, node: usize, part: &str) -> Option<usize> {
        self.conjunct_trie
            .advance(node, canonical_conjunct_part(part))
    }

    /// Return the conjunct value at an incremental trie cursor, if terminal.
    pub(crate) fn conjunct_match_value(&self, node: usize) -> Option<&'static str> {
        self.conjunct_trie.value(node)
    }

    /// Get romanized consonants for a conjunct
    pub fn get_components(&self, conjunct: &str) -> Option<Vec<String>> {
        for rule in CONJUNCT_RULES {
            if rule.value() == conjunct {
                return Some(self.components_for_key(rule.key()));
            }
        }
        None
    }

    fn components_for_key(&self, key: &str) -> Vec<String> {
        let mut components = Vec::new();
        let mut i = 0;
        while i < key.len() {
            let mut found = false;
            for len in (1..=key.len() - i).rev() {
                let substr = &key[i..i + len];
                if consonant_value(substr).is_some() || is_special_form_key(substr) {
                    components.push(substr.to_string());
                    i += len;
                    found = true;
                    break;
                }
            }
            if !found {
                components.push(key[i..i + 1].to_string());
                i += 1;
            }
        }
        components
    }

    /// Check if a given sequence is a valid conjunct
    pub fn is_valid_conjunct(&self, components: &[String]) -> bool {
        if components.is_empty() {
            return false;
        }

        let mut node = self.conjunct_trie.root();
        for component in components {
            let Some(next_node) = self
                .conjunct_trie
                .advance(node, canonical_conjunct_part(component))
            else {
                return false;
            };
            node = next_node;
        }

        self.conjunct_trie.value(node).is_some()
    }

    /// Get all valid conjuncts
    pub fn get_all_valid_conjuncts(&self) -> BTreeSet<&'static str> {
        CONJUNCT_RULES.iter().map(|rule| rule.key()).collect()
    }

    /// Check if a form is a special form (like reph, ya-phola, ba-phola)
    pub fn is_special_form(&self, form: &str) -> bool {
        is_special_form_key(form)
    }
}

impl Default for ConjunctDefinitions {
    fn default() -> Self {
        Self::new()
    }
}

/// Return a singleton instance of ConjunctDefinitions
pub fn conjuncts() -> &'static ConjunctDefinitions {
    static INSTANCE: OnceLock<ConjunctDefinitions> = OnceLock::new();
    INSTANCE.get_or_init(ConjunctDefinitions::new)
}

fn conjunct_trie_node_capacity() -> usize {
    1 + CONJUNCT_RULES
        .iter()
        .map(|rule| rule.key().len())
        .sum::<usize>()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn conjunct_rule_table_has_unique_keys() {
        let mut keys = BTreeSet::new();

        for rule in CONJUNCT_RULES {
            assert!(!rule.key().is_empty());
            assert!(!rule.value().is_empty());
            assert!(
                keys.insert(rule.key()),
                "duplicate conjunct rule key: {}",
                rule.key()
            );
        }
    }

    #[test]
    fn conjunct_definitions_load_every_static_rule() {
        let definitions = ConjunctDefinitions::new();

        assert_eq!(
            definitions.get_all_valid_conjuncts().len(),
            CONJUNCT_RULES.len()
        );

        for rule in CONJUNCT_RULES {
            assert_eq!(definitions.create_conjunct(rule.key()), Some(rule.value()));
        }
    }
}