readsight 1.0.1

Multilingual readability library — 86 languages, 17 formulas, TeX-based syllable counting via the Frank M. Liang algorithm.
Documentation
//! Orchestrates hyphenation, syllable counting, and text splitting.

use std::collections::BTreeMap;
use std::rc::Rc;

use crate::error::{Error, Result};
use crate::hyphenation::{Hyphenator, LiangHyphenator};
use crate::language::language::value_as_f64;
use crate::language::Language;
use crate::syllable::{SyllableCounter, SyllableCounterKind};

use super::splitter::TextSplitter;
use super::statistics::TextStatistics;

/// High-level analysis facade over the lower-level components.
pub struct TextAnalyzer {
    hyphenator: Rc<LiangHyphenator>,
    syllable_counter: SyllableCounterKind,
    splitter: TextSplitter,
    language: Rc<Language>,
}

impl TextAnalyzer {
    /// Assemble an analyzer from its components.
    pub fn new(
        hyphenator: Rc<LiangHyphenator>,
        syllable_counter: SyllableCounterKind,
        splitter: TextSplitter,
        language: Rc<Language>,
    ) -> Self {
        TextAnalyzer {
            hyphenator,
            syllable_counter,
            splitter,
            language,
        }
    }

    /// Exact TeX hyphenation split of a word.
    pub fn split_word(&self, word: &str) -> Vec<String> {
        self.hyphenator.hyphenate(word)
    }

    /// Syllable split of a word (per the language's syllable mode).
    pub fn split_syllables(&self, word: &str) -> Vec<String> {
        self.syllable_counter.split_syllables(word)
    }

    /// Syllable count of a word.
    pub fn syllable_count(&self, word: &str) -> i64 {
        self.syllable_counter.count_syllables(word)
    }

    /// Word count of a text.
    pub fn word_count(&self, text: &str) -> i64 {
        self.splitter.count_words(text)
    }

    /// Sentence count of a text.
    pub fn sentence_count(&self, text: &str) -> i64 {
        self.splitter.count_sentences(text)
    }

    /// Letter count of a text.
    pub fn letter_count(&self, text: &str) -> i64 {
        self.splitter.count_letters(text)
    }

    /// Total syllables across all words in a text.
    pub fn total_syllables(&self, text: &str) -> i64 {
        self.splitter
            .split_words(text)
            .iter()
            .map(|w| self.syllable_counter.count_syllables(w))
            .sum()
    }

    /// Mean syllables per word.
    pub fn average_syllables_per_word(&self, text: &str) -> f64 {
        let words = self.splitter.split_words(text);
        if words.is_empty() {
            return 0.0;
        }
        let total: i64 = words
            .iter()
            .map(|w| self.syllable_counter.count_syllables(w))
            .sum();
        total as f64 / words.len() as f64
    }

    /// Mean words per sentence.
    pub fn average_words_per_sentence(&self, text: &str) -> f64 {
        let word_count = self.splitter.count_words(text);
        let sentence_count = self.splitter.count_sentences(text);
        if sentence_count == 0 {
            word_count as f64
        } else {
            word_count as f64 / sentence_count as f64
        }
    }

    /// Count words with more than `n` syllables.
    ///
    /// When `count_proper_nouns` is false, words beginning with an uppercase
    /// letter are skipped.
    pub fn words_with_more_than_n_syllables(
        &self,
        text: &str,
        n: i64,
        count_proper_nouns: bool,
    ) -> i64 {
        let mut count = 0i64;
        for word in self.splitter.split_words(text) {
            if self.syllable_counter.count_syllables(&word) > n {
                let counts = count_proper_nouns
                    || word.chars().next().is_some_and(|first| {
                        first.to_string() != first.to_uppercase().collect::<String>()
                    });
                if counts {
                    count += 1;
                }
            }
        }
        count
    }

    /// Count polysyllabic words (> 2 syllables).
    pub fn polysyllable_count(&self, text: &str, count_proper_nouns: bool) -> i64 {
        self.words_with_more_than_n_syllables(text, 2, count_proper_nouns)
    }

    /// Histogram of syllable counts (ascending keys, excludes 0-syllable words).
    pub fn histogram_syllables(&self, text: &str) -> BTreeMap<i64, i64> {
        let mut histogram: BTreeMap<i64, i64> = BTreeMap::new();
        for word in self.splitter.split_words(text) {
            let syllables = self.syllable_counter.count_syllables(&word);
            if syllables > 0 {
                *histogram.entry(syllables).or_insert(0) += 1;
            }
        }
        histogram
    }

    /// Compute the full statistics bundle. Errors on empty text.
    pub fn analyze(&self, text: &str) -> Result<TextStatistics> {
        let text = text.trim();

        let words = self.splitter.split_words(text);
        let word_count = words.len() as i64;

        if word_count == 0 {
            return Err(Error::EmptyText);
        }

        let letter_count = self.splitter.count_letters(text);
        let sentence_count = self.splitter.count_sentences(text);

        let mut total_syllables = 0i64;
        let mut polysyllable_count = 0i64;
        let mut histogram: BTreeMap<i64, i64> = BTreeMap::new();

        for word in &words {
            let syllables = self.syllable_counter.count_syllables(word);
            total_syllables += syllables;
            if syllables > 2 {
                polysyllable_count += 1;
            }
            if syllables > 0 {
                *histogram.entry(syllables).or_insert(0) += 1;
            }
        }

        let sentence_count_for_average = sentence_count.max(1);

        let long_word_threshold = self
            .language
            .get_formula_config("lix")
            .and_then(|cfg| cfg.get("longWordThreshold"))
            .and_then(value_as_f64)
            .map(|v| v as i64)
            .unwrap_or(6);
        let long_word_count = self.splitter.count_long_words(text, long_word_threshold);

        Ok(TextStatistics {
            letter_count,
            word_count,
            sentence_count,
            syllable_count: total_syllables,
            polysyllable_count,
            average_syllables_per_word: total_syllables as f64 / word_count as f64,
            average_words_per_sentence: word_count as f64 / sentence_count_for_average as f64,
            long_word_count,
            syllable_histogram: histogram,
        })
    }

    /// Forward user hyphenation overrides to the underlying Liang hyphenator.
    pub fn add_hyphenations<I, K, V>(&self, hyphenations: I)
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
        self.hyphenator.add_hyphenations(hyphenations);
    }
}