verba 0.5.1

A library for working with Latin words.
Documentation
use std::fmt;
use std::borrow::Cow;
use std::collections::HashMap;

use crate::unicode as U;
use crate::noun::endings as E;
use crate::inflection as I;
use crate::decline as D;

use super::{Group, Number, Gender, Case};

#[derive(Clone, Debug)]
pub struct Irregular {
    declensions: HashMap<(Number, Case), Option<Vec<String>>>,
    gender: Gender,
    stem: String,
    group: Group,
    // Second, third, and fourth declension nouns have alternate declensions. 
    // If this flag is set, it will use those alternate declensions (see 
    // Irregular::endings for those alternatives).
    alt_declension: bool,
}

#[derive(Clone, Debug)]
pub enum IrregularError {
    EmptyDeclension((Number, Case)),
    EmptyDeclensionValue((Number, Case)),
}

impl fmt::Display for IrregularError {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match self {
            IrregularError::EmptyDeclension((number, case)) => {
                write!(f, "The declined form for the {} {} is empty", number, case)
            },
            IrregularError::EmptyDeclensionValue((number, case)) => {
                write!(f, "The declined form for the {} {} contains an empty string.", number, case)
            },
        }
    }
}


impl Irregular {
    /// Ensure all of the String values contained in the declensions HashMap
    /// are in NFC form. 
    fn normalize_declensions(declensions: &mut HashMap<(Number, Case), Option<Vec<String>>>) {
        for maybe_words in declensions.values_mut() { 
            if let Some(words) = maybe_words {
                for word in words.iter_mut() {
                    if let Cow::Owned(normalized) = U::normalize_if_needed(word) { // Receiving a Cow::Owned from U::normalize_if_needed indicates that the String wasn't in NFC form. 
                        *word = normalized; // Overwrite the non-NFC form String with the NFC form String. 
                    }
                }
            }
        }
    }

    /// Verifies that the declensions HashMap doesn't contain empty Vecs and 
    /// that the Vecs don't contain empty strings. 
    fn declensions_not_empty(declensions: &HashMap<(Number, Case), Option<Vec<String>>>) -> Result<(), IrregularError> {
        for (key, maybe_declension) in declensions.iter() {
            if let Some(declension) = maybe_declension {
                if declension.is_empty() {
                    return Err(IrregularError::EmptyDeclension(*key))
                } else {
                    for word in declension {
                        if word.is_empty() {
                            return Err(IrregularError::EmptyDeclensionValue(*key))
                        }
                    }
                }
            }
        }

        Ok(())
    }

    /// Creates an irregular Latin noun.
    /// 
    /// Latin nouns can contain declensions that do not follow the rules of any
    /// regular declension group. These nouns are, not surprisingly, referred 
    /// to as irregular. 
    /// 
    /// This function provides a method for creating irregular nouns. Unlike 
    /// [`Regular::new`], which takes the singular nominative and genitive 
    /// forms, this function takes a HashMap containing all of the irregular 
    /// declined forms, the stem, and the declension group. If a declined form 
    /// isn't present in the HashMap, it is assumed to be regular and will be 
    /// declined according to the regular Latin rules for the passed in 
    /// declension group. 
    /// 
    /// For example, if the HashMap contains values for the singular dative
    /// form but not the singular ablative form, the singular dative form will
    /// be declined as it appears in the HashMap but the singular ablative form
    /// will be declined the same as a noun created using [`Regular::new`].
    /// 
    /// # Warning
    /// 
    /// Third declension nouns have a singular nominative and vocative (and 
    /// accusative in the case of neuter nouns) form that cannot be declined
    /// by combining the stem with an ending. Because of this, calling 
    /// [`Irregular::decline`] on a third declension noun for those cases will
    /// return [`Non`] unless they are added to the declension HashMap.
    /// 
    /// # Warning
    /// 
    /// Third declension nouns can be either consonant-stem or i-stem. Third 
    /// declension nouns created with this function will decline as consonant-
    /// stem. When creating a third declension i-stem noun, use 
    /// [`new_third_i_stem`] instead.
    /// 
    /// # Example
    /// 
    /// vīs, vīs is an irregular noun with an irregular singular genitive form. 
    /// The stem is vīr- but none of the singular cases use. Moreover, the 
    /// plural cases decline like a third declension i-stem. So all six
    /// singular forms must be provided as do the plural genitive and 
    /// accusative forms. The rest of the forms can be determined by combining
    /// the provided stem with the regular third declension endings. 
    /// 
    /// This example shows how to create the HashMap containing the irregular
    /// declined forms and demonstrates two declensions. The first declension
    /// is regular, the second is irregular. 
    /// ```
    /// use std::collections::HashMap;
    /// use verba::noun as N;
    /// use verba::noun::{Noun};
    /// 
    /// let mut declension = HashMap::new();
    /// declension.insert((N::Number::Singular, N::Case::Nominative), Some(vec!["vīs".to_string()]));
    /// declension.insert((N::Number::Singular, N::Case::Genitive), Some(vec!["vīs".to_string()]));
    /// declension.insert((N::Number::Singular, N::Case::Dative), Some(vec!["vī".to_string()]));
    /// declension.insert((N::Number::Singular, N::Case::Accusative), Some(vec!["vim".to_string()]));
    /// declension.insert((N::Number::Singular, N::Case::Ablative), Some(vec!["vī".to_string()]));
    /// declension.insert((N::Number::Singular, N::Case::Vocative), Some(vec!["vīs".to_string()]));
    /// declension.insert((N::Number::Plural, N::Case::Genitive), Some(vec!["vīrium".to_string()]));
    /// declension.insert((N::Number::Plural, N::Case::Accusative), Some(vec!["vīrēs".to_string(), "vīrīs".to_string()]));
    /// 
    /// let noun = N::Irregular::new(declension, N::Gender::Feminine, "vīr".to_string(), N::Group::Third).unwrap();
    /// 
    /// assert_eq!(noun.decline(N::Number::Plural, N::Case::Nominative), Some(vec!["vīrēs".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Plural, N::Case::Genitive), Some(vec!["vīrium".to_string()]));
    /// ```
    pub fn new(mut declensions: HashMap<(Number, Case), Option<Vec<String>>>, gender: Gender, stem: String, group: Group) -> Result<Irregular, IrregularError> {
        Irregular::normalize_declensions(&mut declensions);
        let stem = U::normalize(stem);

        match Irregular::declensions_not_empty(&declensions) {
            Ok(()) => {
                Ok(Irregular {
                    declensions,
                    gender,
                    stem,
                    group,
                    alt_declension: false,
                })
            },
            Err(error) => Err(error),
        }
    }

    /// Creates a new Irregular and sets it alt_declension value to true. 
    fn new_alt_declension(declensions: HashMap<(Number, Case), Option<Vec<String>>>, gender: Gender, stem: String, group: Group) -> Result<Irregular, IrregularError> {
        match Irregular::new(declensions, gender, stem, group) {
            Ok(mut noun) => {
                noun.alt_declension = true;

                Ok(noun)
            },
            Err(error) => Err(error),
        }
    }

    pub fn new_second_ius(declensions: HashMap<(Number, Case), Option<Vec<String>>>, gender: Gender, stem: String) -> Result<Irregular, IrregularError> {
        Irregular::new_alt_declension(declensions, gender, stem, Group::Second)
    }

    pub fn new_third_i_stem(declensions: HashMap<(Number, Case), Option<Vec<String>>>, gender: Gender, stem: String) -> Result<Irregular, IrregularError> {
        Irregular::new_alt_declension(declensions, gender, stem, Group::Third)
    }

    pub fn new_fourth_u(declensions: HashMap<(Number, Case), Option<Vec<String>>>, gender: Gender, stem: String) -> Result<Irregular, IrregularError> {
        Irregular::new_alt_declension(declensions, gender, stem, Group::Fourth)
    }

    fn endings(&self, number: Number, case: Case) -> Option<E::Suffixes> {
        match self.group {
            Group::First => E::first_endings(number, case, self.gender),
            Group::Second if self.alt_declension => E::second_ius_endings(number, case, self.gender),
            Group::Second => E::second_endings(number, case, self.gender),
            Group::Third if self.alt_declension => E::third_i_stem_endings(number, case, self.gender),
            Group::Third => E::third_endings(number, case, self.gender),
            Group::Fourth if self.alt_declension => E::fourth_u_endings(number, case, self.gender),
            Group::Fourth => E::fourth_endings(number, case, self.gender),
            Group::Fifth if D::does_end_with_vowel(&self.stem) => E::fifth_vowel_stem_endings(number, case, self.gender),
            Group::Fifth => E::fifth_endings(number, case, self.gender),
        }
    }
}

impl super::Noun for Irregular {
    fn gender(&self) -> Gender {
        self.gender
    }

    fn stem(&self) -> Option<&str> {
        Some(&self.stem)
    }

    fn group(&self) -> Option<Group> {
        Some(self.group)
    }

    fn decline(&self, number: Number, case: Case) -> Option<Vec<String>> {
        // If self.declensions contains the declension for the passed in number
        // and case, create a clone of it and return the clone. Otherwise, 
        // decline the case as you would a RegularNoun. 
        if let Some(declension) = self.declensions.get(&(number, case)) {
            match declension {
                Some(words) => Some(words.to_vec()),
                None => None,
            }
        } else {
            match self.endings(number, case) {
                Some(suffixes) => {
                    match self.stem() {
                        Some(stem) => Some(I::stem_with_endings(stem, &suffixes)),
                        None => None,
                    }
                },
                None => None,
            }
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    
    use crate::noun::{Noun, Group, Number, Gender, Case};
    use unicode_normalization::{UnicodeNormalization, is_nfc};

    fn verify_normalization(declensions: &HashMap<(Number, Case), Option<Vec<String>>>) {
        for key in declensions.keys() {
            if let Some(Some(words)) = declensions.get(key) {
                for word in words {
                    if is_nfc(word) == false {
                        panic!("Word {} was not in NFC form.", word);
                    }
                }
            }
        }
    }

    /// Takes two Vec<String> arguments. The first is a value returned from 
    /// Noun::decline and the second is the expected values. 
    /// 
    /// First the two arguments are checked to make sure they have the same 
    /// length. If they are, then each element in the first argument's Unicode
    /// normalized form is checked to be composed. If it is, then the function
    /// checks if the element is stored in the second argument. If it is, then
    /// the function returns true. 
    fn verify_declension(declension: &Option<Vec<String>>, correct: &Option<Vec<String>>) {
        match (declension, correct) {
            (Some(declension), Some(correct)) => {
                assert!(declension.len() == correct.len(), "The Vecs `declension` and `correct` are not the same length.");

                for (index, form) in declension.iter().enumerate() {
                    // First check to ensure the element is in normalized composed
                    // form. If it's not, the second check may return false even if the
                    // strings appears equal.
                    assert!(is_nfc(form), "{} is not in normalized composed form.", form);

                    assert!(form == &correct[index], "Element {}, {}, from `declension` is not equal to element {}, {}, from `correct`.", index, form, index, &correct[index]);
                }
            },
            (Some(_), None) => panic!("Vec `declension` contained data but `correct` was None."),
            (None, Some(_)) => panic!("Vec `declension` was None but `correct` contained data."),
            (None, None) => (),
        }
    }

    #[test]
    fn test_normalize_declensions() {
        let mut declensions = HashMap::new();

        // Obviously this isn't a valid declension.
        declensions.insert((Number::Singular, Case::Nominative), Some(vec!["cornū".nfd().collect::<String>()]));
        declensions.insert((Number::Singular, Case::Genitive), Some(vec!["cornūs".nfd().collect::<String>()]));
        declensions.insert((Number::Singular, Case::Dative), Some(vec!["cornū".nfd().collect::<String>(), "cornūs".nfd().collect::<String>()]));
        declensions.insert((Number::Singular, Case::Accusative), Some(vec!["cornū".nfd().collect::<String>()]));
        declensions.insert((Number::Singular, Case::Ablative), Some(vec!["cornū".nfd().collect::<String>()]));
        declensions.insert((Number::Singular, Case::Vocative), Some(vec!["cornū".nfd().collect::<String>()]));
        declensions.insert((Number::Plural, Case::Nominative), Some(vec!["cornū".nfd().collect::<String>()]));
        declensions.insert((Number::Plural, Case::Genitive), Some(vec!["cornūs".nfd().collect::<String>()]));
        declensions.insert((Number::Plural, Case::Dative), Some(vec!["cornū".nfd().collect::<String>(), "cornūs".nfd().collect::<String>()]));
        declensions.insert((Number::Plural, Case::Accusative), Some(vec!["cornū".nfd().collect::<String>()]));
        declensions.insert((Number::Plural, Case::Ablative), Some(vec!["cornū".nfd().collect::<String>()]));
        declensions.insert((Number::Plural, Case::Vocative), Some(vec!["cornū".nfd().collect::<String>()]));

        Irregular::normalize_declensions(&mut declensions);

        verify_normalization(&declensions);
    }

    #[test]
    fn test_irregular_vis() {
        let mut declension = HashMap::new();
        declension.insert((Number::Singular, Case::Nominative), Some(vec!["vīs".to_string()]));
        declension.insert((Number::Singular, Case::Genitive), Some(vec!["vīs".to_string()]));
        declension.insert((Number::Singular, Case::Dative), Some(vec!["".to_string()]));
        declension.insert((Number::Singular, Case::Accusative), Some(vec!["vim".to_string()]));
        declension.insert((Number::Singular, Case::Ablative), Some(vec!["".to_string()]));
        declension.insert((Number::Singular, Case::Vocative), Some(vec!["vīs".to_string()]));
        declension.insert((Number::Plural, Case::Genitive), Some(vec!["vīrium".to_string()]));
        declension.insert((Number::Plural, Case::Accusative), Some(vec!["vīrēs".to_string(), "vīrīs".to_string()]));

        match Irregular::new(declension, Gender::Feminine, "vīr".to_string(), Group::Third) {
            Ok(noun) => {
                verify_declension(&noun.decline(Number::Singular, Case::Nominative), &Some(vec!["vīs".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Genitive), &Some(vec!["vīs".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Dative), &Some(vec!["".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Accusative), &Some(vec!["vim".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Ablative), &Some(vec!["".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Vocative), &Some(vec!["vīs".to_string()]));

                verify_declension(&noun.decline(Number::Plural, Case::Nominative), &Some(vec!["vīrēs".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Genitive), &Some(vec!["vīrium".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Dative), &Some(vec!["vīribus".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Accusative), &Some(vec!["vīrēs".to_string(), "vīrīs".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Ablative), &Some(vec!["vīribus".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Vocative), &Some(vec!["vīrēs".to_string()]));
            },
            Err(_) => panic!("Failed to create irregular noun vīs, vīs"),
        }
    }

    #[test]
    fn test_irregular_i_stem_vis() {
        let mut declension = HashMap::new();
        declension.insert((Number::Singular, Case::Nominative), Some(vec!["vīs".to_string()]));
        declension.insert((Number::Singular, Case::Genitive), Some(vec!["vīs".to_string()]));
        declension.insert((Number::Singular, Case::Dative), Some(vec!["".to_string()]));
        declension.insert((Number::Singular, Case::Accusative), Some(vec!["vim".to_string()]));
        declension.insert((Number::Singular, Case::Ablative), Some(vec!["".to_string()]));
        declension.insert((Number::Singular, Case::Vocative), Some(vec!["vīs".to_string()]));
        declension.insert((Number::Plural, Case::Genitive), Some(vec!["vīrium".to_string()]));
        declension.insert((Number::Plural, Case::Accusative), Some(vec!["vīrēs".to_string(), "vīrīs".to_string()]));

        match Irregular::new_third_i_stem(declension, Gender::Feminine, "vīr".to_string()) {
            Ok(noun) => {
                verify_declension(&noun.decline(Number::Singular, Case::Nominative), &Some(vec!["vīs".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Genitive), &Some(vec!["vīs".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Dative), &Some(vec!["".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Accusative), &Some(vec!["vim".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Ablative), &Some(vec!["".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Vocative), &Some(vec!["vīs".to_string()]));

                verify_declension(&noun.decline(Number::Plural, Case::Nominative), &Some(vec!["vīrēs".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Genitive), &Some(vec!["vīrium".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Dative), &Some(vec!["vīribus".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Accusative), &Some(vec!["vīrēs".to_string(), "vīrīs".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Ablative), &Some(vec!["vīribus".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Vocative), &Some(vec!["vīrēs".to_string()]));
            },
            Err(_) => panic!("Failed to create irregular noun vīs, vīs"),
        }
    }

}