Skip to main content

ebook_rs/
analytics.rs

1use serde::{Deserialize, Serialize};
2
3/// Structural NLP Reading Analytics for a chapter or section.
4#[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    /// Calculate structural analytics and top keywords from plain text.
21    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        // Compute top frequency keywords (excluding common stopwords)
36        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        // Flesch-Kincaid style difficulty score indicator (0.0 easy to 10.0 complex)
58        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}