use super::common::syllable::{SyllableCondition, SyllableExpr};
use super::ipa_syllable;
const VOWELS: [char; 5] = ['a', 'e', 'i', 'o', 'u'];
const CONSONANTS: [char; 21] = [
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x',
'y', 'z',
];
const VALID_ONSETS: &[&str] = &[
"b", "c", "d", "f", "g", "h", "j", "k", "l", "m", "n", "p", "q", "r", "s", "t", "v", "w", "x",
"y", "z", "bl", "br", "ch", "cl", "cr", "dr", "dw", "fl", "fr", "gh", "gl", "gn", "gr", "kn", "ph", "pl",
"pr", "qu", "sc", "sh", "sk", "sl", "sm", "sn", "sp", "sq", "st", "sw", "th", "tr", "tw", "wh",
"wr", "scr", "shr", "spl", "spr", "squ", "str", "thr",
];
#[inline]
fn is_vowel(c: char) -> bool {
let lower = c.to_ascii_lowercase();
VOWELS.contains(&lower)
}
#[inline]
fn is_consonant(c: char) -> bool {
let lower = c.to_ascii_lowercase();
CONSONANTS.contains(&lower)
}
fn is_y_vowel(chars: &[char], pos: usize) -> bool {
let c = chars[pos].to_ascii_lowercase();
if c != 'y' {
return false;
}
let prev_is_vowel = pos > 0 && is_vowel(chars[pos - 1]);
if prev_is_vowel {
return false;
}
let next_is_vowel = pos + 1 < chars.len() && is_vowel(chars[pos + 1]);
!next_is_vowel
}
fn is_vowel_at(chars: &[char], pos: usize) -> bool {
if pos >= chars.len() {
return false;
}
is_vowel(chars[pos]) || is_y_vowel(chars, pos)
}
fn is_valid_onset(cluster: &str) -> bool {
let lower = cluster.to_ascii_lowercase();
VALID_ONSETS.contains(&lower.as_str())
}
fn find_vowel_positions(chars: &[char]) -> Vec<usize> {
chars
.iter()
.enumerate()
.filter(|(i, _)| is_vowel_at(chars, *i))
.map(|(i, _)| i)
.collect()
}
pub fn syllable_count(word: &str) -> usize {
let chars: Vec<char> = word.chars().collect();
if chars.is_empty() {
return 0;
}
let mut count = 0;
let mut prev_vowel = false;
for i in 0..chars.len() {
let curr_vowel = is_vowel_at(&chars, i);
if curr_vowel && !prev_vowel {
count += 1;
}
prev_vowel = curr_vowel;
}
if chars.len() > 2 {
let last = chars[chars.len() - 1].to_ascii_lowercase();
let second_last = chars[chars.len() - 2].to_ascii_lowercase();
if last == 'e' && is_consonant(second_last) && count > 1 {
if second_last != 'l' && second_last != 'r' {
count -= 1;
}
}
}
count.max(1)
}
pub fn syllable_boundaries(word: &str) -> Vec<usize> {
let chars: Vec<char> = word.chars().collect();
if chars.is_empty() {
return vec![];
}
let vowel_positions = find_vowel_positions(&chars);
if vowel_positions.is_empty() {
return vec![0];
}
let mut boundaries = vec![0];
for i in 0..vowel_positions.len().saturating_sub(1) {
let v1 = vowel_positions[i];
let v2 = vowel_positions[i + 1];
let consonant_start = v1 + 1;
let consonant_end = v2;
if consonant_start >= consonant_end {
continue;
}
let consonants: String = chars[consonant_start..consonant_end]
.iter()
.collect::<String>()
.to_ascii_lowercase();
let boundary_pos = find_onset_boundary(&consonants, consonant_start);
boundaries.push(boundary_pos);
}
boundaries
}
fn find_onset_boundary(consonants: &str, start_pos: usize) -> usize {
let len = consonants.len();
for onset_len in (1..=len.min(3)).rev() {
let potential_onset = &consonants[len - onset_len..];
if is_valid_onset(potential_onset) {
return start_pos + len - onset_len;
}
}
if len > 1 {
start_pos + len - 1
} else {
start_pos
}
}
pub fn is_open_syllable(word: &str, vowel_pos: usize) -> bool {
let chars: Vec<char> = word.chars().collect();
if vowel_pos >= chars.len() || !is_vowel_at(&chars, vowel_pos) {
return false;
}
let boundaries = syllable_boundaries(word);
let num_syllables = boundaries.len();
let syllable_idx = boundaries
.iter()
.enumerate()
.rev()
.find(|(_, &b)| b <= vowel_pos)
.map(|(i, _)| i)
.unwrap_or(0);
let syllable_end = if syllable_idx + 1 < num_syllables {
boundaries[syllable_idx + 1]
} else {
chars.len()
};
if syllable_end == vowel_pos + 1 {
return true;
}
for i in (vowel_pos + 1)..syllable_end {
if is_consonant(chars[i]) {
return false;
}
}
true
}
pub fn is_before_doubled_consonant(word: &str, vowel_pos: usize) -> bool {
let chars: Vec<char> = word.chars().collect();
if vowel_pos >= chars.len() - 2 {
return false;
}
let next1 = chars[vowel_pos + 1].to_ascii_lowercase();
let next2 = chars[vowel_pos + 2].to_ascii_lowercase();
is_consonant(next1) && next1 == next2
}
pub fn is_final_syllable(word: &str, pos: usize) -> bool {
let boundaries = syllable_boundaries(word);
if boundaries.is_empty() {
return true;
}
let last_boundary = boundaries[boundaries.len() - 1];
pos >= last_boundary
}
pub fn is_initial_syllable(word: &str, pos: usize) -> bool {
let boundaries = syllable_boundaries(word);
if boundaries.len() <= 1 {
return true;
}
pos < boundaries[1]
}
pub fn evaluate_syllable_condition(
condition: &SyllableCondition,
word: &str,
match_pos: usize,
) -> bool {
match condition {
SyllableCondition::Monosyllable => syllable_count(word) == 1,
SyllableCondition::Polysyllable => syllable_count(word) > 1,
SyllableCondition::OpenSyllable => is_open_syllable(word, match_pos),
SyllableCondition::ClosedSyllable => !is_open_syllable(word, match_pos),
SyllableCondition::FinalSyllable => is_final_syllable(word, match_pos),
SyllableCondition::InitialSyllable => is_initial_syllable(word, match_pos),
}
}
pub fn evaluate_syllable_expr(expr: &SyllableExpr, word: &str, match_pos: usize) -> bool {
match expr {
SyllableExpr::Cond(cond) => evaluate_syllable_condition(cond, word, match_pos),
SyllableExpr::And(left, right) => {
evaluate_syllable_expr(left, word, match_pos)
&& evaluate_syllable_expr(right, word, match_pos)
}
SyllableExpr::Or(left, right) => {
evaluate_syllable_expr(left, word, match_pos)
|| evaluate_syllable_expr(right, word, match_pos)
}
SyllableExpr::Not(inner) => !evaluate_syllable_expr(inner, word, match_pos),
}
}
pub fn evaluate_syllable_condition_ipa(
condition: &SyllableCondition,
ipa: &str,
match_pos: usize,
) -> bool {
match condition {
SyllableCondition::Monosyllable => ipa_syllable::ipa_syllable_count(ipa) == 1,
SyllableCondition::Polysyllable => ipa_syllable::ipa_syllable_count(ipa) > 1,
SyllableCondition::OpenSyllable => ipa_syllable::is_open_syllable(ipa, match_pos),
SyllableCondition::ClosedSyllable => !ipa_syllable::is_open_syllable(ipa, match_pos),
SyllableCondition::FinalSyllable => ipa_syllable::is_final_syllable(ipa, match_pos),
SyllableCondition::InitialSyllable => ipa_syllable::is_initial_syllable(ipa, match_pos),
}
}
pub fn evaluate_syllable_expr_ipa(expr: &SyllableExpr, ipa: &str, match_pos: usize) -> bool {
match expr {
SyllableExpr::Cond(cond) => evaluate_syllable_condition_ipa(cond, ipa, match_pos),
SyllableExpr::And(left, right) => {
evaluate_syllable_expr_ipa(left, ipa, match_pos)
&& evaluate_syllable_expr_ipa(right, ipa, match_pos)
}
SyllableExpr::Or(left, right) => {
evaluate_syllable_expr_ipa(left, ipa, match_pos)
|| evaluate_syllable_expr_ipa(right, ipa, match_pos)
}
SyllableExpr::Not(inner) => !evaluate_syllable_expr_ipa(inner, ipa, match_pos),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_syllable_count_monosyllables() {
assert_eq!(syllable_count("cat"), 1);
assert_eq!(syllable_count("dog"), 1);
assert_eq!(syllable_count("run"), 1);
assert_eq!(syllable_count("fly"), 1);
assert_eq!(syllable_count("sky"), 1);
assert_eq!(syllable_count("the"), 1);
}
#[test]
fn test_syllable_count_disyllables() {
assert_eq!(syllable_count("happy"), 2);
assert_eq!(syllable_count("water"), 2);
assert_eq!(syllable_count("butter"), 2);
assert_eq!(syllable_count("running"), 2);
assert_eq!(syllable_count("after"), 2);
}
#[test]
fn test_syllable_count_trisyllables() {
assert_eq!(syllable_count("wonderful"), 3);
assert_eq!(syllable_count("example"), 3);
assert_eq!(syllable_count("computer"), 3);
}
#[test]
fn test_syllable_count_polysyllables() {
assert_eq!(syllable_count("understanding"), 4);
assert_eq!(syllable_count("communication"), 5);
}
#[test]
fn test_syllable_count_silent_e() {
assert_eq!(syllable_count("make"), 1);
assert_eq!(syllable_count("time"), 1);
assert_eq!(syllable_count("home"), 1);
assert_eq!(syllable_count("cute"), 1);
}
#[test]
fn test_syllable_count_y_as_vowel() {
assert_eq!(syllable_count("gym"), 1); assert_eq!(syllable_count("myth"), 1);
assert_eq!(syllable_count("fly"), 1);
assert_eq!(syllable_count("cry"), 1);
assert_eq!(syllable_count("fry"), 1);
assert_eq!(syllable_count("rhythm"), 1);
}
#[test]
fn test_syllable_count_vowel_digraphs() {
assert_eq!(syllable_count("boat"), 1); assert_eq!(syllable_count("meat"), 1); assert_eq!(syllable_count("rain"), 1); }
#[test]
fn test_syllable_count_empty_and_edge_cases() {
assert_eq!(syllable_count(""), 0);
assert_eq!(syllable_count("a"), 1);
assert_eq!(syllable_count("I"), 1);
}
#[test]
fn test_syllable_boundaries_simple() {
let bounds = syllable_boundaries("happy");
assert_eq!(bounds.len(), 2);
assert_eq!(bounds[0], 0); assert!(bounds[1] >= 2 && bounds[1] <= 3); }
#[test]
fn test_syllable_boundaries_monosyllable() {
let bounds = syllable_boundaries("cat");
assert_eq!(bounds.len(), 1);
assert_eq!(bounds[0], 0);
}
#[test]
fn test_is_open_syllable_true() {
assert!(is_open_syllable("be", 1));
assert!(is_open_syllable("go", 1));
}
#[test]
fn test_is_open_syllable_false() {
assert!(!is_open_syllable("cat", 1));
assert!(!is_open_syllable("bit", 1));
}
#[test]
fn test_is_before_doubled_consonant_true() {
assert!(is_before_doubled_consonant("butter", 1)); assert!(is_before_doubled_consonant("happy", 1)); assert!(is_before_doubled_consonant("hello", 1)); }
#[test]
fn test_is_before_doubled_consonant_false() {
assert!(!is_before_doubled_consonant("water", 1)); assert!(!is_before_doubled_consonant("cat", 1)); }
#[test]
fn test_is_final_syllable() {
assert!(is_final_syllable("happy", 4));
assert!(!is_final_syllable("happy", 0));
assert!(is_final_syllable("cat", 0));
assert!(is_final_syllable("cat", 2));
}
#[test]
fn test_is_initial_syllable() {
assert!(is_initial_syllable("happy", 0));
assert!(!is_initial_syllable("happy", 4));
assert!(is_initial_syllable("cat", 0));
assert!(is_initial_syllable("cat", 2));
}
#[test]
fn test_is_y_vowel() {
let chars: Vec<char> = "gym".chars().collect();
assert!(is_y_vowel(&chars, 1));
let chars: Vec<char> = "fly".chars().collect();
assert!(is_y_vowel(&chars, 2));
let chars: Vec<char> = "yes".chars().collect();
assert!(!is_y_vowel(&chars, 0));
let chars: Vec<char> = "day".chars().collect();
assert!(!is_y_vowel(&chars, 2)); }
#[test]
fn test_is_valid_onset() {
assert!(is_valid_onset("b"));
assert!(is_valid_onset("bl"));
assert!(is_valid_onset("str"));
assert!(is_valid_onset("thr"));
assert!(!is_valid_onset("bk")); assert!(!is_valid_onset("ng")); }
#[test]
fn test_evaluate_syllable_condition_monosyllable() {
assert!(evaluate_syllable_condition(
&SyllableCondition::Monosyllable,
"fly",
0
));
assert!(evaluate_syllable_condition(
&SyllableCondition::Monosyllable,
"cat",
0
));
assert!(!evaluate_syllable_condition(
&SyllableCondition::Monosyllable,
"happy",
0
));
assert!(!evaluate_syllable_condition(
&SyllableCondition::Monosyllable,
"running",
0
));
}
#[test]
fn test_evaluate_syllable_condition_polysyllable() {
assert!(!evaluate_syllable_condition(
&SyllableCondition::Polysyllable,
"fly",
0
));
assert!(!evaluate_syllable_condition(
&SyllableCondition::Polysyllable,
"cat",
0
));
assert!(evaluate_syllable_condition(
&SyllableCondition::Polysyllable,
"happy",
0
));
assert!(evaluate_syllable_condition(
&SyllableCondition::Polysyllable,
"running",
0
));
}
#[test]
fn test_evaluate_syllable_condition_initial_syllable() {
assert!(evaluate_syllable_condition(
&SyllableCondition::InitialSyllable,
"happy",
0
));
assert!(evaluate_syllable_condition(
&SyllableCondition::InitialSyllable,
"happy",
1
));
assert!(!evaluate_syllable_condition(
&SyllableCondition::InitialSyllable,
"happy",
4
));
}
#[test]
fn test_evaluate_syllable_condition_final_syllable() {
assert!(!evaluate_syllable_condition(
&SyllableCondition::FinalSyllable,
"happy",
0
));
assert!(evaluate_syllable_condition(
&SyllableCondition::FinalSyllable,
"happy",
4
));
assert!(evaluate_syllable_condition(
&SyllableCondition::FinalSyllable,
"cat",
0
));
}
#[test]
fn test_evaluate_syllable_expr_simple() {
let mono = SyllableExpr::cond(SyllableCondition::Monosyllable);
assert!(evaluate_syllable_expr(&mono, "fly", 0));
assert!(!evaluate_syllable_expr(&mono, "running", 0));
}
#[test]
fn test_evaluate_syllable_expr_not() {
let not_mono = SyllableExpr::not(SyllableExpr::cond(SyllableCondition::Monosyllable));
assert!(!evaluate_syllable_expr(¬_mono, "fly", 0));
assert!(evaluate_syllable_expr(¬_mono, "running", 0));
}
#[test]
fn test_evaluate_syllable_expr_and() {
let expr = SyllableExpr::and(
SyllableExpr::cond(SyllableCondition::Polysyllable),
SyllableExpr::cond(SyllableCondition::FinalSyllable),
);
assert!(evaluate_syllable_expr(&expr, "happy", 4));
assert!(!evaluate_syllable_expr(&expr, "happy", 0));
assert!(!evaluate_syllable_expr(&expr, "cat", 0));
}
#[test]
fn test_evaluate_syllable_expr_or() {
let expr = SyllableExpr::or(
SyllableExpr::cond(SyllableCondition::Monosyllable),
SyllableExpr::cond(SyllableCondition::InitialSyllable),
);
assert!(evaluate_syllable_expr(&expr, "fly", 0));
assert!(evaluate_syllable_expr(&expr, "happy", 0));
assert!(!evaluate_syllable_expr(&expr, "happy", 4));
}
#[test]
fn test_evaluate_syllable_expr_complex() {
let expr = SyllableExpr::and(
SyllableExpr::or(
SyllableExpr::cond(SyllableCondition::Monosyllable),
SyllableExpr::cond(SyllableCondition::InitialSyllable),
),
SyllableExpr::not(SyllableExpr::cond(SyllableCondition::FinalSyllable)),
);
assert!(!evaluate_syllable_expr(&expr, "cat", 0));
assert!(evaluate_syllable_expr(&expr, "happy", 0));
assert!(!evaluate_syllable_expr(&expr, "happy", 4));
}
#[test]
fn test_evaluate_syllable_condition_ipa_monosyllable() {
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Monosyllable,
"kæt",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Monosyllable,
"dɔg",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Monosyllable,
"flaɪ",
0
));
assert!(!evaluate_syllable_condition_ipa(
&SyllableCondition::Monosyllable,
"hæpi",
0
));
assert!(!evaluate_syllable_condition_ipa(
&SyllableCondition::Monosyllable,
"wɔːtər",
0
));
}
#[test]
fn test_evaluate_syllable_condition_ipa_polysyllable() {
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Polysyllable,
"hæpi",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Polysyllable,
"wɔːtər",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Polysyllable,
"kamal",
0
));
assert!(!evaluate_syllable_condition_ipa(
&SyllableCondition::Polysyllable,
"kæt",
0
));
assert!(!evaluate_syllable_condition_ipa(
&SyllableCondition::Polysyllable,
"flaɪ",
0
));
}
#[test]
fn test_evaluate_syllable_condition_ipa_final_syllable() {
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::FinalSyllable,
"hæpi",
3
));
assert!(!evaluate_syllable_condition_ipa(
&SyllableCondition::FinalSyllable,
"hæpi",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::FinalSyllable,
"kæt",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::FinalSyllable,
"kæt",
2
));
}
#[test]
fn test_evaluate_syllable_condition_ipa_initial_syllable() {
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::InitialSyllable,
"hæpi",
0
));
assert!(!evaluate_syllable_condition_ipa(
&SyllableCondition::InitialSyllable,
"hæpi",
3
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::InitialSyllable,
"kæt",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::InitialSyllable,
"kæt",
2
));
}
#[test]
fn test_evaluate_syllable_expr_ipa_compound() {
let expr = SyllableExpr::and(
SyllableExpr::not(SyllableExpr::cond(SyllableCondition::Monosyllable)),
SyllableExpr::cond(SyllableCondition::FinalSyllable),
);
assert!(evaluate_syllable_expr_ipa(&expr, "kamal", 4));
assert!(!evaluate_syllable_expr_ipa(&expr, "kamal", 0));
assert!(!evaluate_syllable_expr_ipa(&expr, "kæt", 2));
}
#[test]
fn test_evaluate_syllable_expr_ipa_with_length_markers() {
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Monosyllable,
"biː",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Monosyllable,
"siː",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Polysyllable,
"wɔːtər",
0
));
}
#[test]
fn test_evaluate_syllable_expr_ipa_with_explicit_boundaries() {
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Polysyllable,
"hæp.i",
0
));
assert!(evaluate_syllable_condition_ipa(
&SyllableCondition::Polysyllable,
"bju.tɪ.fəl",
0
));
}
}