pub const POSITIVE_WORDS: &[&str] = &[
"good", "great", "excellent", "amazing", "wonderful", "fantastic", "awesome",
"love", "loved", "loves", "happy", "joy", "joyful", "beautiful", "brilliant",
"best", "better", "bright", "charming", "delight", "delightful", "enjoy",
"enjoyed", "fabulous", "favor", "favorite", "fun", "generous", "gift",
"glad", "glorious", "grace", "grateful", "heaven", "helpful", "hope",
"hopeful", "incredible", "inspire", "inspiring", "kind", "lucky", "marvelous",
"nice", "perfect", "pleasant", "pleasure", "positive", "remarkable",
"shining", "splendid", "stellar", "superb", "terrific", "thank", "thanks",
"thrive", "treasure", "triumph", "vibrant", "victory", "warm", "win", "winner",
"wonder", "worth", "worthy", "yes",
];
pub const NEGATIVE_WORDS: &[&str] = &[
"bad", "terrible", "awful", "horrible", "worst", "worse", "hate", "hated",
"hates", "sad", "angry", "annoying", "boring", "broken", "careless",
"cold", "complain", "complaint", "cruel", "cursed", "damage", "danger",
"dangerous", "dark", "dead", "death", "defeat", "deny", "depressed",
"despair", "destroy", "destructive", "difficult", "disappoint",
"disappointed", "disgust", "disgusting", "dread", "dreadful", "dull",
"enemy", "evil", "fail", "failed", "failure", "fake", "fear", "gloomy",
"gross", "harm", "harmful", "harsh", "hurt", "hurts", "ill", "inferior",
"insult", "lame", "liar", "lonely", "lose", "loser", "loss", "mean",
"mess", "miserable", "negative", "nightmare", "no", "pain", "painful",
"pathetic", "poor", "problem", "punish", "reject", "rejected", "rotten",
"rude", "sick", "sorrow", "stink", "stupid", "suffer", "ugly", "unfair",
"unhappy", "weak", "wrong",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sentiment {
pub positive_hits: u32,
pub negative_hits: u32,
pub tokens: u32,
}
impl Sentiment {
#[must_use]
#[inline]
pub fn net(&self) -> i64 {
i64::from(self.positive_hits) - i64::from(self.negative_hits)
}
#[must_use]
#[inline]
pub fn comparison(&self) -> f64 {
let total = self.positive_hits + self.negative_hits;
if total == 0 {
0.0
} else {
(i64::from(self.positive_hits) - i64::from(self.negative_hits)) as f64 / f64::from(total)
}
}
#[must_use]
#[inline]
pub fn positive_share(&self) -> f64 {
let total = self.positive_hits + self.negative_hits;
if total == 0 {
0.0
} else {
f64::from(self.positive_hits) / f64::from(total)
}
}
#[must_use]
pub fn label(&self) -> &'static str {
match self.net() {
n if n > 0 => "positive",
n if n < 0 => "negative",
_ => "neutral",
}
}
}
fn tokenize(text: &str) -> Vec<String> {
text.split(|c: char| !(c.is_alphanumeric() || c == '\''))
.filter(|s| !s.is_empty())
.map(|s| s.to_ascii_lowercase())
.collect()
}
#[must_use]
pub fn score(text: &str) -> Sentiment {
use std::collections::HashSet;
let tokens = tokenize(text);
let token_count = tokens.len() as u32;
let pos: HashSet<&str> = POSITIVE_WORDS.iter().copied().collect();
let neg: HashSet<&str> = NEGATIVE_WORDS.iter().copied().collect();
let mut positive_hits = 0u32;
let mut negative_hits = 0u32;
for tok in &tokens {
if pos.contains(tok.as_str()) {
positive_hits += 1;
} else if neg.contains(tok.as_str()) {
negative_hits += 1;
}
}
Sentiment {
positive_hits,
negative_hits,
tokens: token_count,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_text_is_neutral() {
let r = score("");
assert_eq!(r.positive_hits, 0);
assert_eq!(r.negative_hits, 0);
assert_eq!(r.net(), 0);
assert_eq!(r.comparison(), 0.0);
assert_eq!(r.label(), "neutral");
}
#[test]
fn counts_positive_words_case_insensitively() {
let r = score("GOOD great AMAZING love Happy");
assert_eq!(r.positive_hits, 5);
assert_eq!(r.negative_hits, 0);
assert!(r.comparison() > 0.0);
assert_eq!(r.label(), "positive");
}
#[test]
fn counts_negative_words_case_insensitively() {
let r = score("BAD Terrible awful hate worst");
assert_eq!(r.negative_hits, 5);
assert_eq!(r.positive_hits, 0);
assert!(r.comparison() < 0.0);
assert_eq!(r.label(), "negative");
}
#[test]
fn mixed_text_balances_correctly() {
let r = score("I love this great day, but the food was awful and terrible.");
assert_eq!(r.positive_hits, 2); assert_eq!(r.negative_hits, 2); assert_eq!(r.net(), 0);
assert_eq!(r.comparison(), 0.0);
assert_eq!(r.label(), "neutral");
}
#[test]
fn whole_word_matching_not_substring() {
let r = score("goodness scaring");
assert_eq!(r.positive_hits, 0);
assert_eq!(r.negative_hits, 0);
}
#[test]
fn punctuation_does_not_break_matching() {
let r = score("great! wonderful? amazing. terrific,");
assert_eq!(r.positive_hits, 4);
}
#[test]
fn comparison_is_bounded_to_unit_interval() {
let r = score("love love love love love");
assert!((r.comparison() - 1.0).abs() < 1e-9);
let r = score("hate hate hate hate hate");
assert!((r.comparison() - (-1.0)).abs() < 1e-9);
}
#[test]
fn positive_share() {
let r = score("love love hate");
assert!((r.positive_share() - 2.0 / 3.0).abs() < 1e-9);
let r = score("the cat sat on the mat");
assert_eq!(r.positive_share(), 0.0);
}
#[test]
fn tokens_count_all_words_not_just_lexicon() {
let r = score("the good cat sat happily");
assert_eq!(r.tokens, 5);
assert!(r.positive_hits <= 2); }
#[test]
fn determinism() {
let t = "This is a great, wonderful, terrible, awful sentence.";
assert_eq!(score(t), score(t));
}
#[test]
fn contraction_handled_as_single_token() {
let r = score("I can't love this enough");
assert!(r.positive_hits >= 1); assert_eq!(r.negative_hits, 0);
}
#[test]
fn label_thresholds() {
assert_eq!(score("great wonderful amazing").label(), "positive");
assert_eq!(score("awful terrible bad").label(), "negative");
assert_eq!(score("great terrible").label(), "neutral"); assert_eq!(score("the cat sat").label(), "neutral"); }
}