readsight 1.0.1

Multilingual readability library — 86 languages, 17 formulas, TeX-based syllable counting via the Frank M. Liang algorithm.
Documentation
//! Unit tests for the syllable layer (ported from the PHP suite).

use std::rc::Rc;

use readsight::hyphenation::{
    HyphenationExceptionsCollection, HyphenationOverride, LiangHyphenator, PatternsCollection,
};
use readsight::syllable::{
    CompositeSyllableCounter, HeuristicSyllableCounter, SyllableCounter, TexSyllableCounter,
};
use serde_json::{json, Value};

fn counter(cfg: &Value) -> HeuristicSyllableCounter {
    HeuristicSyllableCounter::new(Some(cfg))
}

fn full_config() -> Value {
    json!({
        "problemWords": {"banana": 3, "beautiful": 3, "extraordinary": 6},
        "subtractPatterns": ["cial", "tia", "cious", "eous$"],
        "addPatterns": ["ia", "io", "iu"],
        "prefixes": {"un": 1, "pre": 1, "dis": 1},
        "suffixes": {"ly": 1, "tion": 1, "ment": 1},
        "vowelPattern": "[aeiouy]"
    })
}

#[test]
fn has_rules_variants() {
    assert!(counter(&full_config()).has_rules());
    assert!(counter(&json!({
        "problemWords": {}, "subtractPatterns": ["cial"], "addPatterns": [],
        "prefixes": {}, "suffixes": {}, "vowelPattern": "[aeiouy]"
    }))
    .has_rules());
    assert!(!HeuristicSyllableCounter::new(None).has_rules());
    assert!(!counter(&json!({
        "problemWords": {}, "subtractPatterns": [], "addPatterns": [],
        "prefixes": {}, "suffixes": {}, "vowelPattern": "[aeiouy]"
    }))
    .has_rules());
}

#[test]
fn has_word_cases() {
    let c = counter(&full_config());
    assert!(c.has_word("banana"));
    assert!(c.has_word("BANANA"));
    assert!(c.has_word("Beautiful"));
    assert!(!c.has_word("computer"));
    assert!(!c.has_word(""));
    assert!(!c.has_word("   "));
}

#[test]
fn problem_words_exact_count() {
    let c = counter(&full_config());
    assert_eq!(c.count_syllables("banana"), 3);
    assert_eq!(c.count_syllables("extraordinary"), 6);
    assert_eq!(c.count_syllables("BaNaNa"), 3);
}

#[test]
fn vowel_counting_basic() {
    let c = counter(&json!({"vowelPattern": "[aeiouy]"}));
    assert_eq!(c.count_syllables("cat"), 1);
    assert_eq!(c.count_syllables("table"), 2);
    assert_eq!(c.count_syllables("computer"), 3);
}

#[test]
fn never_returns_zero_but_empty_is_zero() {
    let c = counter(&full_config());
    assert_eq!(c.count_syllables("bcd"), 1);
    assert_eq!(c.count_syllables(""), 0);
    assert_eq!(c.count_syllables("   "), 0);
}

#[test]
fn cluster_vs_individual_mode() {
    let vowels = "[аеёиоуыэюяій]";
    let cluster = counter(&json!({"vowelPattern": vowels, "vowelMode": "cluster"}));
    let individual = counter(&json!({"vowelPattern": vowels, "vowelMode": "individual"}));
    let default = counter(&json!({"vowelPattern": vowels}));

    assert_eq!(cluster.count_syllables("дыхание"), 3);
    assert_eq!(individual.count_syllables("дыхание"), 4);
    assert_eq!(default.count_syllables("дыхание"), 3); // defaults to cluster
    assert_eq!(individual.count_syllables("молоко"), 3);
}

#[test]
fn affixes_add_syllables() {
    let pre = counter(&json!({"prefixes": {"un": 1}, "vowelPattern": "[aeiouy]"}));
    assert_eq!(pre.count_syllables("undo"), 2);
    let suf = counter(&json!({"suffixes": {"ly": 1}, "vowelPattern": "[aeiouy]"}));
    assert_eq!(suf.count_syllables("slowly"), 2);
}

#[test]
fn subtract_and_add_patterns() {
    let sub = counter(&json!({"subtractPatterns": ["eous$"], "vowelPattern": "[aeiouy]"}));
    assert!(sub.count_syllables("courageous") >= 2);
    let add = counter(&json!({"addPatterns": ["ia", "io"], "vowelPattern": "[aeiouy]"}));
    assert!(add.count_syllables("radio") >= 3);
}

#[test]
fn backreference_add_pattern_compiles() {
    // en-us ships `([^aeiouy])\1l$`; ensure fancy-regex handles it.
    let c = counter(&json!({"addPatterns": ["([^aeiouy])\\1l$"], "vowelPattern": "[aeiouy]"}));
    let with = c.count_syllables("little");
    assert!(with >= 1);
}

#[test]
fn split_syllables_behaviour() {
    let c = counter(&full_config());
    assert_eq!(c.split_syllables("cat"), vec!["cat"]);
    assert_eq!(c.split_syllables(""), Vec::<String>::new());
    let word = "communication";
    assert_eq!(
        c.split_syllables(word).len() as i64,
        c.count_syllables(word)
    );
}

#[test]
fn default_vowel_pattern_when_absent() {
    let c = counter(&json!({"problemWords": {}}));
    assert_eq!(c.count_syllables("cat"), 1);
    assert_eq!(c.count_syllables("table"), 2);
}

#[test]
fn split_syllables_more_syllables_than_chars() {
    let c = counter(&json!({"vowelPattern": "[aeiou]", "vowelMode": "individual"}));
    // "aei" -> 3 syllables, 3 chars -> one part per char
    assert_eq!(c.split_syllables("aei"), vec!["a", "e", "i"]);
}

#[test]
fn non_letter_word_returns_one() {
    // After stripping non-letters the cleaned word is empty -> 1 syllable.
    let c = counter(&full_config());
    assert_eq!(c.count_syllables("123"), 1);
    assert_eq!(c.count_syllables("!!!"), 1);
}

#[test]
fn invalid_vowel_pattern_falls_back() {
    // "[z-a]" is an out-of-order range -> both vowel regexes fail to compile
    // and the counter falls back to the default `[aeiouy]`.
    let c = counter(&json!({"vowelPattern": "[z-a]"}));
    assert_eq!(c.count_syllables("cat"), 1);
    assert_eq!(c.count_syllables("table"), 2);
}

fn tex_counter() -> TexSyllableCounter {
    let mut exceptions = HyphenationExceptionsCollection::new();
    exceptions.add(HyphenationOverride::new("associate", "as-so-ci-ate"));
    let h = Rc::new(LiangHyphenator::new(
        PatternsCollection::new(),
        exceptions,
        2,
        2,
    ));
    TexSyllableCounter::new(h)
}

#[test]
fn tex_counter_delegates() {
    let tex = tex_counter();
    assert_eq!(tex.count_syllables("associate"), 4);
    assert_eq!(
        tex.split_syllables("associate"),
        vec!["as", "so", "ci", "ate"]
    );
}

#[test]
fn composite_uses_heuristic_when_rules_present() {
    let composite = CompositeSyllableCounter::new(counter(&full_config()), tex_counter());
    // heuristic has rules -> problem word wins
    assert_eq!(composite.count_syllables("banana"), 3);
    assert_eq!(composite.split_syllables("banana").len(), 3);
}

#[test]
fn composite_falls_back_to_tex_without_rules() {
    let empty = HeuristicSyllableCounter::new(None);
    let composite = CompositeSyllableCounter::new(empty, tex_counter());
    assert_eq!(composite.count_syllables("associate"), 4);
    assert_eq!(
        composite.split_syllables("associate"),
        vec!["as", "so", "ci", "ate"]
    );
}