Skip to main content

writing_analysis/
sentiment.rs

1use crate::error::{Result, WritingAnalysisError};
2use crate::lexicon::get_score;
3use crate::utils::split_words;
4
5/// Result of sentiment analysis.
6#[derive(Debug, Clone, PartialEq)]
7pub struct SentimentResult {
8    /// Overall sentiment score (-1.0 to 1.0)
9    pub score: f64,
10    /// Comparative score: total sentiment / token count
11    pub comparative: f64,
12    /// Per-token sentiment breakdown
13    pub tokens: Vec<TokenSentiment>,
14}
15
16/// Sentiment data for a single token.
17#[derive(Debug, Clone, PartialEq)]
18pub struct TokenSentiment {
19    /// The word/token
20    pub word: String,
21    /// AFINN score (-5 to +5), 0 if not in lexicon
22    pub score: i32,
23}
24
25/// Analyze sentiment of text using AFINN lexicon.
26pub fn analyze_sentiment(text: &str) -> Result<SentimentResult> {
27    let words = split_words(text);
28    if words.is_empty() {
29        return Err(WritingAnalysisError::EmptyText);
30    }
31
32    let mut tokens = Vec::new();
33    let mut total_score: i32 = 0;
34
35    for word in &words {
36        let lower = word.to_lowercase();
37        let score = get_score(&lower).unwrap_or(0);
38        total_score += score;
39        tokens.push(TokenSentiment {
40            word: word.to_string(),
41            score,
42        });
43    }
44
45    let token_count = tokens.len() as f64;
46    let comparative = total_score as f64 / token_count;
47    let score = (comparative * 2.0).clamp(-1.0, 1.0);
48
49    Ok(SentimentResult {
50        score,
51        comparative,
52        tokens,
53    })
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn positive_sentiment() {
62        let result = analyze_sentiment("I love this wonderful movie.").unwrap();
63        assert!(result.score > 0.0);
64        assert!(result.comparative > 0.0);
65    }
66
67    #[test]
68    fn negative_sentiment() {
69        let result = analyze_sentiment("This is a terrible awful disaster.").unwrap();
70        assert!(result.score < 0.0);
71        assert!(result.comparative < 0.0);
72    }
73
74    #[test]
75    fn neutral_sentiment() {
76        let result = analyze_sentiment("The table is in the room.").unwrap();
77        assert!(result.score.abs() < 0.3);
78    }
79
80    #[test]
81    fn token_level_scores() {
82        let result = analyze_sentiment("I love hate things.").unwrap();
83        let love_token = result.tokens.iter().find(|t| t.word == "love").unwrap();
84        let hate_token = result.tokens.iter().find(|t| t.word == "hate").unwrap();
85        assert!(love_token.score > 0);
86        assert!(hate_token.score < 0);
87    }
88
89    #[test]
90    fn empty_text_error() {
91        let result = analyze_sentiment("");
92        assert!(result.is_err());
93    }
94
95    #[test]
96    fn comparative_normalization() {
97        let short = analyze_sentiment("I love this.").unwrap();
98        let long = analyze_sentiment("I love this thing that is here today.").unwrap();
99        assert!(short.comparative.abs() > long.comparative.abs());
100    }
101}