use std::collections::HashMap;
use serde::Serialize;
use crate::conlang::types::Phonology;
use crate::conlang::types::morphology::{AffixPosition, Morphology};
use crate::language_entry::DictionaryEntry;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Parse {
pub root: String,
pub gloss: String,
pub affixes: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, PartialEq)]
pub struct ParseReport {
pub surface: String,
pub segments: Vec<String>,
pub parses: Vec<Parse>,
}
const MAX_AFFIXES: usize = 6;
pub fn parse(phon: &Phonology, morph: &Morphology, entries: &[DictionaryEntry], surface: &str) -> ParseReport {
let segs = phon.segment(&surface.to_lowercase());
let mut report = ParseReport { surface: surface.to_string(), segments: segs.clone(), ..Default::default() };
if segs.is_empty() {
return report;
}
let mut roots: HashMap<Vec<String>, String> = HashMap::new();
for e in entries {
let rs = phon.segment(&e.word.to_lowercase());
if !rs.is_empty() {
roots.entry(rs).or_insert_with(|| e.translation.clone());
}
}
let affixes: Vec<(Vec<String>, String, bool)> = morph
.morphemes
.iter()
.filter_map(|m| match m.position {
Some(AffixPosition::Suffix) => Some((phon.segment(&m.form), affix_label(&m.gloss, &m.id), false)),
Some(AffixPosition::Prefix) => Some((phon.segment(&m.form), affix_label(&m.gloss, &m.id), true)),
_ => None,
})
.filter(|(f, _, _)| !f.is_empty())
.collect();
let mut applied: Vec<String> = Vec::new();
strip(&segs, phon, &roots, &affixes, &mut applied, &mut report.parses);
report.parses.sort_by(|a, b| a.affixes.len().cmp(&b.affixes.len()).then_with(|| a.root.cmp(&b.root)));
report.parses.dedup();
report
}
#[allow(clippy::too_many_arguments)]
fn strip(
segs: &[String],
phon: &Phonology,
roots: &HashMap<Vec<String>, String>,
affixes: &[(Vec<String>, String, bool)],
applied: &mut Vec<String>,
out: &mut Vec<Parse>,
) {
if let Some(gloss) = roots.get(segs) {
out.push(Parse { root: render(phon, segs), gloss: gloss.clone(), affixes: applied.clone() });
}
if applied.len() >= MAX_AFFIXES {
return;
}
for (form, label, is_prefix) in affixes {
let stripped: Option<&[String]> = if *is_prefix {
segs.strip_prefix(form.as_slice())
} else {
segs.strip_suffix(form.as_slice())
};
if let Some(rem) = stripped {
if rem.len() < segs.len() && !rem.is_empty() {
applied.push(label.clone());
strip(rem, phon, roots, affixes, applied, out);
applied.pop();
}
}
}
}
fn affix_label(gloss: &str, id: &str) -> String {
if gloss.trim().is_empty() { id.to_string() } else { gloss.to_string() }
}
fn render(phon: &Phonology, segs: &[String]) -> String {
segs.iter().map(|ipa| phon.phoneme(ipa).map(|p| p.grapheme()).unwrap_or(ipa.as_str())).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn phon() -> Phonology {
let body = r#"{
phonemes: [
{ ipa: "k", kind: "consonant" }, { ipa: "t", kind: "consonant" },
{ ipa: "n", kind: "consonant" }, { ipa: "s", kind: "consonant" },
{ ipa: "a", kind: "vowel" }, { ipa: "i", kind: "vowel" }
]
}"#;
Phonology::from_hjson(body).unwrap().unwrap()
}
fn morph() -> Morphology {
let body = r#"{
morphemes: [
{ id: "pl", gloss: "PL", form: "i", position: "suffix" }
{ id: "dat", gloss: "DAT", form: "s", position: "suffix" }
{ id: "def", gloss: "DEF", form: "na", position: "prefix" }
]
}"#;
Morphology::from_hjson(body).unwrap().unwrap()
}
fn entry(w: &str, g: &str) -> DictionaryEntry {
DictionaryEntry { word: w.into(), pos: "noun".into(), translation: g.into(), ..Default::default() }
}
#[test]
fn parses_a_suffixed_form_to_its_root() {
let lex = [entry("kata", "stone")];
let r = parse(&phon(), &morph(), &lex, "katai");
assert!(r.parses.iter().any(|p| p.root == "kata" && p.affixes == vec!["PL".to_string()]));
}
#[test]
fn parses_a_stacked_affix_form() {
let lex = [entry("kata", "stone")];
let r = parse(&phon(), &morph(), &lex, "nakatas");
assert!(r.parses.iter().any(|p| {
p.root == "kata" && p.affixes.contains(&"DEF".to_string()) && p.affixes.contains(&"DAT".to_string())
}));
}
#[test]
fn a_bare_root_parses_with_no_affixes() {
let lex = [entry("kata", "stone")];
let r = parse(&phon(), &morph(), &lex, "kata");
assert!(r.parses.iter().any(|p| p.root == "kata" && p.affixes.is_empty()));
}
#[test]
fn an_unanalyzable_form_yields_no_parses() {
let lex = [entry("kata", "stone")];
let r = parse(&phon(), &morph(), &lex, "sini");
assert!(r.parses.is_empty());
}
}