english_pos_tagger/
lib.rs1#![forbid(unsafe_code)]
30#![warn(missing_docs)]
31
32mod data;
33mod lemmatize;
34mod normalize;
35mod rules;
36mod token;
37mod tokenizer;
38mod unigram;
39
40use std::collections::HashMap;
41
42pub use token::Token;
43
44const LEMMA_EXCEPTIONS: &[(&str, &str)] = &[
46 ("ai", "be"),
47 ("ca", "can"),
48 ("sha", "shall"),
49 ("'ll", "will"),
50 ("wo", "will"),
51 ("'ve", "have"),
52 ("'m", "am"),
53 ("'re", "be"),
54 ("n't", "not"),
55 ("'d", "would"),
56];
57
58const YEARS: &[&str] = &[
60 "1990s",
61 "1980s",
62 "1970s",
63 "1960s",
64 "1950s",
65 "1940s",
66 "1930s",
67 "1910s",
68 "mid-1990s",
69 "mid-1980s",
70 "mid-1970s",
71 "mid-1960s",
72 "mid-1950s",
73 "mid-1940s",
74 "mid-1930s",
75 "mid-1920s",
76 "mid-1910s",
77];
78
79fn lemma_exception(word: &str) -> Option<&'static str> {
80 LEMMA_EXCEPTIONS
81 .iter()
82 .find(|(k, _)| *k == word)
83 .map(|(_, v)| *v)
84}
85
86fn is_year(word: &str) -> bool {
87 YEARS.contains(&word)
88}
89
90pub struct Tagger {
92 lexicon: HashMap<String, Vec<String>>,
93}
94
95impl Default for Tagger {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101impl Tagger {
102 pub fn new() -> Self {
104 Tagger {
105 lexicon: data::LEXICON.clone(),
106 }
107 }
108
109 pub fn update_lexicon<I, K, V>(&mut self, entries: I)
112 where
113 I: IntoIterator<Item = (K, V)>,
114 K: AsRef<str>,
115 V: IntoIterator,
116 V::Item: Into<String>,
117 {
118 for (key, value) in entries {
119 let normalized = normalize::normalize(key.as_ref());
120 let poses: Vec<String> = value.into_iter().map(Into::into).collect();
121 self.lexicon.insert(normalized, poses);
122 }
123 }
124
125 pub fn tag(&self, mut tokens: Vec<Token>) -> Vec<Token> {
128 let mut poses: Vec<Vec<String>> = Vec::with_capacity(tokens.len());
129 for token in tokens.iter_mut() {
130 token.normal = Some(normalize::normalize(&token.value));
131 poses.push(unigram::unigram_pos_tagger(token, &self.lexicon));
132 }
133 rules::apply_all_context_rules(&mut tokens, &poses);
134 self.lemmatize(&mut tokens);
135 tokens
136 }
137
138 pub fn tag_tokens(&self, tokens: Vec<Token>) -> Vec<Token> {
140 self.tag(tokens)
141 }
142
143 pub fn tag_raw_tokens<S: AsRef<str>>(&self, raw_tokens: &[S]) -> Vec<Token> {
145 let tokens: Vec<Token> = raw_tokens
146 .iter()
147 .map(|t| {
148 let t = t.as_ref();
149 let tag = if rgx::is_number(t) {
150 "number"
151 } else if rgx::is_punctuation(t) {
152 "punctuation"
153 } else {
154 "word"
155 };
156 Token::new(t, tag)
157 })
158 .collect();
159 self.tag(tokens)
160 }
161
162 pub fn tag_sentence(&self, sentence: &str) -> Vec<Token> {
164 self.tag(tokenizer::tokenize(sentence))
165 }
166
167 fn lemmatize(&self, tokens: &mut [Token]) {
169 for token in tokens.iter_mut() {
170 let w = token.normal.clone().unwrap_or_default();
171 let v0 = token.value.chars().next();
172 let cap = matches!(v0, Some(c) if c.is_ascii_uppercase());
173
174 if is_year(&w) {
175 token.pos = Some("CD".to_string());
176 }
177
178 if let Some(lemma) = lemma_exception(&w) {
179 token.lemma = Some(lemma.to_string());
180 continue;
181 }
182
183 let pos = match &token.pos {
184 Some(p) => p.clone(),
185 None => continue,
186 };
187 let first = pos.chars().next();
188 match first {
189 Some('J') => {
190 if cap {
191 token.lemma = Some(w.clone());
192 token.pos = Some("NNP".to_string());
193 } else if pos.len() > 2 {
194 token.lemma = Some(lemmatize::adjective(&w));
195 } else {
196 token.lemma = Some(w.clone());
197 }
198 }
199 Some('V') => {
200 if pos.len() > 2 {
201 let lemma = if w == "'s" {
202 "be".to_string()
203 } else {
204 lemmatize::verb(&w)
205 };
206 token.lemma = Some(lemma);
207 } else {
208 token.lemma = Some(w.clone());
209 }
210 }
211 Some('N') => {
212 if cap {
213 token.lemma = Some(w.clone());
214 token.pos = Some("NNP".to_string());
215 } else if pos != "NNP" && pos.len() > 2 {
216 token.lemma = Some(lemmatize::noun(&w));
217 } else {
218 token.lemma = Some(w.clone());
219 }
220 }
221 Some('M') => {
222 token.lemma = Some(lemmatize::verb(&w));
223 }
224 _ => {}
225 }
226 }
227 }
228}
229
230mod rgx {
232 use once_cell::sync::Lazy;
233 use regex::Regex;
234
235 static NUMBER: Lazy<Regex> =
236 Lazy::new(|| Regex::new(r"(?-u:^(?:\d+/\d+|\d(?:[.,\-/]?\d)*(?:\.\d+)?)$)").unwrap());
237 static PUNCTUATION: Lazy<Regex> = Lazy::new(|| {
238 Regex::new("^[\u{2019}'\u{2018}\u{2019}`\u{201C}\u{201D}\"\\[\\](){}\u{2026},.!;?\\-:]+$")
239 .unwrap()
240 });
241
242 pub fn is_number(s: &str) -> bool {
243 NUMBER.is_match(s)
244 }
245
246 pub fn is_punctuation(s: &str) -> bool {
247 PUNCTUATION.is_match(s)
248 }
249}