1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
5pub struct ReadingAnalytics {
6 pub word_count: usize,
7 pub reading_time_minutes: f32,
8 pub difficulty_score: f32,
9 pub top_keywords: Vec<(String, usize)>,
10}
11
12const STOP_WORDS: &[&str] = &[
13 "the", "and", "is", "of", "to", "in", "that", "it", "with", "for", "as", "was", "on", "are",
14 "by", "at", "an", "be", "this", "which", "from", "or", "have", "had", "has", "not", "but",
15 "what", "all", "were", "when", "we", "there", "can", "an", "your", "how", "her", "him", "his",
16 "them", "their", "into", "some", "than", "then", "now", "only", "other", "its", "also", "out",
17];
18
19impl ReadingAnalytics {
20 pub fn analyze_text(text: &str) -> Self {
22 let words: Vec<&str> = text
23 .split_whitespace()
24 .map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()))
25 .filter(|w| !w.is_empty())
26 .collect();
27
28 let word_count = words.len();
29 let reading_time_minutes = if word_count == 0 {
30 0.0
31 } else {
32 (word_count as f32 / 200.0 * 10.0).round() / 10.0
33 };
34
35 let mut freq_map = ahash::AHashMap::new();
37 let mut total_chars = 0;
38
39 for word in &words {
40 let lower = word.to_lowercase();
41 total_chars += lower.chars().count();
42 if lower.len() >= 3 && !STOP_WORDS.contains(&lower.as_str()) {
43 *freq_map.entry(lower).or_insert(0usize) += 1;
44 }
45 }
46
47 let mut top_keywords: Vec<(String, usize)> = freq_map.into_iter().collect();
48 top_keywords.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
49 top_keywords.truncate(10);
50
51 let avg_word_length = if word_count == 0 {
52 0.0
53 } else {
54 total_chars as f32 / word_count as f32
55 };
56
57 let difficulty_score = (avg_word_length * 1.5).min(10.0);
59 let difficulty_score = (difficulty_score * 10.0).round() / 10.0;
60
61 Self {
62 word_count,
63 reading_time_minutes,
64 difficulty_score,
65 top_keywords,
66 }
67 }
68}