use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use svara::phoneme::Phoneme;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StressLevel {
Unstressed,
Primary,
Secondary,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Syllable {
phonemes: Vec<Phoneme>,
stress: StressLevel,
}
impl Syllable {
#[must_use]
pub fn new(phonemes: Vec<Phoneme>) -> Self {
Self {
phonemes,
stress: StressLevel::Unstressed,
}
}
#[must_use]
pub fn with_stress(mut self, stress: StressLevel) -> Self {
self.stress = stress;
self
}
#[must_use]
pub fn phonemes(&self) -> &[Phoneme] {
&self.phonemes
}
#[must_use]
pub fn stress(&self) -> StressLevel {
self.stress
}
#[must_use]
pub fn len(&self) -> usize {
self.phonemes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.phonemes.is_empty()
}
}
#[must_use]
fn is_nucleus(phoneme: &Phoneme) -> bool {
matches!(
phoneme,
Phoneme::VowelA
| Phoneme::VowelE
| Phoneme::VowelI
| Phoneme::VowelO
| Phoneme::VowelU
| Phoneme::VowelOpenA
| Phoneme::VowelOpenE
| Phoneme::VowelOpenO
| Phoneme::VowelNearI
| Phoneme::VowelNearU
| Phoneme::VowelAsh
| Phoneme::VowelSchwa
| Phoneme::VowelCupV
| Phoneme::VowelBird
| Phoneme::VowelLongI
| Phoneme::DiphthongAI
| Phoneme::DiphthongAU
| Phoneme::DiphthongEI
| Phoneme::DiphthongOI
| Phoneme::DiphthongOU
)
}
#[must_use]
pub fn syllabify(phonemes: &[Phoneme]) -> Vec<Syllable> {
if phonemes.is_empty() {
return Vec::new();
}
let nuclei: Vec<usize> = phonemes
.iter()
.enumerate()
.filter(|(_, p)| is_nucleus(p))
.map(|(i, _)| i)
.collect();
if nuclei.is_empty() {
return alloc::vec![Syllable::new(phonemes.to_vec())];
}
let mut syllables = Vec::with_capacity(nuclei.len());
let mut start = 0_usize;
for (i, &nucleus_idx) in nuclei.iter().enumerate() {
let end = if i + 1 < nuclei.len() {
let next_nucleus = nuclei[i + 1];
let gap_start = nucleus_idx + 1;
let gap_len = next_nucleus - gap_start;
if gap_len <= 1 {
gap_start
} else {
gap_start + 1
}
} else {
phonemes.len()
};
syllables.push(Syllable::new(phonemes[start..end].to_vec()));
start = end;
}
syllables
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_syllable_new() {
let s = Syllable::new(alloc::vec![Phoneme::PlosiveK, Phoneme::VowelAsh]);
assert_eq!(s.len(), 2);
assert_eq!(s.stress(), StressLevel::Unstressed);
assert!(!s.is_empty());
}
#[test]
fn test_syllable_with_stress() {
let s = Syllable::new(alloc::vec![Phoneme::VowelA]).with_stress(StressLevel::Primary);
assert_eq!(s.stress(), StressLevel::Primary);
}
#[test]
fn test_syllabify_empty() {
assert!(syllabify(&[]).is_empty());
}
#[test]
fn test_syllabify_single_vowel() {
let syls = syllabify(&[Phoneme::VowelA]);
assert_eq!(syls.len(), 1);
assert_eq!(syls[0].phonemes(), &[Phoneme::VowelA]);
}
#[test]
fn test_syllabify_cvc() {
let syls = syllabify(&[Phoneme::PlosiveK, Phoneme::VowelAsh, Phoneme::PlosiveT]);
assert_eq!(syls.len(), 1);
}
#[test]
fn test_syllabify_cvcv() {
let syls = syllabify(&[
Phoneme::NasalM,
Phoneme::VowelA,
Phoneme::NasalM,
Phoneme::VowelA,
]);
assert_eq!(syls.len(), 2);
assert_eq!(syls[0].phonemes(), &[Phoneme::NasalM, Phoneme::VowelA]);
assert_eq!(syls[1].phonemes(), &[Phoneme::NasalM, Phoneme::VowelA]);
}
#[test]
fn test_syllabify_hello() {
let syls = syllabify(&[
Phoneme::FricativeH,
Phoneme::VowelOpenE,
Phoneme::LateralL,
Phoneme::DiphthongOU,
]);
assert_eq!(syls.len(), 2);
}
#[test]
fn test_syllabify_consonant_cluster() {
let syls = syllabify(&[
Phoneme::FricativeS,
Phoneme::PlosiveT,
Phoneme::ApproximantR,
Phoneme::VowelE,
Phoneme::PlosiveT,
]);
assert_eq!(syls.len(), 1);
}
#[test]
fn test_syllabify_no_vowels() {
let syls = syllabify(&[Phoneme::PlosiveP, Phoneme::FricativeS]);
assert_eq!(syls.len(), 1);
}
#[test]
fn test_syllable_serde_roundtrip() {
let s = Syllable::new(alloc::vec![
Phoneme::PlosiveK,
Phoneme::VowelAsh,
Phoneme::PlosiveT
])
.with_stress(StressLevel::Primary);
let json = serde_json::to_string(&s).unwrap();
let s2: Syllable = serde_json::from_str(&json).unwrap();
assert_eq!(s, s2);
}
#[test]
fn test_stress_level_serde_roundtrip() {
for level in [
StressLevel::Unstressed,
StressLevel::Primary,
StressLevel::Secondary,
] {
let json = serde_json::to_string(&level).unwrap();
let level2: StressLevel = serde_json::from_str(&json).unwrap();
assert_eq!(level, level2);
}
}
#[test]
fn test_is_nucleus() {
assert!(is_nucleus(&Phoneme::VowelA));
assert!(is_nucleus(&Phoneme::DiphthongAI));
assert!(is_nucleus(&Phoneme::VowelSchwa));
assert!(!is_nucleus(&Phoneme::PlosiveK));
assert!(!is_nucleus(&Phoneme::NasalM));
assert!(!is_nucleus(&Phoneme::Silence));
}
}