1pub const POSITIVE_WORDS: &[&str] = &[
35 "good", "great", "excellent", "amazing", "wonderful", "fantastic", "awesome",
36 "love", "loved", "loves", "happy", "joy", "joyful", "beautiful", "brilliant",
37 "best", "better", "bright", "charming", "delight", "delightful", "enjoy",
38 "enjoyed", "fabulous", "favor", "favorite", "fun", "generous", "gift",
39 "glad", "glorious", "grace", "grateful", "heaven", "helpful", "hope",
40 "hopeful", "incredible", "inspire", "inspiring", "kind", "lucky", "marvelous",
41 "nice", "perfect", "pleasant", "pleasure", "positive", "remarkable",
42 "shining", "splendid", "stellar", "superb", "terrific", "thank", "thanks",
43 "thrive", "treasure", "triumph", "vibrant", "victory", "warm", "win", "winner",
44 "wonder", "worth", "worthy", "yes",
45];
46
47pub const NEGATIVE_WORDS: &[&str] = &[
49 "bad", "terrible", "awful", "horrible", "worst", "worse", "hate", "hated",
50 "hates", "sad", "angry", "annoying", "boring", "broken", "careless",
51 "cold", "complain", "complaint", "cruel", "cursed", "damage", "danger",
52 "dangerous", "dark", "dead", "death", "defeat", "deny", "depressed",
53 "despair", "destroy", "destructive", "difficult", "disappoint",
54 "disappointed", "disgust", "disgusting", "dread", "dreadful", "dull",
55 "enemy", "evil", "fail", "failed", "failure", "fake", "fear", "gloomy",
56 "gross", "harm", "harmful", "harsh", "hurt", "hurts", "ill", "inferior",
57 "insult", "lame", "liar", "lonely", "lose", "loser", "loss", "mean",
58 "mess", "miserable", "negative", "nightmare", "no", "pain", "painful",
59 "pathetic", "poor", "problem", "punish", "reject", "rejected", "rotten",
60 "rude", "sick", "sorrow", "stink", "stupid", "suffer", "ugly", "unfair",
61 "unhappy", "weak", "wrong",
62];
63
64#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Sentiment {
70 pub positive_hits: u32,
72 pub negative_hits: u32,
74 pub tokens: u32,
76}
77
78impl Sentiment {
79 #[must_use]
84 #[inline]
85 pub fn net(&self) -> i64 {
86 i64::from(self.positive_hits) - i64::from(self.negative_hits)
87 }
88
89 #[must_use]
94 #[inline]
95 pub fn comparison(&self) -> f64 {
96 let total = self.positive_hits + self.negative_hits;
97 if total == 0 {
98 0.0
99 } else {
100 (i64::from(self.positive_hits) - i64::from(self.negative_hits)) as f64 / f64::from(total)
101 }
102 }
103
104 #[must_use]
108 #[inline]
109 pub fn positive_share(&self) -> f64 {
110 let total = self.positive_hits + self.negative_hits;
111 if total == 0 {
112 0.0
113 } else {
114 f64::from(self.positive_hits) / f64::from(total)
115 }
116 }
117
118 #[must_use]
120 pub fn label(&self) -> &'static str {
121 match self.net() {
122 n if n > 0 => "positive",
123 n if n < 0 => "negative",
124 _ => "neutral",
125 }
126 }
127}
128
129fn tokenize(text: &str) -> Vec<String> {
135 text.split(|c: char| !(c.is_alphanumeric() || c == '\''))
136 .filter(|s| !s.is_empty())
137 .map(|s| s.to_ascii_lowercase())
138 .collect()
139}
140
141#[must_use]
155pub fn score(text: &str) -> Sentiment {
156 use std::collections::HashSet;
157
158 let tokens = tokenize(text);
159 let token_count = tokens.len() as u32;
160
161 let pos: HashSet<&str> = POSITIVE_WORDS.iter().copied().collect();
162 let neg: HashSet<&str> = NEGATIVE_WORDS.iter().copied().collect();
163
164 let mut positive_hits = 0u32;
165 let mut negative_hits = 0u32;
166 for tok in &tokens {
167 if pos.contains(tok.as_str()) {
168 positive_hits += 1;
169 } else if neg.contains(tok.as_str()) {
170 negative_hits += 1;
171 }
172 }
173
174 Sentiment {
175 positive_hits,
176 negative_hits,
177 tokens: token_count,
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn empty_text_is_neutral() {
187 let r = score("");
188 assert_eq!(r.positive_hits, 0);
189 assert_eq!(r.negative_hits, 0);
190 assert_eq!(r.net(), 0);
191 assert_eq!(r.comparison(), 0.0);
192 assert_eq!(r.label(), "neutral");
193 }
194
195 #[test]
196 fn counts_positive_words_case_insensitively() {
197 let r = score("GOOD great AMAZING love Happy");
198 assert_eq!(r.positive_hits, 5);
199 assert_eq!(r.negative_hits, 0);
200 assert!(r.comparison() > 0.0);
201 assert_eq!(r.label(), "positive");
202 }
203
204 #[test]
205 fn counts_negative_words_case_insensitively() {
206 let r = score("BAD Terrible awful hate worst");
207 assert_eq!(r.negative_hits, 5);
208 assert_eq!(r.positive_hits, 0);
209 assert!(r.comparison() < 0.0);
210 assert_eq!(r.label(), "negative");
211 }
212
213 #[test]
214 fn mixed_text_balances_correctly() {
215 let r = score("I love this great day, but the food was awful and terrible.");
216 assert_eq!(r.positive_hits, 2); assert_eq!(r.negative_hits, 2); assert_eq!(r.net(), 0);
219 assert_eq!(r.comparison(), 0.0);
220 assert_eq!(r.label(), "neutral");
221 }
222
223 #[test]
224 fn whole_word_matching_not_substring() {
225 let r = score("goodness scaring");
229 assert_eq!(r.positive_hits, 0);
230 assert_eq!(r.negative_hits, 0);
231 }
232
233 #[test]
234 fn punctuation_does_not_break_matching() {
235 let r = score("great! wonderful? amazing. terrific,");
236 assert_eq!(r.positive_hits, 4);
237 }
238
239 #[test]
240 fn comparison_is_bounded_to_unit_interval() {
241 let r = score("love love love love love");
242 assert!((r.comparison() - 1.0).abs() < 1e-9);
243 let r = score("hate hate hate hate hate");
244 assert!((r.comparison() - (-1.0)).abs() < 1e-9);
245 }
246
247 #[test]
248 fn positive_share() {
249 let r = score("love love hate");
250 assert!((r.positive_share() - 2.0 / 3.0).abs() < 1e-9);
251 let r = score("the cat sat on the mat");
253 assert_eq!(r.positive_share(), 0.0);
254 }
255
256 #[test]
257 fn tokens_count_all_words_not_just_lexicon() {
258 let r = score("the good cat sat happily");
259 assert_eq!(r.tokens, 5);
260 assert!(r.positive_hits <= 2); }
262
263 #[test]
264 fn determinism() {
265 let t = "This is a great, wonderful, terrible, awful sentence.";
266 assert_eq!(score(t), score(t));
267 }
268
269 #[test]
270 fn contraction_handled_as_single_token() {
271 let r = score("I can't love this enough");
273 assert!(r.positive_hits >= 1); assert_eq!(r.negative_hits, 0);
275 }
276
277 #[test]
278 fn label_thresholds() {
279 assert_eq!(score("great wonderful amazing").label(), "positive");
280 assert_eq!(score("awful terrible bad").label(), "negative");
281 assert_eq!(score("great terrible").label(), "neutral"); assert_eq!(score("the cat sat").label(), "neutral"); }
284}