verba 0.5.1

A library for working with Latin words.
Documentation
mod endings;

use std::fmt;
use crate::unicode as U;

pub mod regular;

pub use crate::adjective::regular::{Regular, RegularError};

pub use crate::decline::{Gender, Case, is_i_stem};
pub use crate::inflection::{Number};

pub trait Adjective {
    fn stem(&self) -> Option<&str>;
    fn group(&self) -> Option<Group>;
    fn decline(&self, number: Number, case: Case, gender: Gender) -> Option<Vec<String>>;
}

/// The dictionary form of an adjective can contain tow to three forms. For 
/// first and second declension adjectives, the dictionary form always consists
/// of the singular nominative for the masculine, feminine, and neuter forms. 
/// For third declension adjectives, things are slightly more complicated.
/// 
/// Third declension adjectives can have one, two, or three terminations. For 
/// third declension adjectives of one termination, the dictionary form 
/// consists of the singular nominative that is the same for all three genders
/// and the singular genitive, which is also the same for all three genders. 
/// 
/// For third declension adjectives of two terminations, the dictionary form
/// consists of the singular nominative for masculine and feminine forms, which
/// are the same, and the singular nominative for neuter forms. 
/// 
/// For third declension adjectives of three terminations, the dictionary form
/// consists of the singular nominative for masculine, feminine, and neuter 
/// forms, which are all different. 
pub enum DictionaryForm {
    Three(String, String, String),
    Two(String, String),
}

impl fmt::Display for DictionaryForm {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match self {
            DictionaryForm::Three(first, second, third) => write!(f, "{}, {}, {}", first, second, third),
            DictionaryForm::Two(first, second) => write!(f, "{}, {}", first, second),
        }
    }
}

pub enum Termination {
    One,
    Two,
    Three,
}

/// Regular Latin adjectives fall into either the first and second or the third
/// declension group. Third declension adjectives can have one, two, or three
/// terminations. 
pub enum Group {
    FirstSecond,
    Third(Termination),
}

impl DictionaryForm {
    /// A convenience function that returns a reference to the first element of a 
    /// DictionaryForm. 
    pub(self) fn first(&self) -> &str {
        match self {
            DictionaryForm::Three(first, _, _) => first,
            DictionaryForm::Two(first, _) => first,
        }
    }

    /// A convenience function that returns a reference to the second element of a 
    /// DictionaryForm. 
    pub(self) fn second(&self) -> &str {
        match self {
            DictionaryForm::Three(_, second, _) => second,
            DictionaryForm::Two(_, second) => second,
        }
    }

    /// A convenience function that returns a reference to the second element of a 
    /// DictionaryForm. 
    pub(self) fn third(&self) -> Option<&str> {
        match self {
            DictionaryForm::Three(_, _, third) => Some(third),
            DictionaryForm::Two(_, _) => None,
        }
    }
}

fn normalize_dictionary_form(dictionary_form: DictionaryForm) -> DictionaryForm {
    match dictionary_form {
        DictionaryForm::Three(f, s, t) => DictionaryForm::Three(U::normalize(f), U::normalize(s), U::normalize(t)),
        DictionaryForm::Two(f, s) => DictionaryForm::Two(U::normalize(f), U::normalize(s)),
    }
}

/// Why doesn't this function take a DictionaryForm? Because doing so would 
/// require [`group`] to basically create a new DictionaryForm containing 
/// normalized Strings whether the DictionaryForm's Strings were already in NFC
/// form or not. By taking two string references and one optional string 
/// reference [`group`] can call this by sending string references returned 
/// from [`normalize_if_needed`], which will references to the original String 
/// values if they're already in NFC form. 
fn not_normalized_group(f: &str, s: &str, t: Option<&str>) -> Option<Group> {
    match (f, s, t) {
        (f, s, Some(t)) if (f.ends_with("us") || f.ends_with("er")) && s.ends_with('a') && t.ends_with("um") => Some(Group::FirstSecond),
        (f, s, Some(t)) if f.ends_with("er") && s.ends_with("is") && t.ends_with('e') => Some(Group::Third(Termination::Three)),
        (f, s, None) if f.ends_with("is") && s.ends_with('e') => Some(Group::Third(Termination::Two)),
        (_, s, None) if s.ends_with("is") => Some(Group::Third(Termination::One)),
        _ => None,
    }
}

pub fn group(dictionary_form: &DictionaryForm) -> Option<Group> {
    match dictionary_form {
        DictionaryForm::Three(f, s, t) => not_normalized_group(U::normalize_if_needed(f).as_ref(), U::normalize_if_needed(s).as_ref(), Some(U::normalize_if_needed(t).as_ref())),
        DictionaryForm::Two(f, s) => not_normalized_group(U::normalize_if_needed(f).as_ref(), U::normalize_if_needed(s).as_ref(), None),
    }
}

/// This function doesn't take a DictionaryForm for the same reason 
/// not_normalized_group doesn't. 
fn not_normalized_stem<'a>(f: &'a str, s: &'a str, t: Option<&'a str>) -> Option<&'a str> {
    match not_normalized_group(f, s, t) {
        Some(Group::FirstSecond) => Some(s.trim_end_matches('a')),
        Some(Group::Third(Termination::Two)) => Some(f.trim_end_matches("is")),
        Some(Group::Third(_)) => Some(s.trim_end_matches("is")),
        _ => None,
    }
}

pub fn stem(dictionary_form: &DictionaryForm) -> Option<String> {
    match dictionary_form {
        DictionaryForm::Three(f, s, t) => {
            match not_normalized_stem(U::normalize_if_needed(f).as_ref(), U::normalize_if_needed(s).as_ref(), Some(U::normalize_if_needed(t).as_ref())) {
                Some(stem) => Some(stem.to_string()),
                None => None,
            }
        },
        DictionaryForm::Two(f, s) => {
            match not_normalized_stem(U::normalize_if_needed(f).as_ref(), U::normalize_if_needed(s).as_ref(), None) {
                Some(stem) => Some(stem.to_string()),
                None => None,
            }
        },
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use unicode_normalization::{is_nfc};

    #[test]
    fn test_group() {
        let first = DictionaryForm::Three("altus".to_string(), "alta".to_string(), "altum".to_string());
        match group(&first) {
            Some(Group::FirstSecond) => (), 
            _ => panic!("Received an incorrect group for a first and second declension adjective."),
        }

        let third_one = DictionaryForm::Two("atrōx".to_string(), "atrōcis".to_string());
        match group(&third_one) {
            Some(Group::Third(Termination::One)) => (), 
            _ => panic!("Received an incorrect group for a third declension adjective with one termination."),
        }

        let third_two = DictionaryForm::Two("agilis".to_string(), "agile".to_string());
        match group(&third_two) {
            Some(Group::Third(Termination::Two)) => (), 
            _ => panic!("Received an incorrect group for a third declension adjective with two terminations."),
        }

        let third_three = DictionaryForm::Three("celer".to_string(), "celeris".to_string(), "celere".to_string());
        match group(&third_three) {
            Some(Group::Third(Termination::Three)) => (), 
            _ => panic!("Received an incorrect group for a third declension adjective with three terminations."),
        }
    }

    #[test]
    fn test_stem() {
        let first = DictionaryForm::Three("altus".to_string(), "alta".to_string(), "altum".to_string());
        assert_eq!(stem(&first), Some("alt".to_string()), "Failed to get the stem for a first and second declension adjective.");

        let third_one = DictionaryForm::Two("atrōx".to_string(), "atrōcis".to_string());
        assert_eq!(stem(&third_one), Some("atrōc".to_string()), "Failed to get the stem for a third declension adjective with one termination.");

        let third_two = DictionaryForm::Two("agilis".to_string(), "agile".to_string());
        assert_eq!(stem(&third_two), Some("agil".to_string()), "Failed to get the stem for a third declension adjective with two terminations.");

        let third_three = DictionaryForm::Three("celer".to_string(), "celeris".to_string(), "celere".to_string());
        assert_eq!(stem(&third_three), Some("celer".to_string()), "Failed to get the stem for a third declension adjective with three terminations.");
    }

}