use crate::mood::EmotionalState;
use crate::sentiment::{self, SentimentConfig, SentimentResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SentimentMonitor {
buffer: String,
scale: f32,
results: Vec<SentimentResult>,
config: SentimentConfig,
}
impl SentimentMonitor {
#[must_use]
pub fn new(scale: f32) -> Self {
Self {
buffer: String::new(),
scale: scale.clamp(0.0, 1.0),
results: Vec::new(),
config: SentimentConfig::default(),
}
}
#[must_use]
pub fn with_config(scale: f32, config: SentimentConfig) -> Self {
Self {
buffer: String::new(),
scale: scale.clamp(0.0, 1.0),
results: Vec::new(),
config,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn feed(&mut self, chunk: &str) -> Vec<SentimentResult> {
self.buffer.push_str(chunk);
self.drain_sentences()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn feed_and_apply(&mut self, chunk: &str, state: &mut EmotionalState) {
let results = self.feed(chunk);
for result in &results {
self.apply_to_mood(state, result);
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn flush(&mut self) -> Vec<SentimentResult> {
let trimmed_is_empty = self.buffer.trim().is_empty();
if trimmed_is_empty {
self.buffer.clear();
return Vec::new();
}
let result = sentiment::analyze_with_config(self.buffer.trim(), &self.config);
self.buffer.clear();
self.results.push(result.clone());
vec![result]
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn apply_to_mood(&self, state: &mut EmotionalState, result: &SentimentResult) {
for &(emotion, intensity) in &result.emotions {
state.stimulate(emotion, intensity * self.scale);
}
}
#[must_use]
pub fn results(&self) -> &[SentimentResult] {
&self.results
}
#[must_use]
pub fn sentence_count(&self) -> usize {
self.results.len()
}
#[must_use]
pub fn average_valence(&self) -> f32 {
if self.results.is_empty() {
return 0.0;
}
let sum: f32 = self.results.iter().map(|r| r.valence).sum();
sum / self.results.len() as f32
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn summary(&self) -> MonitorSummary {
let count = self.results.len();
let positive = self.results.iter().filter(|r| r.is_positive()).count();
let negative = self.results.iter().filter(|r| r.is_negative()).count();
let neutral = count - positive - negative;
MonitorSummary {
sentence_count: count,
positive_count: positive,
negative_count: negative,
neutral_count: neutral,
average_valence: self.average_valence(),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn reset(&mut self) {
self.buffer.clear();
self.results.clear();
}
fn drain_sentences(&mut self) -> Vec<SentimentResult> {
let mut results = Vec::new();
loop {
let boundary = self.buffer.find(['.', '!', '?']);
match boundary {
Some(pos) => {
let sentence: String = self.buffer.drain(..=pos).collect();
let trimmed = sentence.trim();
if !trimmed.is_empty() {
let result = sentiment::analyze_with_config(trimmed, &self.config);
self.results.push(result.clone());
results.push(result);
}
}
None => break,
}
}
results
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitorSummary {
pub sentence_count: usize,
pub positive_count: usize,
pub negative_count: usize,
pub neutral_count: usize,
pub average_valence: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_monitor_new() {
let m = SentimentMonitor::new(0.5);
assert_eq!(m.sentence_count(), 0);
assert!(m.results().is_empty());
}
#[test]
fn test_feed_single_sentence() {
let mut m = SentimentMonitor::new(1.0);
let results = m.feed("This is great!");
assert_eq!(results.len(), 1);
assert!(results[0].is_positive());
assert_eq!(m.sentence_count(), 1);
}
#[test]
fn test_feed_partial_then_complete() {
let mut m = SentimentMonitor::new(1.0);
assert!(m.feed("This is ").is_empty());
assert!(m.feed("wonderful ").is_empty());
let results = m.feed("work!");
assert_eq!(results.len(), 1);
assert!(results[0].is_positive());
}
#[test]
fn test_feed_multiple_sentences() {
let mut m = SentimentMonitor::new(1.0);
let results = m.feed("Great work! Terrible result.");
assert_eq!(results.len(), 2);
}
#[test]
fn test_flush_remaining() {
let mut m = SentimentMonitor::new(1.0);
m.feed("This is great but no period");
let results = m.flush();
assert_eq!(results.len(), 1);
}
#[test]
fn test_flush_empty() {
let mut m = SentimentMonitor::new(1.0);
assert!(m.flush().is_empty());
}
#[test]
fn test_feed_and_apply() {
let mut m = SentimentMonitor::new(1.0);
let mut state = EmotionalState::new();
m.feed_and_apply("This is wonderful!", &mut state);
assert!(state.mood.joy > 0.0);
}
#[test]
fn test_apply_to_mood_scaled() {
let m = SentimentMonitor::new(0.0); let mut state = EmotionalState::new();
let result = sentiment::analyze("Amazing wonderful fantastic!");
m.apply_to_mood(&mut state, &result);
assert!(state.deviation() < f32::EPSILON);
}
#[test]
fn test_average_valence() {
let mut m = SentimentMonitor::new(1.0);
m.feed("Great! Terrible.");
let avg = m.average_valence();
assert!(avg.abs() < 0.5);
}
#[test]
fn test_average_valence_empty() {
let m = SentimentMonitor::new(1.0);
assert!(m.average_valence().abs() < f32::EPSILON);
}
#[test]
fn test_summary() {
let mut m = SentimentMonitor::new(1.0);
m.feed("Amazing! Terrible! Whatever.");
let s = m.summary();
assert_eq!(s.sentence_count, 3);
assert!(s.positive_count >= 1);
assert!(s.negative_count >= 1);
}
#[test]
fn test_reset() {
let mut m = SentimentMonitor::new(1.0);
m.feed("Great work!");
assert_eq!(m.sentence_count(), 1);
m.reset();
assert_eq!(m.sentence_count(), 0);
assert!(m.results().is_empty());
}
#[test]
fn test_with_config() {
let mut config = SentimentConfig::default();
config.extra_positive.push("rad".to_string());
let mut m = SentimentMonitor::with_config(1.0, config);
let results = m.feed("This is rad!");
assert_eq!(results.len(), 1);
assert!(results[0].is_positive());
}
#[test]
fn test_summary_serde() {
let mut m = SentimentMonitor::new(1.0);
m.feed("Great!");
let s = m.summary();
let json = serde_json::to_string(&s).unwrap();
let s2: MonitorSummary = serde_json::from_str(&json).unwrap();
assert_eq!(s2.sentence_count, s.sentence_count);
}
#[test]
fn test_streaming_simulation() {
let mut m = SentimentMonitor::new(0.5);
let mut state = EmotionalState::new();
let tokens = [
"I ",
"love ",
"this ",
"project! ",
"But ",
"the ",
"bugs ",
"are ",
"terrible.",
];
for token in &tokens {
m.feed_and_apply(token, &mut state);
}
let remaining = m.flush();
for r in &remaining {
m.apply_to_mood(&mut state, r);
}
assert_eq!(m.sentence_count(), 2);
assert!(state.deviation() > 0.0);
}
}