mlmorph 1.4.0

Malayalam morphological analyzer
Documentation
use crate::{
    AnalysisResult, Morpheme, ParsedAnalysis, foreign_word_detector::check_foreign_word,
    normalizer::normalize,
};
use regex::Regex;
use sfst::Sfst;
use std::collections::HashMap;
use std::path::PathBuf;

lazy_static::lazy_static! {
    static ref ANALYSER_REGEX: Regex = {
        Regex::new(r"((?P<root>([^<])+)(?P<tags>(<[^>]+>)+))+").unwrap()
    };

    static ref POS_REGEX: Regex = {
        Regex::new(r"(<(?P<tag>([^>]+))>)+").unwrap()
    };
}

pub struct Analyser {
    sfst: Sfst,
}

impl Analyser {
    const RESOURCE_PATH: &'static str = "data/malayalam.a";

    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
        let fsa_path = Self::get_fsa_path()?;
        let sfst = Sfst::new(&fsa_path)?;
        Ok(Analyser { sfst })
    }

    fn get_fsa_path() -> Result<String, Box<dyn std::error::Error>> {
        // First try to find the file in the current directory structure
        let current_dir_path = PathBuf::from(Self::RESOURCE_PATH);
        if current_dir_path.exists() {
            return Ok(current_dir_path.to_string_lossy().to_string());
        }

        // Try relative to the executable
        if let Ok(exe_path) = std::env::current_exe() {
            if let Some(exe_dir) = exe_path.parent() {
                let relative_path = exe_dir.join(Self::RESOURCE_PATH);
                if relative_path.exists() {
                    return Ok(relative_path.to_string_lossy().to_string());
                }
            }
        }

        // For development and testing, try some common locations
        let fallback_paths = [
            "malayalam.a",
            "../data/malayalam.a",
            "../../data/malayalam.a",
            "/usr/share/mlmorph/malayalam.a",
            "/usr/local/share/mlmorph/malayalam.a",
        ];

        for path in &fallback_paths {
            let path_buf = PathBuf::from(path);
            if path_buf.exists() {
                return Ok(path.to_string());
            }
        }

        Err("Could not find malayalam.a FSA file. Please ensure it's available in the data/ directory.".into())
    }

    pub fn analyse(
        &self,
        word: &str,
        weighted: bool,
        foreign_word_check: bool,
    ) -> Result<Vec<AnalysisResult>, Box<dyn std::error::Error>> {
        let word = normalize(word);
        let mut analysis_results = self.sfst.analyse(&word)?;

        if analysis_results.is_empty() {
            if foreign_word_check && check_foreign_word(&word) == 1 {
                analysis_results = vec![format!("{}<fw>", word)];
            }
        }

        if !weighted {
            return Ok(analysis_results.into_iter().map(|a| (a, 0)).collect());
        }

        let mut processed_result = Vec::new();
        for analysis in analysis_results {
            let weight = if weighted {
                Self::parse_analysis(&analysis)?.weight
            } else {
                0
            };
            processed_result.push((analysis, weight));
        }

        processed_result.sort_by_key(|(_, weight)| *weight);
        Ok(processed_result)
    }

    pub fn parse_analysis(analysis: &str) -> Result<ParsedAnalysis, Box<dyn std::error::Error>> {
        if analysis.is_empty() {
            return Ok(ParsedAnalysis {
                morphemes: Vec::new(),
                weight: 0,
            });
        }

        let analysis = if analysis.starts_with('<') {
            format!(" {}", analysis)
        } else {
            analysis.to_string()
        };

        let _caps = ANALYSER_REGEX
            .captures(&analysis)
            .ok_or("Could not parse the analysis")?;

        let mut morphemes = Vec::new();
        let roots: Vec<&str> = ANALYSER_REGEX
            .captures_iter(&analysis)
            .filter_map(|cap| cap.name("root"))
            .map(|m| m.as_str())
            .collect();

        let tags_list: Vec<&str> = ANALYSER_REGEX
            .captures_iter(&analysis)
            .filter_map(|cap| cap.name("tags"))
            .map(|m| m.as_str())
            .collect();

        for (i, root) in roots.iter().enumerate() {
            if let Some(tags) = tags_list.get(i) {
                let pos: Vec<String> = POS_REGEX
                    .captures_iter(tags)
                    .filter_map(|cap| cap.name("tag"))
                    .map(|m| m.as_str().to_string())
                    .collect();

                morphemes.push(Morpheme {
                    root: root.to_string(),
                    pos,
                });
            }
        }

        let weight = Self::get_weight(&morphemes);
        Ok(ParsedAnalysis { morphemes, weight })
    }

    fn get_weight(morphemes: &[Morpheme]) -> i32 {
        let morpheme_length = morphemes.len() as i32;
        let mut weight = morpheme_length * 100;

        for morpheme in morphemes {
            for pos in &morpheme.pos {
                weight += morpheme.pos.len() as i32 * 5
                    + morpheme.root.len() as i32 * 2
                    + Self::get_pos_weight(pos) * 3;
            }
        }

        weight
    }

    fn get_pos_weight(pos: &str) -> i32 {
        let weights: HashMap<&str, i32> = [
            ("v", 1),
            ("n", 2),
            ("adv", 3),
            ("adj", 4),
            ("coordinative", 4),
            ("v-n-compound", 4),
            ("prn", 5),
            ("past", 4),
            ("cvb-adv-part-past", 5),
            ("np", 5),
            ("fw", 1),
        ]
        .iter()
        .cloned()
        .collect();

        weights.get(pos).copied().unwrap_or(pos.len() as i32)
    }
}