pub mod allophone;
pub mod inventories;
pub mod syllable;
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Manner {
Plosive,
Nasal,
Trill,
TapFlap,
Fricative,
LateralFricative,
Approximant,
LateralApproximant,
Affricate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Place {
Bilabial,
Labiodental,
Dental,
Alveolar,
Postalveolar,
Retroflex,
Palatal,
Velar,
Uvular,
Pharyngeal,
Glottal,
LabialVelar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Height {
Close,
NearClose,
CloseMid,
Mid,
OpenMid,
NearOpen,
Open,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Backness {
Front,
Central,
Back,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Phoneme {
pub ipa: Cow<'static, str>,
pub kind: PhonemeKind,
}
impl Phoneme {
#[must_use]
pub fn consonant(
ipa: impl Into<Cow<'static, str>>,
manner: Manner,
place: Place,
voiced: bool,
) -> Self {
Self {
ipa: ipa.into(),
kind: PhonemeKind::Consonant {
manner,
place,
voiced,
},
}
}
#[must_use]
pub fn vowel(
ipa: impl Into<Cow<'static, str>>,
height: Height,
backness: Backness,
rounded: bool,
) -> Self {
Self {
ipa: ipa.into(),
kind: PhonemeKind::Vowel {
height,
backness,
rounded,
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PhonemeKind {
#[non_exhaustive]
Consonant {
manner: Manner,
place: Place,
voiced: bool,
},
#[non_exhaustive]
Vowel {
height: Height,
backness: Backness,
rounded: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PhonemeInventory {
pub language_code: Cow<'static, str>,
pub language_name: Cow<'static, str>,
pub phonemes: Vec<Phoneme>,
pub tones: Option<Vec<Cow<'static, str>>>,
pub stress: StressPattern,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum StressPattern {
Fixed,
Free,
PitchAccent,
Tonal,
}
impl PhonemeInventory {
#[must_use]
#[inline]
pub fn consonant_count(&self) -> usize {
self.phonemes
.iter()
.filter(|p| matches!(p.kind, PhonemeKind::Consonant { .. }))
.count()
}
#[must_use]
#[inline]
pub fn vowel_count(&self) -> usize {
self.phonemes
.iter()
.filter(|p| matches!(p.kind, PhonemeKind::Vowel { .. }))
.count()
}
#[must_use]
#[inline]
pub fn find(&self, ipa: &str) -> Option<&Phoneme> {
tracing::trace!(language = %self.language_code, ipa, "phoneme lookup");
self.phonemes.iter().find(|p| p.ipa == ipa)
}
#[must_use]
#[inline]
pub fn has(&self, ipa: &str) -> bool {
self.find(ipa).is_some()
}
}
pub struct PhonemeInventoryBuilder {
language_code: Cow<'static, str>,
language_name: Cow<'static, str>,
phonemes: Vec<Phoneme>,
tones: Option<Vec<Cow<'static, str>>>,
stress: StressPattern,
}
impl PhonemeInventoryBuilder {
#[must_use]
pub fn new(code: impl Into<Cow<'static, str>>, name: impl Into<Cow<'static, str>>) -> Self {
Self::with_capacity(code, name, 48)
}
#[must_use]
pub fn with_capacity(
code: impl Into<Cow<'static, str>>,
name: impl Into<Cow<'static, str>>,
capacity: usize,
) -> Self {
Self {
language_code: code.into(),
language_name: name.into(),
phonemes: Vec::with_capacity(capacity),
tones: None,
stress: StressPattern::Free,
}
}
#[must_use]
pub fn stress(mut self, pattern: StressPattern) -> Self {
self.stress = pattern;
self
}
#[must_use]
pub fn tones(mut self, tones: Vec<Cow<'static, str>>) -> Self {
self.tones = Some(tones);
self
}
#[must_use]
pub fn consonant(
mut self,
ipa: impl Into<Cow<'static, str>>,
manner: Manner,
place: Place,
voiced: bool,
) -> Self {
self.phonemes
.push(Phoneme::consonant(ipa, manner, place, voiced));
self
}
#[must_use]
pub fn vowel(
mut self,
ipa: impl Into<Cow<'static, str>>,
height: Height,
backness: Backness,
rounded: bool,
) -> Self {
self.phonemes
.push(Phoneme::vowel(ipa, height, backness, rounded));
self
}
#[must_use]
pub fn phoneme(mut self, phoneme: Phoneme) -> Self {
self.phonemes.push(phoneme);
self
}
#[must_use]
pub fn build(self) -> PhonemeInventory {
debug_assert!(
{
let mut seen = std::collections::HashSet::new();
self.phonemes.iter().all(|p| seen.insert(&p.ipa))
},
"duplicate IPA symbol in {} inventory",
self.language_code
);
tracing::debug!(
language = %self.language_code,
phonemes = self.phonemes.len(),
"built phoneme inventory"
);
PhonemeInventory {
language_code: self.language_code,
language_name: self.language_name,
phonemes: self.phonemes,
tones: self.tones,
stress: self.stress,
}
}
}
#[must_use]
pub fn english() -> PhonemeInventory {
use Backness::*;
use Height::*;
use Manner::*;
use Place::*;
PhonemeInventoryBuilder::with_capacity("en", "English", 36)
.stress(StressPattern::Free)
.consonant("p", Plosive, Bilabial, false)
.consonant("b", Plosive, Bilabial, true)
.consonant("t", Plosive, Alveolar, false)
.consonant("d", Plosive, Alveolar, true)
.consonant("k", Plosive, Velar, false)
.consonant("ɡ", Plosive, Velar, true)
.consonant("f", Fricative, Labiodental, false)
.consonant("v", Fricative, Labiodental, true)
.consonant("θ", Fricative, Dental, false)
.consonant("ð", Fricative, Dental, true)
.consonant("s", Fricative, Alveolar, false)
.consonant("z", Fricative, Alveolar, true)
.consonant("ʃ", Fricative, Postalveolar, false)
.consonant("ʒ", Fricative, Postalveolar, true)
.consonant("h", Fricative, Glottal, false)
.consonant("m", Nasal, Bilabial, true)
.consonant("n", Nasal, Alveolar, true)
.consonant("ŋ", Nasal, Velar, true)
.consonant("ɹ", Approximant, Alveolar, true)
.consonant("l", LateralApproximant, Alveolar, true)
.consonant("w", Approximant, LabialVelar, true)
.consonant("j", Approximant, Palatal, true)
.consonant("t͡ʃ", Affricate, Postalveolar, false)
.consonant("d͡ʒ", Affricate, Postalveolar, true)
.vowel("iː", Close, Front, false)
.vowel("ɪ", NearClose, Front, false)
.vowel("eɪ", CloseMid, Front, false)
.vowel("ɛ", OpenMid, Front, false)
.vowel("æ", NearOpen, Front, false)
.vowel("ɑː", Open, Back, false)
.vowel("ɔː", OpenMid, Back, true)
.vowel("oʊ", CloseMid, Back, true)
.vowel("ʊ", NearClose, Back, true)
.vowel("uː", Close, Back, true)
.vowel("ʌ", OpenMid, Central, false)
.vowel("ə", Mid, Central, false)
.build()
}
#[must_use]
pub fn sanskrit() -> PhonemeInventory {
use Backness::*;
use Height::*;
use Manner::*;
use Place::*;
PhonemeInventoryBuilder::with_capacity("sa", "Sanskrit", 50)
.stress(StressPattern::PitchAccent)
.consonant("k", Plosive, Velar, false)
.consonant("kʰ", Plosive, Velar, false) .consonant("ɡ", Plosive, Velar, true)
.consonant("ɡʰ", Plosive, Velar, true) .consonant("ŋ", Nasal, Velar, true)
.consonant("t͡ɕ", Affricate, Palatal, false)
.consonant("t͡ɕʰ", Affricate, Palatal, false)
.consonant("d͡ʑ", Affricate, Palatal, true)
.consonant("d͡ʑʰ", Affricate, Palatal, true)
.consonant("ɲ", Nasal, Palatal, true)
.consonant("ʈ", Plosive, Retroflex, false)
.consonant("ʈʰ", Plosive, Retroflex, false)
.consonant("ɖ", Plosive, Retroflex, true)
.consonant("ɖʰ", Plosive, Retroflex, true)
.consonant("ɳ", Nasal, Retroflex, true)
.consonant("t̪", Plosive, Dental, false)
.consonant("t̪ʰ", Plosive, Dental, false)
.consonant("d̪", Plosive, Dental, true)
.consonant("d̪ʰ", Plosive, Dental, true)
.consonant("n̪", Nasal, Dental, true)
.consonant("p", Plosive, Bilabial, false)
.consonant("pʰ", Plosive, Bilabial, false)
.consonant("b", Plosive, Bilabial, true)
.consonant("bʰ", Plosive, Bilabial, true)
.consonant("m", Nasal, Bilabial, true)
.consonant("j", Approximant, Palatal, true)
.consonant("r", Trill, Alveolar, true)
.consonant("l", LateralApproximant, Alveolar, true)
.consonant("ʋ", Approximant, Labiodental, true)
.consonant("ɕ", Fricative, Palatal, false)
.consonant("ʂ", Fricative, Retroflex, false)
.consonant("s", Fricative, Alveolar, false)
.consonant("ɦ", Fricative, Glottal, true)
.consonant("ɭ", LateralApproximant, Retroflex, true) .consonant("kʂ", Affricate, Velar, false) .consonant("d͡ʑɲ", Affricate, Palatal, true) .vowel("ɐ", NearOpen, Central, false) .vowel("i", Close, Front, false) .vowel("u", Close, Back, true) .vowel("r̩", Close, Central, false) .vowel("l̩", Close, Central, false) .vowel("ɐː", NearOpen, Central, false) .vowel("iː", Close, Front, false) .vowel("uː", Close, Back, true) .vowel("r̩ː", Close, Central, false) .vowel("l̩ː", Close, Central, false) .vowel("eː", CloseMid, Front, false) .vowel("ɐi", NearOpen, Front, false) .vowel("oː", CloseMid, Back, true) .vowel("ɐu", NearOpen, Back, true) .build()
}
#[must_use]
pub fn greek() -> PhonemeInventory {
use Backness::*;
use Height::*;
use Manner::*;
use Place::*;
PhonemeInventoryBuilder::with_capacity("el", "Greek", 25)
.stress(StressPattern::Free)
.consonant("p", Plosive, Bilabial, false)
.consonant("b", Plosive, Bilabial, true)
.consonant("t", Plosive, Alveolar, false)
.consonant("d", Plosive, Alveolar, true)
.consonant("k", Plosive, Velar, false)
.consonant("ɡ", Plosive, Velar, true)
.consonant("f", Fricative, Labiodental, false)
.consonant("v", Fricative, Labiodental, true)
.consonant("θ", Fricative, Dental, false)
.consonant("ð", Fricative, Dental, true)
.consonant("s", Fricative, Alveolar, false)
.consonant("z", Fricative, Alveolar, true)
.consonant("x", Fricative, Velar, false)
.consonant("ɣ", Fricative, Velar, true)
.consonant("m", Nasal, Bilabial, true)
.consonant("n", Nasal, Alveolar, true)
.consonant("l", LateralApproximant, Alveolar, true)
.consonant("r", Trill, Alveolar, true)
.consonant("t͡s", Affricate, Alveolar, false)
.consonant("d͡z", Affricate, Alveolar, true)
.vowel("i", Close, Front, false)
.vowel("e", CloseMid, Front, false)
.vowel("a", Open, Central, false)
.vowel("o", CloseMid, Back, true)
.vowel("u", Close, Back, true)
.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_english_inventory_size() {
let en = english();
assert_eq!(en.consonant_count(), 24);
assert_eq!(en.vowel_count(), 12);
}
#[test]
fn test_english_has_th() {
let en = english();
assert!(en.has("θ"));
assert!(en.has("ð"));
}
#[test]
fn test_english_no_tones() {
let en = english();
assert!(en.tones.is_none());
assert_eq!(en.stress, StressPattern::Free);
}
#[test]
fn test_find_phoneme() {
let en = english();
let p = en.find("ʃ").unwrap();
assert!(matches!(
p.kind,
PhonemeKind::Consonant {
manner: Manner::Fricative,
place: Place::Postalveolar,
voiced: false
}
));
}
#[test]
fn test_missing_phoneme() {
let en = english();
assert!(!en.has("ʀ"));
}
#[test]
fn test_w_is_labial_velar() {
let en = english();
let w = en.find("w").unwrap();
assert!(matches!(
w.kind,
PhonemeKind::Consonant {
place: Place::LabialVelar,
..
}
));
}
#[test]
fn test_phoneme_eq() {
let a = Phoneme::consonant("p", Manner::Plosive, Place::Bilabial, false);
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn test_sanskrit_inventory_size() {
let sa = sanskrit();
assert_eq!(sa.consonant_count(), 36);
assert_eq!(sa.vowel_count(), 14);
assert_eq!(sa.language_code, "sa");
}
#[test]
fn test_sanskrit_five_vargas() {
let sa = sanskrit();
assert!(sa.has("k"));
assert!(sa.has("kʰ"));
assert!(sa.has("ɡ"));
assert!(sa.has("ɡʰ"));
assert!(sa.has("ŋ"));
}
#[test]
fn test_sanskrit_retroflexes() {
let sa = sanskrit();
assert!(sa.has("ʈ"));
assert!(sa.has("ɖ"));
assert!(sa.has("ɳ"));
}
#[test]
fn test_sanskrit_sibilants() {
let sa = sanskrit();
assert!(sa.has("ɕ")); assert!(sa.has("ʂ")); assert!(sa.has("s")); }
#[test]
fn test_sanskrit_vowels() {
let sa = sanskrit();
assert!(sa.has("r̩"));
assert!(sa.has("l̩"));
assert!(sa.has("ɐ"));
assert!(sa.has("ɐː"));
}
#[test]
fn test_sanskrit_stress() {
let sa = sanskrit();
assert_eq!(sa.stress, StressPattern::PitchAccent);
assert!(sa.tones.is_none());
}
#[test]
fn test_greek_inventory_size() {
let el = greek();
assert_eq!(el.consonant_count(), 20);
assert_eq!(el.vowel_count(), 5);
assert_eq!(el.language_code, "el");
}
#[test]
fn test_greek_five_vowels() {
let el = greek();
assert!(el.has("i"));
assert!(el.has("e"));
assert!(el.has("a"));
assert!(el.has("o"));
assert!(el.has("u"));
}
#[test]
fn test_greek_velar_fricatives() {
let el = greek();
assert!(el.has("x"));
assert!(el.has("ɣ"));
}
#[test]
fn test_greek_stress() {
let el = greek();
assert_eq!(el.stress, StressPattern::Free);
}
#[test]
fn test_builder_minimal() {
let inv = PhonemeInventoryBuilder::new("xx", "Test")
.consonant("t", Manner::Plosive, Place::Alveolar, false)
.vowel("a", Height::Open, Backness::Central, false)
.build();
assert_eq!(inv.language_code, "xx");
assert_eq!(inv.consonant_count(), 1);
assert_eq!(inv.vowel_count(), 1);
assert_eq!(inv.stress, StressPattern::Free); }
#[test]
fn test_builder_with_tones() {
let inv = PhonemeInventoryBuilder::new("xx", "Tonal Test")
.stress(StressPattern::Tonal)
.tones(vec![
Cow::Borrowed("˥"),
Cow::Borrowed("˧˥"),
Cow::Borrowed("˨˩˦"),
Cow::Borrowed("˥˩"),
])
.vowel("a", Height::Open, Backness::Central, false)
.build();
assert_eq!(inv.stress, StressPattern::Tonal);
assert_eq!(inv.tones.as_ref().unwrap().len(), 4);
}
#[test]
fn test_builder_phoneme_method() {
let custom = Phoneme::consonant("ɬ", Manner::LateralFricative, Place::Alveolar, false);
let inv = PhonemeInventoryBuilder::new("xx", "Test")
.phoneme(custom.clone())
.build();
assert_eq!(inv.phonemes[0], custom);
}
}