Skip to main content

writing_analysis/
readability.rs

1use crate::error::{Result, WritingAnalysisError};
2use crate::utils::{compute_statistics, TextStatistics};
3
4/// Readability scores computed from text statistics.
5#[derive(Debug, Clone, PartialEq)]
6pub struct ReadabilityScores {
7    /// Flesch-Kincaid Grade Level (US school grade)
8    pub flesch_kincaid_grade: f64,
9    /// Flesch Reading Ease (0-100, higher = easier)
10    pub flesch_reading_ease: f64,
11    /// SMOG Index (years of education needed).
12    /// Note: SMOG is designed for texts with 30+ sentences. Results for shorter
13    /// texts are approximations and may be less accurate.
14    pub smog_index: f64,
15    /// Coleman-Liau Index (grade level)
16    pub coleman_liau_index: f64,
17    /// Automated Readability Index (grade level)
18    pub automated_readability_index: f64,
19}
20
21fn flesch_kincaid_grade(stats: &TextStatistics) -> f64 {
22    let words = stats.word_count as f64;
23    let sentences = stats.sentence_count as f64;
24    let syllables = stats.syllable_count as f64;
25    0.39 * (words / sentences) + 11.8 * (syllables / words) - 15.59
26}
27
28fn flesch_reading_ease(stats: &TextStatistics) -> f64 {
29    let words = stats.word_count as f64;
30    let sentences = stats.sentence_count as f64;
31    let syllables = stats.syllable_count as f64;
32    206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words)
33}
34
35fn smog_index(stats: &TextStatistics) -> f64 {
36    let polysyllables = stats.polysyllable_count as f64;
37    let sentences = stats.sentence_count as f64;
38    3.0 + (polysyllables * 30.0 / sentences).sqrt()
39}
40
41fn coleman_liau_index(stats: &TextStatistics) -> f64 {
42    let words = stats.word_count as f64;
43    let l = (stats.character_count as f64 / words) * 100.0;
44    let s = (stats.sentence_count as f64 / words) * 100.0;
45    0.0588 * l - 0.296 * s - 15.8
46}
47
48fn automated_readability_index(stats: &TextStatistics) -> f64 {
49    let words = stats.word_count as f64;
50    let sentences = stats.sentence_count as f64;
51    let characters = stats.character_count as f64;
52    4.71 * (characters / words) + 0.5 * (words / sentences) - 21.43
53}
54
55/// Analyze text readability using 5 standard formulas.
56pub fn analyze_readability(text: &str) -> Result<ReadabilityScores> {
57    let stats = compute_statistics(text);
58
59    if stats.word_count == 0 {
60        return Err(WritingAnalysisError::EmptyText);
61    }
62    if stats.word_count < 2 {
63        return Err(WritingAnalysisError::TextTooShort {
64            min_words: 2,
65            found: stats.word_count,
66        });
67    }
68    if stats.sentence_count == 0 {
69        return Err(WritingAnalysisError::NoSentences);
70    }
71
72    Ok(ReadabilityScores {
73        flesch_kincaid_grade: flesch_kincaid_grade(&stats),
74        flesch_reading_ease: flesch_reading_ease(&stats),
75        smog_index: smog_index(&stats),
76        coleman_liau_index: coleman_liau_index(&stats),
77        automated_readability_index: automated_readability_index(&stats),
78    })
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn readability_simple_text() {
87        let scores = analyze_readability("The cat sat on the mat.").unwrap();
88        assert!(scores.flesch_kincaid_grade < 5.0);
89        assert!(scores.flesch_reading_ease > 80.0);
90    }
91
92    #[test]
93    fn readability_complex_text() {
94        let text = "The implementation of sophisticated algorithms \
95                    necessitates comprehensive understanding of computational \
96                    complexity and mathematical abstractions.";
97        let scores = analyze_readability(text).unwrap();
98        assert!(scores.flesch_kincaid_grade > 12.0);
99        assert!(scores.flesch_reading_ease < 30.0);
100    }
101
102    #[test]
103    fn readability_empty_text() {
104        let result = analyze_readability("");
105        assert!(result.is_err());
106    }
107
108    #[test]
109    fn readability_all_scores_finite() {
110        let scores = analyze_readability("Hello world. This is a test.").unwrap();
111        assert!(scores.flesch_kincaid_grade.is_finite());
112        assert!(scores.flesch_reading_ease.is_finite());
113        assert!(scores.smog_index.is_finite());
114        assert!(scores.coleman_liau_index.is_finite());
115        assert!(scores.automated_readability_index.is_finite());
116    }
117}