#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Place {
Labial = 0,
Dental = 1,
Alveolar = 2,
PostAlveolar = 3, Palatal = 4, Velar = 5,
Glottal = 6,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Manner {
Stop,
Affricate,
Nasal,
Fricative,
Lateral,
Rhotic,
Approximant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Phoneme {
place: Place,
manner: Manner,
voiced: bool,
aspirated: bool, }
const W_PLACE: u16 = 2; const W_MANNER: u16 = 3; const W_VOICE: u16 = 2;
const W_ASPIRATION: u16 = 1;
fn manner_distance(a: Manner, b: Manner) -> u16 {
use Manner::*;
if a == b {
return 0;
}
let obstruent = |m: Manner| matches!(m, Stop | Affricate | Fricative);
let sonorant = |m: Manner| matches!(m, Nasal | Lateral | Rhotic | Approximant);
match (a, b) {
(Stop, Affricate) | (Affricate, Stop) => 1,
(Affricate, Fricative) | (Fricative, Affricate) => 1,
(Stop, Fricative) | (Fricative, Stop) => 2,
_ if obstruent(a) && obstruent(b) => 2,
_ if sonorant(a) && sonorant(b) => 2, _ => 4, }
}
fn phoneme_distance(a: Phoneme, b: Phoneme) -> u16 {
let place = (a.place as i16 - b.place as i16).unsigned_abs() * W_PLACE;
let manner = manner_distance(a.manner, b.manner) * W_MANNER;
let voice = u16::from(a.voiced != b.voiced) * W_VOICE;
let aspiration = u16::from(a.aspirated != b.aspirated) * W_ASPIRATION;
place + manner + voice + aspiration
}
fn base_phoneme(ch: char) -> Option<Phoneme> {
use Manner::*;
use Place::*;
let p = |place, manner, voiced, aspirated| {
Some(Phoneme {
place,
manner,
voiced,
aspirated,
})
};
match ch {
'ক' => p(Velar, Stop, false, false),
'খ' => p(Velar, Stop, false, true),
'গ' => p(Velar, Stop, true, false),
'ঘ' => p(Velar, Stop, true, true),
'ঙ' => p(Velar, Nasal, true, false),
'চ' => p(Palatal, Affricate, false, false),
'ছ' => p(Palatal, Affricate, false, true),
'জ' | 'য' => p(Palatal, Affricate, true, false), 'ঝ' => p(Palatal, Affricate, true, true),
'ঞ' => p(Palatal, Nasal, true, false),
'ট' => p(PostAlveolar, Stop, false, false),
'ঠ' => p(PostAlveolar, Stop, false, true),
'ড' => p(PostAlveolar, Stop, true, false),
'ঢ' => p(PostAlveolar, Stop, true, true),
'ত' | 'ৎ' => p(Dental, Stop, false, false),
'থ' => p(Dental, Stop, false, true),
'দ' => p(Dental, Stop, true, false),
'ধ' => p(Dental, Stop, true, true),
'ণ' | 'ন' => p(Alveolar, Nasal, true, false),
'প' => p(Labial, Stop, false, false),
'ফ' => p(Labial, Stop, false, true),
'ব' => p(Labial, Stop, true, false),
'ভ' => p(Labial, Stop, true, true),
'ম' => p(Labial, Nasal, true, false),
'র' => p(Alveolar, Rhotic, true, false), '\u{09DC}' | '\u{09DD}' => p(PostAlveolar, Rhotic, true, false), 'ল' => p(Alveolar, Lateral, true, false),
'শ' | 'ষ' => p(PostAlveolar, Fricative, false, false), 'স' => p(Alveolar, Fricative, false, false), 'হ' => p(Glottal, Fricative, true, false),
'\u{09DF}' => p(Palatal, Approximant, true, false), _ => None,
}
}
pub fn consonant_distance(a: (char, bool), b: (char, bool)) -> Option<u16> {
Some(phoneme_distance(phota_phoneme(a)?, phota_phoneme(b)?))
}
const BASE_CONSONANTS: &[char] = &[
'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ', 'ণ', 'ত', 'থ',
'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল', 'শ', 'ষ', 'স', 'হ',
];
pub fn near_consonants(ch: char, max_distance: u16) -> Vec<(char, u16)> {
let Some(source) = base_phoneme(ch) else {
return Vec::new();
};
let mut near = Vec::new();
for &other in BASE_CONSONANTS {
if other == ch {
continue;
}
if let Some(candidate) = base_phoneme(other) {
let distance = phoneme_distance(source, candidate);
if distance <= max_distance {
near.push((other, distance));
}
}
}
near.sort_by_key(|&(_, distance)| distance);
near
}
fn phota_phoneme((ch, phota): (char, bool)) -> Option<Phoneme> {
if phota {
return match ch {
'ড' | 'ঢ' => Some(Phoneme {
place: Place::PostAlveolar,
manner: Manner::Rhotic,
voiced: true,
aspirated: false,
}), 'য' => base_phoneme('\u{09DF}'), _ => base_phoneme(ch),
};
}
base_phoneme(ch)
}
#[cfg(test)]
mod tests {
use super::*;
fn dist(a: char, b: char) -> u16 {
consonant_distance((a, false), (b, false)).expect("mapped consonants")
}
#[test]
fn identical_phonemes_are_zero() {
assert_eq!(dist('শ', 'ষ'), 0); assert_eq!(dist('জ', 'য'), 0); assert_eq!(dist('ণ', 'ন'), 0); assert_eq!(dist('ক', 'ক'), 0);
}
#[test]
fn nearby_phonemes_are_small_and_ordered_below_far_ones() {
let s_sh = dist('স', 'শ'); let k_kh = dist('ক', 'খ'); let p_ph = dist('প', 'ফ');
let t_stop = dist('ত', 'ট'); let r_flap = consonant_distance(('র', false), ('ড', true)).unwrap();
let k_p = dist('ক', 'প'); let m_s = dist('ম', 'স'); let l_k = dist('ল', 'ক');
for near in [s_sh, k_kh, p_ph, t_stop, r_flap] {
assert!(near > 0, "nearby but not identical");
assert!(near < k_p, "nearby {near} should be < far ক/প {k_p}");
assert!(near < m_s && near < l_k, "nearby {near} should be < unrelated");
}
assert!(k_kh <= s_sh && k_kh <= t_stop);
}
#[test]
fn phota_letters_map_to_flap() {
let r_flap = consonant_distance(('র', false), ('ড', true)).unwrap();
let stop_flap = consonant_distance(('ড', false), ('ড', true)).unwrap();
assert!(r_flap < stop_flap, "ড় should be closer to র than to ড");
}
}