readsight 1.0.0

Multilingual readability library — 86 languages, 17 formulas, TeX-based syllable counting via the Frank M. Liang algorithm.
Documentation
//! The five universal formulas: Gunning Fog, SMOG, Coleman-Liau, ARI, LIX.

use crate::formula::{
    grade_clamp, inputs, round_to, Formula, FormulaResult, GradeLevelInterpretation,
};
use crate::language::language::value_as_f64;
use crate::language::Language;
use crate::text::TextStatistics;

fn wf(stats: &TextStatistics) -> f64 {
    stats.word_count.max(1) as f64
}
fn sf(stats: &TextStatistics) -> f64 {
    stats.sentence_count.max(1) as f64
}

/// Gunning Fog Index.
pub struct GunningFog;

impl Formula for GunningFog {
    fn name(&self) -> &'static str {
        "gunning_fog"
    }
    fn description(&self) -> &'static str {
        "Gunning Fog Index - estimates years of education needed to understand text."
    }
    fn supported_languages(&self) -> &'static [&'static str] {
        &["*"]
    }
    fn calculate(&self, stats: &TextStatistics, language: &Language) -> FormulaResult {
        let poly_pct = if stats.word_count > 0 {
            (stats.polysyllable_count as f64 / stats.word_count as f64) * 100.0
        } else {
            0.0
        };
        let score = 0.4 * (stats.average_words_per_sentence + poly_pct);
        FormulaResult {
            formula_name: self.name().to_string(),
            language_code: language.code.clone(),
            score: round_to(score, 1),
            grade_level: Some(grade_clamp(score, 0.0, 19.0)),
            interpretation: interpret_fog(score).to_string(),
            inputs: inputs(&[
                ("asl", stats.average_words_per_sentence),
                ("polysyllablePct", poly_pct),
                ("polysyllableCount", stats.polysyllable_count as f64),
                ("wordCount", stats.word_count as f64),
            ]),
        }
    }
}

fn interpret_fog(score: f64) -> &'static str {
    if score < 6.0 {
        "Very Easy"
    } else if score < 8.0 {
        "Easy"
    } else if score < 12.0 {
        "Standard"
    } else if score < 14.0 {
        "Hard"
    } else if score < 17.0 {
        "Very Hard"
    } else {
        "Extremely Hard"
    }
}

/// SMOG Index.
pub struct SmogIndex;

impl Formula for SmogIndex {
    fn name(&self) -> &'static str {
        "smog"
    }
    fn description(&self) -> &'static str {
        "SMOG Index - Simple Measure of Gobbledygook. Estimates years of education needed."
    }
    fn supported_languages(&self) -> &'static [&'static str] {
        &["*"]
    }
    fn calculate(&self, stats: &TextStatistics, language: &Language) -> FormulaResult {
        let s = sf(stats);
        let score = 1.0430 * (stats.polysyllable_count as f64 * (30.0 / s)).sqrt() + 3.1291;
        FormulaResult {
            formula_name: self.name().to_string(),
            language_code: language.code.clone(),
            score: round_to(score, 1),
            grade_level: Some(grade_clamp(score, 0.0, 18.0)),
            interpretation: GradeLevelInterpretation::for_score(score).to_string(),
            inputs: inputs(&[
                ("polysyllableCount", stats.polysyllable_count as f64),
                ("sentenceCount", stats.sentence_count as f64),
            ]),
        }
    }
}

/// Coleman-Liau Index.
pub struct ColemanLiau;

impl Formula for ColemanLiau {
    fn name(&self) -> &'static str {
        "coleman_liau"
    }
    fn description(&self) -> &'static str {
        "Coleman-Liau Index - character-based readability formula (no syllable counting needed)."
    }
    fn supported_languages(&self) -> &'static [&'static str] {
        &["*"]
    }
    fn calculate(&self, stats: &TextStatistics, language: &Language) -> FormulaResult {
        let w = wf(stats);
        let s = sf(stats);
        let lv = (stats.letter_count as f64 / w) * 100.0;
        let sv = (s / w) * 100.0;
        let score = 0.0588 * lv - 0.296 * sv - 15.8;
        FormulaResult {
            formula_name: self.name().to_string(),
            language_code: language.code.clone(),
            score: round_to(score, 1),
            grade_level: Some(grade_clamp(score, 0.0, 18.0)),
            interpretation: GradeLevelInterpretation::for_score(score).to_string(),
            inputs: inputs(&[
                ("L", round_to(lv, 2)),
                ("S", round_to(sv, 2)),
                ("letterCount", stats.letter_count as f64),
                ("wordCount", stats.word_count as f64),
                ("sentenceCount", stats.sentence_count as f64),
            ]),
        }
    }
}

/// Automated Readability Index.
pub struct AutomatedReadabilityIndex;

impl Formula for AutomatedReadabilityIndex {
    fn name(&self) -> &'static str {
        "ari"
    }
    fn description(&self) -> &'static str {
        "Automated Readability Index - character-based formula. Works for all alphabetic languages."
    }
    fn supported_languages(&self) -> &'static [&'static str] {
        &["*"]
    }
    fn calculate(&self, stats: &TextStatistics, language: &Language) -> FormulaResult {
        let w = wf(stats);
        let s = sf(stats);
        let score = 4.71 * (stats.letter_count as f64 / w) + 0.5 * (w / s) - 21.43;
        FormulaResult {
            formula_name: self.name().to_string(),
            language_code: language.code.clone(),
            score: round_to(score, 1),
            grade_level: Some(grade_clamp(score, 0.0, 18.0)),
            interpretation: GradeLevelInterpretation::for_score(score).to_string(),
            inputs: inputs(&[
                ("charsPerWord", round_to(stats.letter_count as f64 / w, 2)),
                ("wordsPerSentence", round_to(w / s, 2)),
            ]),
        }
    }
}

/// LIX (Läsbarhetsindex).
pub struct Lix;

impl Formula for Lix {
    fn name(&self) -> &'static str {
        "lix"
    }
    fn description(&self) -> &'static str {
        "LIX (Läsbarhetsindex) - Scandinavian readability formula. Language-independent, uses letter count."
    }
    fn supported_languages(&self) -> &'static [&'static str] {
        &["*"]
    }
    fn calculate(&self, stats: &TextStatistics, language: &Language) -> FormulaResult {
        let threshold = language
            .get_formula_config(self.name())
            .and_then(|cfg| cfg.get("longWordThreshold"))
            .and_then(value_as_f64)
            .map(|v| v as i64)
            .unwrap_or(6);
        let long_pct = if stats.word_count > 0 {
            (stats.long_word_count as f64 / stats.word_count as f64) * 100.0
        } else {
            0.0
        };
        let score = stats.average_words_per_sentence + long_pct;
        FormulaResult {
            formula_name: self.name().to_string(),
            language_code: language.code.clone(),
            score: round_to(score, 2),
            grade_level: None,
            interpretation: interpret_lix(score).to_string(),
            inputs: inputs(&[
                ("asl", stats.average_words_per_sentence),
                ("longWordPct", round_to(long_pct, 2)),
                ("threshold", threshold as f64),
                ("longWordCount", stats.long_word_count as f64),
                ("wordCount", stats.word_count as f64),
            ]),
        }
    }
}

fn interpret_lix(score: f64) -> &'static str {
    if score < 25.0 {
        "Children's Books"
    } else if score < 30.0 {
        "Simple Texts"
    } else if score < 40.0 {
        "Normal / Fiction"
    } else if score < 50.0 {
        "Factual Information"
    } else if score < 60.0 {
        "Specialized Texts"
    } else {
        "Research / Advanced"
    }
}