use crate::conlang::phonology::allophony_eval;
use crate::conlang::types::morphology::{AffixPosition, Morphology};
use crate::conlang::types::Phonology;
#[derive(Debug, Clone, PartialEq)]
pub struct DerivedForm {
pub rule: String,
pub form: String,
pub gloss: String,
pub pos: String,
}
pub fn generate(
phon: &Phonology,
morph: &Morphology,
root: &str,
root_gloss: &str,
root_pos: &str,
) -> Vec<DerivedForm> {
morph
.derivations
.iter()
.filter(|r| r.from_pos.as_deref().is_none_or(|p| p.eq_ignore_ascii_case(root_pos)))
.map(|r| {
let underlying = match r.position {
AffixPosition::Prefix => format!("{}{root}", r.form),
AffixPosition::Suffix => format!("{root}{}", r.form),
_ => root.to_string(),
};
let surface = allophony_eval::surface_form(phon, &phon.segment(&underlying));
let form = render(phon, &surface);
let gloss = match &r.gloss_template {
Some(t) if t.contains("{}") => t.replace("{}", root_gloss),
_ if !r.gloss.is_empty() => format!("{root_gloss}.{}", r.gloss),
_ => format!("{root_gloss} ({})", r.name),
};
DerivedForm { rule: r.name.clone(), form, gloss, pos: r.to_pos.clone() }
})
.collect()
}
fn render(phon: &Phonology, seq: &[String]) -> String {
seq.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: "r", kind: "consonant" }, { ipa: "n", kind: "consonant" },
{ ipa: "a", kind: "vowel" }, { ipa: "i", kind: "vowel" }, { ipa: "o", kind: "vowel" }
]
}"#;
Phonology::from_hjson(body).unwrap().unwrap()
}
fn morph() -> Morphology {
let body = r#"{
derivations: [
{ name: "agent", form: "ron", position: "suffix", from_pos: "verb", to_pos: "noun",
gloss_template: "one who {}s" }
{ name: "diminutive", gloss: "DIM", form: "i", position: "suffix", to_pos: "noun" }
{ name: "negation", form: "na", position: "prefix", from_pos: "adjective", to_pos: "adjective" }
]
}"#;
Morphology::from_hjson(body).unwrap().unwrap()
}
#[test]
fn applies_pos_matching_rules() {
let d = generate(&phon(), &morph(), "kata", "build", "verb");
assert_eq!(d.len(), 2);
let agent = d.iter().find(|x| x.rule == "agent").unwrap();
assert_eq!(agent.form, "kataron");
assert_eq!(agent.gloss, "one who builds");
assert_eq!(agent.pos, "noun");
let dim = d.iter().find(|x| x.rule == "diminutive").unwrap();
assert_eq!(dim.form, "katai");
assert_eq!(dim.gloss, "build.DIM");
}
#[test]
fn prefix_derivation_and_any_pos() {
let d = generate(&phon(), &morph(), "tana", "good", "adjective");
let neg = d.iter().find(|x| x.rule == "negation").unwrap();
assert_eq!(neg.form, "natana");
assert_eq!(neg.gloss, "good (negation)");
}
}