runmunch 0.1.0

A Rust implementation of hunspell's unmunch tool for expanding dictionary words using affix files
Documentation
pub mod affix;
pub mod dictionary;
pub mod expander;
pub mod error;

pub use affix::{AffixFile, AffixRule, AffixType};
pub use dictionary::Dictionary;
pub use expander::WordExpander;
pub use error::{RunmunchError, Result};

use std::collections::HashSet;

pub struct Runmunch {
    affix_file: Option<AffixFile>,
    dictionary: Option<Dictionary>,
    expander: WordExpander,
}

impl Runmunch {
    pub fn new() -> Self {
        Self {
            affix_file: None,
            dictionary: None,
            expander: WordExpander::new(),
        }
    }

    pub fn load_affix_file<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
        let affix_file = AffixFile::load(path)?;
        self.expander.set_affix_file(&affix_file);
        self.affix_file = Some(affix_file);
        Ok(())
    }

    pub fn load_dictionary<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
        let dictionary = Dictionary::load(path)?;
        self.dictionary = Some(dictionary);
        Ok(())
    }

    pub fn expand_word(&self, word: &str) -> Result<Vec<String>> {
        self.expander.expand(word)
    }

    pub fn find_base_and_expand(&self, inflected_word: &str) -> Result<Vec<String>> {
        let dictionary = self.dictionary.as_ref()
            .ok_or_else(|| RunmunchError::NoDictionary)?;
        self.expander.find_base_and_expand(inflected_word, dictionary)
    }

    pub fn expand_words(&self, words: &[String]) -> Result<Vec<String>> {
        let mut result = Vec::new();
        let mut seen = HashSet::new();

        for word in words {
            let expanded = self.expand_word(word)?;
            for expanded_word in expanded {
                if seen.insert(expanded_word.clone()) {
                    result.push(expanded_word);
                }
            }
        }

        Ok(result)
    }

    pub fn unmunch(&self) -> Result<Vec<String>> {
        let dictionary = self.dictionary.as_ref()
            .ok_or_else(|| RunmunchError::NoDictionary)?;

        let mut result = Vec::new();
        let mut seen = HashSet::new();

        for (word, flags) in dictionary.entries() {
            let expanded = if flags.is_empty() {
                vec![word.clone()]
            } else {
                self.expander.expand_with_flags(word, flags)?
            };

            for expanded_word in expanded {
                if seen.insert(expanded_word.clone()) {
                    result.push(expanded_word);
                }
            }
        }

        Ok(result)
    }
}

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