const LENGTH_MARKERS: &[char] = &[
'ː', 'ˑ', '˘', ];
const STRESS_MARKERS: &[char] = &[
'ˈ', 'ˌ', ];
const SYLLABLE_BOUNDARY: char = '.';
#[inline]
pub fn is_ipa_vowel(c: char) -> bool {
matches!(
c,
'a' | 'e' | 'i' | 'o' | 'u' |
'A' | 'E' | 'I' | 'O' | 'U' |
'ə' | 'ɪ' | 'ʊ' | 'ɛ' | 'ɔ' |
'æ' | 'ʌ' | 'ɑ' | 'ɒ' | 'ɜ' | 'ɐ' |
'y' | 'ʏ' | 'ø' | 'œ' | 'ɶ' |
'ɨ' | 'ʉ' | 'ɘ' | 'ɵ' | 'ɞ' |
'ɯ' | 'ɤ' |
'ɚ' | 'ɝ'
)
}
#[inline]
pub fn is_length_marker(c: char) -> bool {
LENGTH_MARKERS.contains(&c)
}
#[inline]
pub fn is_stress_marker(c: char) -> bool {
STRESS_MARKERS.contains(&c)
}
#[inline]
pub fn is_syllable_boundary(c: char) -> bool {
c == SYLLABLE_BOUNDARY
}
#[inline]
pub fn is_ipa_consonant(c: char) -> bool {
!is_ipa_vowel(c) && !is_length_marker(c) && !is_stress_marker(c) && !is_syllable_boundary(c)
}
pub fn ipa_syllable_count(ipa: &str) -> usize {
if ipa.is_empty() {
return 0;
}
let chars: Vec<char> = ipa.chars().collect();
let boundary_count = chars.iter().filter(|&&c| c == SYLLABLE_BOUNDARY).count();
if boundary_count > 0 {
return boundary_count + 1;
}
let mut count = 0;
let mut in_vowel_group = false;
for &c in &chars {
if is_ipa_vowel(c) {
if !in_vowel_group {
count += 1;
in_vowel_group = true;
}
} else if !is_length_marker(c) && !is_stress_marker(c) {
in_vowel_group = false;
}
}
count.max(1)
}
pub fn ipa_syllable_boundaries(ipa: &str) -> Vec<usize> {
if ipa.is_empty() {
return vec![];
}
let chars: Vec<char> = ipa.chars().collect();
let mut boundaries = vec![0];
for (i, &c) in chars.iter().enumerate() {
if c == SYLLABLE_BOUNDARY && i + 1 < chars.len() {
boundaries.push(i + 1);
}
}
if boundaries.len() > 1 {
return boundaries;
}
let vowel_positions: Vec<usize> = chars
.iter()
.enumerate()
.filter(|(_, &c)| is_ipa_vowel(c))
.map(|(i, _)| i)
.collect();
for window in vowel_positions.windows(2) {
let v1 = window[0];
let v2 = window[1];
let mut v1_end = v1;
while v1_end + 1 < v2
&& (is_ipa_vowel(chars[v1_end + 1]) || is_length_marker(chars[v1_end + 1]))
{
v1_end += 1;
}
let consonant_start = v1_end + 1;
let consonant_end = v2;
if consonant_start >= consonant_end {
continue;
}
boundaries.push(consonant_start);
}
boundaries.sort();
boundaries.dedup();
boundaries
}
pub fn is_final_syllable(ipa: &str, pos: usize) -> bool {
let boundaries = ipa_syllable_boundaries(ipa);
if boundaries.is_empty() {
return true;
}
let last_boundary = *boundaries.last().expect("non-empty checked above");
pos >= last_boundary
}
pub fn is_initial_syllable(ipa: &str, pos: usize) -> bool {
let boundaries = ipa_syllable_boundaries(ipa);
if boundaries.len() <= 1 {
return true;
}
pos < boundaries[1]
}
pub fn is_open_syllable(ipa: &str, pos: usize) -> bool {
let chars: Vec<char> = ipa.chars().collect();
let boundaries = ipa_syllable_boundaries(ipa);
if boundaries.is_empty() || pos >= chars.len() {
return false;
}
let syllable_idx = boundaries
.iter()
.enumerate()
.rev()
.find(|(_, &b)| b <= pos)
.map(|(i, _)| i)
.unwrap_or(0);
let syllable_end = if syllable_idx + 1 < boundaries.len() {
boundaries[syllable_idx + 1]
} else {
chars.len()
};
let mut check_pos = syllable_end.saturating_sub(1);
while check_pos > 0 && is_length_marker(chars[check_pos]) {
check_pos -= 1;
}
is_ipa_vowel(chars[check_pos])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_ipa_vowel_basic() {
assert!(is_ipa_vowel('a'));
assert!(is_ipa_vowel('e'));
assert!(is_ipa_vowel('i'));
assert!(is_ipa_vowel('o'));
assert!(is_ipa_vowel('u'));
}
#[test]
fn test_is_ipa_vowel_ipa_symbols() {
assert!(is_ipa_vowel('ə')); assert!(is_ipa_vowel('ɪ')); assert!(is_ipa_vowel('ʊ')); assert!(is_ipa_vowel('ɛ')); assert!(is_ipa_vowel('ɔ')); assert!(is_ipa_vowel('æ')); assert!(is_ipa_vowel('ʌ')); assert!(is_ipa_vowel('ɑ')); }
#[test]
fn test_is_ipa_vowel_front_rounded() {
assert!(is_ipa_vowel('y')); assert!(is_ipa_vowel('ø')); assert!(is_ipa_vowel('œ')); }
#[test]
fn test_is_ipa_vowel_consonants_false() {
assert!(!is_ipa_vowel('p'));
assert!(!is_ipa_vowel('t'));
assert!(!is_ipa_vowel('k'));
assert!(!is_ipa_vowel('ʃ'));
assert!(!is_ipa_vowel('ŋ'));
}
#[test]
fn test_syllable_count_monosyllables() {
assert_eq!(ipa_syllable_count("kæt"), 1); assert_eq!(ipa_syllable_count("dɔg"), 1); assert_eq!(ipa_syllable_count("rʌn"), 1); assert_eq!(ipa_syllable_count("flaɪ"), 1); }
#[test]
fn test_syllable_count_disyllables() {
assert_eq!(ipa_syllable_count("hæpi"), 2); assert_eq!(ipa_syllable_count("wɔːtər"), 2); assert_eq!(ipa_syllable_count("rʌnɪŋ"), 2); }
#[test]
fn test_syllable_count_trisyllables() {
assert_eq!(ipa_syllable_count("bjuːtɪfəl"), 3); assert_eq!(ipa_syllable_count("kəmpjuːtər"), 3); }
#[test]
fn test_syllable_count_with_explicit_boundaries() {
assert_eq!(ipa_syllable_count("ˈhæp.i"), 2);
assert_eq!(ipa_syllable_count("ˈbju.tɪ.fəl"), 3);
assert_eq!(ipa_syllable_count("kəm.ˈpju.tər"), 3);
}
#[test]
fn test_syllable_count_with_length_markers() {
assert_eq!(ipa_syllable_count("biː"), 1); assert_eq!(ipa_syllable_count("siː"), 1); assert_eq!(ipa_syllable_count("wɔːtər"), 2); }
#[test]
fn test_syllable_count_diphthongs() {
assert_eq!(ipa_syllable_count("haʊs"), 1); assert_eq!(ipa_syllable_count("kɔɪn"), 1); assert_eq!(ipa_syllable_count("baɪ"), 1); assert_eq!(ipa_syllable_count("goʊ"), 1); }
#[test]
fn test_syllable_count_hindi() {
assert_eq!(ipa_syllable_count("kamal"), 2); assert_eq!(ipa_syllable_count("namaste"), 3); }
#[test]
fn test_syllable_count_empty() {
assert_eq!(ipa_syllable_count(""), 0);
}
#[test]
fn test_is_final_syllable() {
assert!(is_final_syllable("hæpi", 3));
assert!(!is_final_syllable("hæpi", 0));
assert!(is_final_syllable("kæt", 0));
assert!(is_final_syllable("kæt", 2));
}
#[test]
fn test_is_initial_syllable() {
assert!(is_initial_syllable("hæpi", 0));
assert!(!is_initial_syllable("hæpi", 3));
assert!(is_initial_syllable("kæt", 0));
assert!(is_initial_syllable("kæt", 2));
}
#[test]
fn test_is_open_syllable() {
assert!(is_open_syllable("bi", 0));
assert!(!is_open_syllable("kæt", 1));
}
#[test]
fn test_is_ipa_consonant() {
assert!(is_ipa_consonant('p'));
assert!(is_ipa_consonant('t'));
assert!(is_ipa_consonant('k'));
assert!(is_ipa_consonant('ʃ'));
assert!(is_ipa_consonant('ŋ'));
assert!(is_ipa_consonant('θ'));
assert!(is_ipa_consonant('ð'));
assert!(!is_ipa_consonant('a'));
assert!(!is_ipa_consonant('ə'));
assert!(!is_ipa_consonant('ː'));
assert!(!is_ipa_consonant('ˈ'));
}
}