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
}
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"
}
}
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),
]),
}
}
}
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),
]),
}
}
}
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)),
]),
}
}
}
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"
}
}