1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
mod endings;

pub mod regular;
pub mod irregular;
pub mod indeclinable;

pub use crate::noun::regular::{Regular};
pub use crate::noun::irregular::{Irregular};
pub use crate::noun::indeclinable::{Indeclinable};

// I want to export Gender, Case, and Number as part of this module.
pub use crate::decline::{Gender, Case, is_i_stem};
pub use crate::inflection::{Number};

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

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

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Group {
    First,
    Second,
    Third,
    Fourth,
    Fifth,
}

impl fmt::Display for Group {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Group::First => write!(f, "first"),
            Group::Second => write!(f, "second"),
            Group::Third => write!(f, "third"),
            Group::Fourth => write!(f, "fourth"),
            Group::Fifth => write!(f, "fifth"),
        }
    }
}

/// When Regular, Irregular, and Indeclinable structs are created, they ensure 
/// the Strings they receive are in NFC form. That makes the conversion done by
/// [`group`] redundant. Once Strings have been normalized, this function can 
/// be called instead of [`group`].
fn not_normalized_group(genitive: &str) -> Option<Group> {
    // Start with detecting fifth declension nouns because they along with
    // second declension nouns end in ī. The test for second declension 
    // nouns will return true before the test for fifth declension nouns 
    // can occur otherwise. 
    if genitive.ends_with("ēī") || genitive.ends_with("eī") {
        Some(Group::Fifth)
    } else if genitive.ends_with("ūs") || genitive.ends_with("ū") {
        Some(Group::Fourth)
    } else if genitive.ends_with("is") {
        Some(Group::Third)
    } else if genitive.ends_with("ī") {
        Some(Group::Second)
    } else if genitive.ends_with("ae") {
        Some(Group::First)
    } else {
        None
    } 
}
    
/// When Regular, Irregular, and Indeclinable structs are created, they ensure 
/// the Strings they receive are in NFC form. That makes the conversion done by
/// [`stem`] redundant. Once Strings have been normalized, this function can be 
/// called instead of [`stem`].
fn not_normalized_stem<'a>(nominative: &'a str, genitive: &'a str) -> Option<&'a str> {
    match not_normalized_group(genitive) {
        Some(Group::First) => Some(genitive.trim_end_matches("ae")),
        Some(Group::Second) if D::is_ius(nominative) => Some(genitive.trim_end_matches("iī")),
        Some(Group::Second) => Some(genitive.trim_end_matches("ī")),
        Some(Group::Third) => Some(genitive.trim_end_matches("is")),
        Some(Group::Fourth) if genitive.ends_with("ūs") => Some(genitive.trim_end_matches("ūs")),
        Some(Group::Fourth) => Some(genitive.trim_end_matches("ū")),
        Some(Group::Fifth) if genitive.ends_with("ēī") => Some(genitive.trim_end_matches("ēī")),
        Some(Group::Fifth) => Some(genitive.trim_end_matches("eī")),
        None => None,
    }
}

/// Determines the declension group of a regular noun from its singular 
/// genitive form. If the group cannot be ascertained, None is returned.
/// 
/// # Example
/// ```
/// extern crate verba;
/// 
/// use verba::noun::{Group, group};
/// 
/// assert_eq!(group("poētae"), Some(Group::First));
/// assert_eq!(group("dominī"), Some(Group::Second));
/// assert_eq!(group("ducis"), Some(Group::Third));
/// assert_eq!(group("portūs"), Some(Group::Fourth));
/// assert_eq!(group("reī"), Some(Group::Fifth));
/// assert_eq!(group("vīs"), None);
/// ```
pub fn group(genitive: &str) -> Option<Group> {
    // Since gen may not be normalized, this function will first calls 
    // Inflection::normalize to obtain normalized versions of the passed in 
    // string. Then it will call helpers::group with that strings and return a
    // Group.
    let genitive = U::normalize_if_needed(genitive);

    not_normalized_group(genitive.as_ref())
}

/// Takes the singular nominative and singular genitive forms of a noun and 
/// returns an Optional. If a stem can be derived from singular nominative and
/// genitive pair, a Some containing the stem is returned, otherwise None is
/// returned.
/// 
/// This function is primarily meant to be used for creating irregular nouns 
/// when the singular nominative and genitive forms are regular.
/// 
/// # Example
/// ```
/// extern crate verba;
/// 
/// use verba::noun::{stem};
/// 
/// assert_eq!(stem("porta", "portae"), Some("port".to_string()));
/// ```
pub fn stem<'a>(nominative: &'a str, genitive: &'a str) -> Option<String> {
    // Since nom and gen may not be normalized, this function will first 
    // called Unicode::normalize to obtain normalized versions of the passed in
    // strings. Then it will call helpers::stem with those strings and return
    // an owned String.
    //
    // The reason this function returns an owned String is so it can be 
    // directly passed into Irregular::new's stem parameter. 
    let nominative = U::normalize_if_needed(nominative);
    let genitive = U::normalize_if_needed(genitive);

    match not_normalized_stem(nominative.as_ref(), genitive.as_ref()) {
        Some(stem) => Some(stem.to_string()),
        None => None,
    }
}

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

    #[test]
    fn test_group() {
        assert_eq!(group("poētae"), Some(Group::First));
        assert_eq!(group("dominī"), Some(Group::Second));
        assert_eq!(group("ducis"), Some(Group::Third));
        assert_eq!(group("portūs"), Some(Group::Fourth));
        assert_eq!(group("reī"), Some(Group::Fifth));
        // If a word that doesn't comply with regular Latin singular genitives
        // is passed in, None should be returned. 
        assert_eq!(group("junk"), None);
    }

    #[test]
    fn test_stem() {
        assert_eq!(stem("poēta", "poētae"), Some("poēt".to_string()));
        assert_eq!(stem("dominus", "dominī"), Some("domin".to_string()));
        assert_eq!(stem("dux", "ducis"), Some("duc".to_string()));
        assert_eq!(stem("portus", "portūs"), Some("port".to_string()));
        assert_eq!(stem("rēs", "reī"), Some("r".to_string()));
        assert_eq!(stem("diēs", "diēī"), Some("di".to_string()));
        // Ensure stem returns None if the group cannot be determined.
        assert_eq!(stem("junk", "junk"), None);
    }
}