verba 0.5.1

A library for working with Latin words.
Documentation
use std::fmt;

use crate::unicode as U;

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

#[derive(Clone, Debug)]
pub struct Indeclinable {
    declension: String,
    gender: Gender,
}

#[derive(Clone, Debug)]
pub enum IndeclinableError {
    EmptyDeclension(),
}

impl fmt::Display for IndeclinableError {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        match self {
            IndeclinableError::EmptyDeclension() => {
                write!(f, "The declension string is empty.")
            },
        }
    }
}

impl Indeclinable {

    /// This function is used to create indeclinable Latin nouns.
    /// 
    /// Certain Latin nouns cannot be declined. These nouns, not surprisingly,
    /// are referred to as indeclinable nouns. When [`decline`] is called on a
    /// noun created with this function, it will always return the singular
    /// nominative form. 
    /// 
    /// # Example
    /// ```
    /// use verba::noun::{Noun};
    /// 
    /// use verba::noun as N;
    /// 
    /// let nom = "nīl".to_string();
    /// 
    /// let noun = N::Indeclinable::new(nom, N::Gender::Neuter).unwrap();
    /// 
    /// assert_eq!(noun.decline(N::Number::Singular, N::Case::Nominative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Singular, N::Case::Genitive), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Singular, N::Case::Dative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Singular, N::Case::Accusative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Singular, N::Case::Ablative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Singular, N::Case::Vocative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Plural, N::Case::Nominative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Plural, N::Case::Genitive), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Plural, N::Case::Dative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Plural, N::Case::Accusative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Plural, N::Case::Ablative), Some(vec!["nīl".to_string()]));
    /// assert_eq!(noun.decline(N::Number::Plural, N::Case::Vocative), Some(vec!["nīl".to_string()]));
    /// ```
    pub fn new(declension: String, gender: Gender) -> Result<Indeclinable, IndeclinableError> {
        if declension.is_empty() {
            Err(IndeclinableError::EmptyDeclension())
        } else {
            let declension = U::normalize(declension);

            Ok(Indeclinable {
                declension,
                gender,
            })
        }
    }
}

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

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

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

    fn decline(&self, _number: Number, _case: Case) -> Option<Vec<String>> {
        Some(vec![self.declension.to_string()])
    }
}

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

    #[test]
    fn test_new() {
        match Indeclinable::new("nīl".to_string(), Gender::Neuter) {
            Ok(noun) => {
                assert!(is_nfc(&noun.declension), "Declension is not in NFC form.");
                assert_eq!(noun.gender(), Gender::Neuter, "Indeclinable::gender returned an incorrect gender.");
                assert_eq!(noun.group(), None, "Indeclinable::group didn't return None.");
                assert_eq!(noun.stem(), None, "Indeclinable::stem didn't return None.");
            }
            Err(error) => panic!("Failed to create indeclinable noun {}. Received the following error: {}", "nīl", error),
        }
    }

    /// 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_indeclinable_nil() {
        let nom = "nīl".to_string();

        match Indeclinable::new(nom, Gender::Neuter) {
            Ok(noun) => {
                verify_declension(&noun.decline(Number::Singular, Case::Nominative), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Genitive), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Dative), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Accusative), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Ablative), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Singular, Case::Vocative), &Some(vec!["nīl".to_string()]));

                verify_declension(&noun.decline(Number::Plural, Case::Nominative), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Genitive), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Dative), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Accusative), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Ablative), &Some(vec!["nīl".to_string()]));
                verify_declension(&noun.decline(Number::Plural, Case::Vocative), &Some(vec!["nīl".to_string()]));
            },
            Err(_) => panic!("Failed to create indeclinable noun nīl"),
        }
    }

}