use low_expectations::ExpectationSuite;
use prosesmasher_domain_types::{Block, CheckConfig, Document, Locale};
use serde_json::json;
use crate::check::Check;
#[derive(Debug)]
pub struct ColemanLiauCheck;
impl Check for ColemanLiauCheck {
fn id(&self) -> &'static str {
"coleman-liau"
}
fn label(&self) -> &'static str {
"Coleman-Liau Index"
}
fn supported_locales(&self) -> Option<&'static [Locale]> {
None
}
fn run(&self, doc: &Document, config: &CheckConfig, suite: &mut ExpectationSuite) {
if !config.quality.readability.enabled {
return;
}
let Some(max) = config.quality.readability.coleman_liau_max else {
return;
};
let total_words = doc.metadata.total_words;
let total_sentences = doc.metadata.total_sentences;
if total_words == 0 || total_sentences == 0 {
return;
}
let mut total_letters: usize = 0;
for section in &doc.sections {
count_letters_in_blocks(§ion.blocks, &mut total_letters);
}
let words_f = f64::from(u32::try_from(total_words).unwrap_or(u32::MAX));
let sentences_f = f64::from(u32::try_from(total_sentences).unwrap_or(u32::MAX));
let letters_f = f64::from(u32::try_from(total_letters).unwrap_or(u32::MAX));
let l = letters_f / words_f * 100.0;
let s = sentences_f / words_f * 100.0;
let score = 0.0588f64.mul_add(l, 0.296f64.mul_add(-s, -15.8));
let score_100 = f64_to_i64_x100(score);
let max_100 = f64_to_i64_x100(max);
let _result = suite
.record_custom_values(
"coleman-liau",
score_100 <= max_100,
json!({
"maximum_score_x100": max_100,
"formula": "0.0588 × L - 0.296 × S - 15.8",
}),
json!({
"score_x100": score_100,
"score": score,
"total_words": total_words,
"total_sentences": total_sentences,
"total_letters": total_letters,
}),
&[json!({
"score_x100": score_100,
"score": score,
"total_words": total_words,
"total_sentences": total_sentences,
"total_letters": total_letters,
"maximum_score_x100": max_100,
})],
)
.label("Coleman-Liau Index")
.checking("grade level index (×100)");
}
}
fn count_letters_in_blocks(blocks: &[Block], count: &mut usize) {
for block in blocks {
match block {
Block::Paragraph(p) => {
for sentence in &p.sentences {
for word in &sentence.words {
*count = count.saturating_add(
word.text.chars().filter(|c| c.is_alphabetic()).count(),
);
}
}
}
Block::BlockQuote(inner) => count_letters_in_blocks(inner, count),
Block::List(_) | Block::CodeBlock(_) => {}
}
}
}
fn f64_to_i64_x100(value: f64) -> i64 {
let rounded = (value * 100.0).round();
let clamped = rounded.clamp(f64::from(i32::MIN), f64::from(i32::MAX));
#[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
let narrow = clamped as i32;
i64::from(narrow)
}
#[cfg(test)]
#[path = "coleman_liau_tests.rs"]
mod tests;