use crate::conlang::phonology::ipa;
use crate::conlang::types::{PhonemeKind, Phonology};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Syllable {
pub onset: Vec<String>,
pub nucleus: Vec<String>,
pub coda: Vec<String>,
}
impl Syllable {
fn empty() -> Self {
Self { onset: Vec::new(), nucleus: Vec::new(), coda: Vec::new() }
}
}
fn is_vowel(phon: &Phonology, ipa: &str) -> bool {
match phon.phoneme(ipa).map(|p| p.kind) {
Some(k) => k == PhonemeKind::Vowel,
None => ipa::sonority_of(phon, ipa) >= ipa::VOWEL,
}
}
pub fn syllabify(phon: &Phonology, seq: &[String]) -> Vec<Syllable> {
let mut nuclei: Vec<(usize, usize)> = Vec::new(); let mut i = 0;
while i < seq.len() {
if is_vowel(phon, &seq[i]) {
let start = i;
while i < seq.len() && is_vowel(phon, &seq[i]) {
i += 1;
}
nuclei.push((start, i));
} else {
i += 1;
}
}
if nuclei.is_empty() {
let mut s = Syllable::empty();
s.coda = seq.to_vec();
return if seq.is_empty() { Vec::new() } else { vec![s] };
}
let mut sylls: Vec<Syllable> = Vec::with_capacity(nuclei.len());
for (idx, &(ns, ne)) in nuclei.iter().enumerate() {
let mut s = Syllable::empty();
s.nucleus = seq[ns..ne].to_vec();
let prev_end = if idx == 0 { 0 } else { nuclei[idx - 1].1 };
let cluster = &seq[prev_end..ns];
let split = if idx == 0 { 0 } else { max_onset_split(phon, cluster) };
s.onset = cluster[split..].to_vec();
if idx > 0 {
sylls[idx - 1].coda = cluster[..split].to_vec();
}
sylls.push(s);
}
let last_end = nuclei.last().unwrap().1;
if last_end < seq.len() {
if let Some(last) = sylls.last_mut() {
last.coda = seq[last_end..].to_vec();
}
}
sylls
}
fn max_onset_split(phon: &Phonology, cluster: &[String]) -> usize {
let n = cluster.len();
if n == 0 {
return 0;
}
let mut start = n - 1;
while start > 0
&& ipa::sonority_of(phon, &cluster[start - 1]) < ipa::sonority_of(phon, &cluster[start])
{
start -= 1;
}
start
}
pub fn render(phon: &Phonology, sylls: &[Syllable]) -> String {
let g = |ipa: &String| {
phon.phoneme(ipa).map(|p| p.grapheme().to_string()).unwrap_or_else(|| ipa.clone())
};
sylls
.iter()
.map(|s| {
let mut out = String::new();
for p in s.onset.iter().chain(&s.nucleus).chain(&s.coda) {
out.push_str(&g(p));
}
out
})
.collect::<Vec<_>>()
.join(".")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conlang::types::{Phoneme, PhonemeKind};
fn ph(ipa: &str, kind: PhonemeKind) -> Phoneme {
Phoneme { ipa: ipa.into(), romanize: None, kind, sonority: None }
}
fn lang() -> Phonology {
Phonology {
phonemes: vec![
ph("p", PhonemeKind::Consonant), ph("t", PhonemeKind::Consonant),
ph("k", PhonemeKind::Consonant), ph("s", PhonemeKind::Consonant),
ph("r", PhonemeKind::Consonant), ph("l", PhonemeKind::Consonant),
ph("n", PhonemeKind::Consonant),
ph("a", PhonemeKind::Vowel), ph("i", PhonemeKind::Vowel), ph("o", PhonemeKind::Vowel),
],
..Default::default()
}
}
fn seq(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| s.to_string()).collect()
}
#[test]
fn simple_cv_cv() {
let p = lang();
let s = syllabify(&p, &seq(&["t", "a", "k", "o"]));
assert_eq!(render(&p, &s), "ta.ko");
assert_eq!(s.len(), 2);
assert_eq!(s[0].onset, seq(&["t"]));
assert_eq!(s[1].onset, seq(&["k"]));
}
#[test]
fn maximal_onset_keeps_rising_cluster_together() {
let p = lang();
let s = syllabify(&p, &seq(&["a", "t", "r", "a"]));
assert_eq!(render(&p, &s), "a.tra");
assert_eq!(s[0].coda, Vec::<String>::new());
assert_eq!(s[1].onset, seq(&["t", "r"]));
}
#[test]
fn falling_cluster_splits_across_the_boundary() {
let p = lang();
let s = syllabify(&p, &seq(&["a", "r", "t", "a"]));
assert_eq!(render(&p, &s), "ar.ta");
assert_eq!(s[0].coda, seq(&["r"]));
assert_eq!(s[1].onset, seq(&["t"]));
}
#[test]
fn leading_and_trailing_consonants() {
let p = lang();
let s = syllabify(&p, &seq(&["s", "t", "a", "r", "k"]));
assert_eq!(s.len(), 1);
assert_eq!(s[0].onset, seq(&["s", "t"]));
assert_eq!(s[0].nucleus, seq(&["a"]));
assert_eq!(s[0].coda, seq(&["r", "k"]));
}
#[test]
fn diphthong_nucleus() {
let p = lang();
let s = syllabify(&p, &seq(&["t", "a", "i", "n"]));
assert_eq!(s.len(), 1);
assert_eq!(s[0].nucleus, seq(&["a", "i"]));
assert_eq!(s[0].coda, seq(&["n"]));
}
}