use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Environment {
WordInitial,
WordFinal,
Intervocalic,
PreVocalic,
PostVocalic,
Before(PhonemeClass),
After(PhonemeClass),
SyllableFinal,
SyllableInitial,
PreStress,
Unstressed,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PhonemeClass {
Vowel,
Consonant,
Nasal,
Plosive,
Fricative,
Voiceless,
Voiced,
Specific(Cow<'static, str>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AllophoneRule {
pub phoneme: Cow<'static, str>,
pub allophone: Cow<'static, str>,
pub environment: Environment,
pub obligatory: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AllophoneRuleSet {
pub language_code: Cow<'static, str>,
pub rules: Vec<AllophoneRule>,
}
impl AllophoneRuleSet {
#[must_use]
pub fn rules_for(&self, phoneme: &str) -> Vec<&AllophoneRule> {
tracing::trace!(language = %self.language_code, phoneme, "allophone rule lookup");
self.rules.iter().filter(|r| r.phoneme == phoneme).collect()
}
#[must_use]
pub fn realize(&self, phoneme: &str, env: &Environment) -> Cow<'static, str> {
tracing::trace!(
language = %self.language_code,
phoneme,
environment = ?env,
"allophone realization"
);
self.rules
.iter()
.find(|r| r.phoneme == phoneme && &r.environment == env)
.map(|r| r.allophone.clone())
.unwrap_or_else(|| Cow::Owned(phoneme.to_owned()))
}
}
#[must_use]
pub fn english_allophones() -> AllophoneRuleSet {
AllophoneRuleSet {
language_code: Cow::Borrowed("en"),
rules: vec![
AllophoneRule {
phoneme: Cow::Borrowed("t"),
allophone: Cow::Borrowed("ɾ"),
environment: Environment::Intervocalic,
obligatory: false,
},
AllophoneRule {
phoneme: Cow::Borrowed("t"),
allophone: Cow::Borrowed("tʰ"),
environment: Environment::WordInitial,
obligatory: true,
},
AllophoneRule {
phoneme: Cow::Borrowed("p"),
allophone: Cow::Borrowed("pʰ"),
environment: Environment::WordInitial,
obligatory: true,
},
AllophoneRule {
phoneme: Cow::Borrowed("k"),
allophone: Cow::Borrowed("kʰ"),
environment: Environment::WordInitial,
obligatory: true,
},
AllophoneRule {
phoneme: Cow::Borrowed("l"),
allophone: Cow::Borrowed("ɫ"),
environment: Environment::SyllableFinal,
obligatory: true,
},
AllophoneRule {
phoneme: Cow::Borrowed("ə"),
allophone: Cow::Borrowed("ɨ"),
environment: Environment::Unstressed,
obligatory: false,
},
],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_english_allophones_count() {
let rules = english_allophones();
assert_eq!(rules.language_code, "en");
assert!(!rules.rules.is_empty());
}
#[test]
fn test_rules_for_t() {
let rules = english_allophones();
let t_rules = rules.rules_for("t");
assert_eq!(t_rules.len(), 2); }
#[test]
fn test_realize_t_intervocalic() {
let rules = english_allophones();
let result = rules.realize("t", &Environment::Intervocalic);
assert_eq!(result, "ɾ");
}
#[test]
fn test_realize_t_word_initial() {
let rules = english_allophones();
let result = rules.realize("t", &Environment::WordInitial);
assert_eq!(result, "tʰ");
}
#[test]
fn test_realize_unknown_env_returns_phoneme() {
let rules = english_allophones();
let result = rules.realize("t", &Environment::WordFinal);
assert_eq!(result, "t");
}
#[test]
fn test_realize_dark_l() {
let rules = english_allophones();
let result = rules.realize("l", &Environment::SyllableFinal);
assert_eq!(result, "ɫ");
}
#[test]
fn test_rules_for_unknown_phoneme() {
let rules = english_allophones();
assert!(rules.rules_for("ʀ").is_empty());
}
#[test]
fn test_allophone_serde_roundtrip() {
let rules = english_allophones();
let json = serde_json::to_string(&rules).unwrap();
let back: AllophoneRuleSet = serde_json::from_str(&json).unwrap();
assert_eq!(rules, back);
}
}