logicaffeine_language/
lexicon.rs1include!(concat!(env!("OUT_DIR"), "/lexicon_data.rs"));
8
9pub use logicaffeine_lexicon::{
12 Aspect, Case, Definiteness, Feature, Gender, Number, Sort, Time, VerbClass,
13 AdjectiveMetadata, MorphologicalRule, NounMetadata, VerbEntry, VerbMetadata,
14};
15
16pub fn get_canonical_verb(lemma: &str) -> Option<(&'static str, bool)> {
20 lookup_canonical(lemma).map(|m| (m.lemma, m.polarity == Polarity::Negative))
21}
22
23pub trait LexiconTrait {
25 fn lookup_verb(&self, word: &str) -> Option<VerbMetadata>;
26 fn lookup_noun(&self, word: &str) -> Option<NounMetadata>;
27 fn lookup_adjective(&self, word: &str) -> Option<AdjectiveMetadata>;
28}
29
30pub struct StaticLexicon;
32
33impl LexiconTrait for StaticLexicon {
34 fn lookup_verb(&self, word: &str) -> Option<VerbMetadata> {
35 lookup_verb_db(word)
36 }
37
38 fn lookup_noun(&self, word: &str) -> Option<NounMetadata> {
39 lookup_noun_db(word)
40 }
41
42 fn lookup_adjective(&self, word: &str) -> Option<AdjectiveMetadata> {
43 lookup_adjective_db(word)
44 }
45}
46
47pub struct Lexicon {}
49
50impl Lexicon {
51 pub fn new() -> Self {
52 Lexicon {}
53 }
54
55 pub fn lookup_verb(&self, word: &str) -> Option<VerbEntry> {
56 let lower = word.to_lowercase();
57
58 if let Some(entry) = lookup_irregular_verb(&lower) {
59 return Some(entry);
60 }
61
62 if lower.ends_with("ing") {
63 let stem = self.strip_ing(&lower);
64 let lemma = Self::capitalize(&stem);
65 let class = self.lookup_verb_class(&lemma.to_lowercase());
66 return Some(VerbEntry {
67 lemma,
68 time: Time::None,
69 aspect: Aspect::Progressive,
70 class,
71 });
72 }
73
74 if lower.ends_with("ed") {
75 let stem = self.strip_ed(&lower);
76 if !is_base_verb(&stem) {
79 return None;
80 }
81 let lemma = Self::capitalize(&stem);
82 let class = self.lookup_verb_class(&lemma.to_lowercase());
83 return Some(VerbEntry {
84 lemma,
85 time: Time::Past,
86 aspect: Aspect::Simple,
87 class,
88 });
89 }
90
91 let is_third_person = if lower.ends_with("es") && lower.len() > 2 {
92 true
93 } else if lower.ends_with("s") && !lower.ends_with("ss") && lower.len() > 2 {
94 true
95 } else {
96 false
97 };
98
99 if is_third_person {
100 if is_stemming_exception(&lower) {
101 return None;
102 }
103
104 let stem = Self::third_person_stem(&lower)?;
105 let lemma = Self::capitalize(&stem);
106 let class = self.lookup_verb_class(&lemma.to_lowercase());
107 return Some(VerbEntry {
108 lemma,
109 time: Time::Present,
110 aspect: Aspect::Simple,
111 class,
112 });
113 }
114
115 if is_base_verb(&lower) {
117 let lemma = Self::capitalize(&lower);
118 let class = self.lookup_verb_class(&lower);
119 return Some(VerbEntry {
120 lemma,
121 time: Time::Present,
122 aspect: Aspect::Simple,
123 class,
124 });
125 }
126
127 None
128 }
129
130 fn lookup_verb_class(&self, lemma: &str) -> VerbClass {
131 lookup_verb_class(lemma)
132 }
133
134 fn strip_ing(&self, word: &str) -> String {
135 let base = &word[..word.len() - 3];
136
137 if base.len() >= 2 {
138 let chars: Vec<char> = base.chars().collect();
139 let last = chars[chars.len() - 1];
140 let second_last = chars[chars.len() - 2];
141
142 if last == second_last && !"aeiou".contains(last) {
143 return base[..base.len() - 1].to_string();
144 }
145 }
146
147 if needs_e_ing(base) {
148 return format!("{}e", base);
149 }
150
151 base.to_string()
152 }
153
154 fn strip_ed(&self, word: &str) -> String {
155 let base = &word[..word.len() - 2];
156
157 if base.ends_with("i") {
158 let with_e = format!("{}e", base);
162 if is_base_verb(&with_e) {
163 return with_e;
164 }
165 return format!("{}y", &base[..base.len() - 1]);
166 }
167
168 if base.len() >= 2 {
169 let chars: Vec<char> = base.chars().collect();
170 let last = chars[chars.len() - 1];
171 let second_last = chars[chars.len() - 2];
172
173 if last == second_last && !"aeiou".contains(last) {
177 if is_base_verb(base) {
179 return base.to_string();
180 }
181 return base[..base.len() - 1].to_string();
183 }
184
185 if (last == 'l' || last == 'r') && !"aeiou".contains(second_last) {
189 if chars.len() >= 3 && "aeiou".contains(chars[chars.len() - 3]) {
190 return format!("{}e", base);
191 }
192 }
193 }
194
195 if needs_e_ed(base) {
196 return format!("{}e", base);
197 }
198
199 let with_e = format!("{}e", base);
203 if is_base_verb(&with_e) {
204 return with_e;
205 }
206
207 base.to_string()
208 }
209
210 fn third_person_of(stem: &str) -> String {
214 if stem.ends_with('s')
215 || stem.ends_with('x')
216 || stem.ends_with('z')
217 || stem.ends_with("ch")
218 || stem.ends_with("sh")
219 || stem.ends_with('o')
220 {
221 format!("{stem}es")
222 } else if stem.ends_with('y')
223 && !stem.ends_with("ay")
224 && !stem.ends_with("ey")
225 && !stem.ends_with("oy")
226 && !stem.ends_with("uy")
227 {
228 format!("{}ies", &stem[..stem.len() - 1])
229 } else {
230 format!("{stem}s")
231 }
232 }
233
234 fn third_person_stem(word: &str) -> Option<String> {
240 let mut candidates = vec![word[..word.len() - 1].to_string()];
241 if word.ends_with("es") {
242 candidates.push(word[..word.len() - 2].to_string());
243 }
244 if word.ends_with("ies") {
245 candidates.push(format!("{}y", &word[..word.len() - 3]));
246 }
247 candidates
248 .into_iter()
249 .find(|stem| is_base_verb(stem) && Self::third_person_of(stem) == word)
250 }
251
252 fn capitalize(s: &str) -> String {
253 let mut chars = s.chars();
254 match chars.next() {
255 None => String::new(),
256 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
257 }
258 }
259}
260
261impl Default for Lexicon {
262 fn default() -> Self {
263 Self::new()
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum WordAnalysis {
270 Noun(NounMetadata),
272 DerivedNoun {
274 lemma: String,
275 number: Number,
276 },
277}
278
279pub fn analyze_word(word: &str) -> Option<WordAnalysis> {
286 let lower = word.to_lowercase();
287
288 if let Some(meta) = lookup_noun_db(&lower) {
291 return Some(WordAnalysis::Noun(meta));
292 }
293
294 if lower.ends_with('s') && lower.len() > 2 {
297 let stem = &lower[..lower.len() - 1];
299 if let Some(meta) = lookup_noun_db(stem) {
300 return Some(WordAnalysis::Noun(NounMetadata {
302 lemma: meta.lemma,
303 number: Number::Plural,
304 features: meta.features,
305 }));
306 }
307
308 if lower.ends_with("es") && lower.len() > 3 {
310 let stem_es = &lower[..lower.len() - 2];
311 if let Some(meta) = lookup_noun_db(stem_es) {
312 return Some(WordAnalysis::Noun(NounMetadata {
313 lemma: meta.lemma,
314 number: Number::Plural,
315 features: meta.features,
316 }));
317 }
318 }
319
320 if lower.ends_with("ies") && lower.len() > 4 {
322 let stem_ies = format!("{}y", &lower[..lower.len() - 3]);
323 if let Some(meta) = lookup_noun_db(&stem_ies) {
324 return Some(WordAnalysis::Noun(NounMetadata {
325 lemma: meta.lemma,
326 number: Number::Plural,
327 features: meta.features,
328 }));
329 }
330 }
331 }
332
333 for rule in get_morphological_rules() {
336 let (is_plural, check_word) = if lower.ends_with('s') && !rule.suffix.ends_with('s') {
338 (true, &lower[..lower.len() - 1])
339 } else {
340 (false, lower.as_str())
341 };
342
343 if check_word.ends_with(rule.suffix) {
344 return Some(WordAnalysis::DerivedNoun {
345 lemma: check_word.to_string(),
346 number: if is_plural { Number::Plural } else { Number::Singular },
347 });
348 }
349 }
350
351 None
352}
353
354pub fn is_derivable_noun(word: &str) -> bool {
357 analyze_word(word).is_some()
358}
359
360pub fn is_proper_name(word: &str) -> bool {
364 let lower = word.to_lowercase();
365 if let Some(meta) = lookup_noun_db(&lower) {
366 return meta.features.contains(&Feature::Proper);
367 }
368 false
369}
370
371pub fn get_canonical_noun(word: &str) -> Option<&'static str> {
382 match analyze_word(word) {
383 Some(WordAnalysis::Noun(meta)) => Some(meta.lemma),
384 Some(WordAnalysis::DerivedNoun { .. }) => {
385 None
389 }
390 _ => None,
391 }
392}