#[derive(Debug, Clone, Copy)]
pub struct PronounSuffix {
pub key: &'static str,
pub meaning: &'static str,
pub endings: &'static [&'static str],
}
#[rustfmt::skip]
pub const PRONOUN_SUFFIXES: &[PronounSuffix] = &[
PronounSuffix { key: "3ms", meaning: "him", endings: &[
"\u{05B8}\u{05D9}\u{05D5}", "\u{05B4}\u{05D9}\u{05D5}", "\u{05B4}\u{05D9}\u{05D4}\u{05D5}\u{05BC}", "\u{05D4}\u{05D5}\u{05BC}", "\u{05D5}\u{05B9}", ]},
PronounSuffix { key: "1cs", meaning: "me", endings: &[
"\u{05E0}\u{05B4}\u{05BC}\u{05D9}", "\u{05B7}\u{05D9}", "\u{05B8}\u{05D9}", "\u{05B4}\u{05D9}", ]},
PronounSuffix { key: "3mp", meaning: "them", endings: &[
"\u{05B5}\u{05D9}\u{05D4}\u{05B6}\u{05DD}", "\u{05B4}\u{05D9}\u{05D4}\u{05B6}\u{05DD}", "\u{05D4}\u{05B6}\u{05DD}", "\u{05DE}\u{05D5}\u{05B9}", "\u{05B8}\u{05DD}", ]},
PronounSuffix { key: "2ms", meaning: "you (m.)", endings: &[
"\u{05B6}\u{05D9}\u{05DA}\u{05B8}", "\u{05B4}\u{05D9}\u{05DA}\u{05B8}", "\u{05DA}\u{05B8}\u{05BC}", "\u{05DA}\u{05B8}", ]},
PronounSuffix { key: "3fs", meaning: "her", endings: &[
"\u{05B6}\u{05D9}\u{05D4}\u{05B8}", "\u{05B4}\u{05D9}\u{05D4}\u{05B8}", "\u{05E0}\u{05B8}\u{05BC}\u{05D4}", "\u{05B8}\u{05D4}\u{05BC}", ]},
PronounSuffix { key: "1cp", meaning: "us", endings: &[
"\u{05B5}\u{05D9}\u{05E0}\u{05D5}\u{05BC}", "\u{05B4}\u{05D9}\u{05E0}\u{05D5}\u{05BC}", "\u{05E0}\u{05BC}\u{05D5}\u{05BC}", "\u{05B8}\u{05E0}\u{05D5}\u{05BC}", ]},
PronounSuffix { key: "2mp", meaning: "you (pl.)", endings: &[
"\u{05B5}\u{05D9}\u{05DB}\u{05B6}\u{05DD}", "\u{05B4}\u{05D9}\u{05DB}\u{05B6}\u{05DD}", "\u{05DB}\u{05BC}\u{05B6}\u{05DD}", "\u{05DB}\u{05B6}\u{05DD}", ]},
PronounSuffix { key: "2fs", meaning: "you (f.)", endings: &[
"\u{05B7}\u{05D9}\u{05B4}\u{05DA}\u{05B0}", "\u{05B8}\u{05D9}\u{05B4}\u{05DA}\u{05B0}", "\u{05B4}\u{05D9}\u{05DA}\u{05B0}", "\u{05B8}\u{05DA}\u{05B0}", "\u{05B5}\u{05DA}\u{05B0}", ]},
PronounSuffix { key: "3fp", meaning: "them (f.)", endings: &[
"\u{05B5}\u{05D9}\u{05D4}\u{05B6}\u{05DF}", "\u{05B4}\u{05D9}\u{05D4}\u{05B6}\u{05DF}", "\u{05D4}\u{05B6}\u{05DF}", ]},
];
pub fn pronoun_suffix(key: &str) -> Option<&'static PronounSuffix> {
PRONOUN_SUFFIXES.iter().find(|p| p.key == key)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SuffixSplit {
pub key: &'static str,
pub meaning: &'static str,
pub stem: String,
pub suffix: String,
}
fn is_mark(c: char) -> bool {
('\u{0591}'..='\u{05C7}').contains(&c)
}
struct Cluster {
base: char,
marks: Vec<(usize, char)>,
start: usize,
}
fn clusters(s: &str) -> Vec<Cluster> {
let mut out: Vec<Cluster> = Vec::new();
for (i, c) in s.char_indices() {
if is_mark(c) {
if let Some(last) = out.last_mut() {
last.marks.push((i, c));
}
} else {
out.push(Cluster {
base: c,
marks: Vec::new(),
start: i,
});
}
}
out
}
fn mark_set(marks: &[(usize, char)]) -> Vec<char> {
let mut m: Vec<char> = marks.iter().map(|&(_, c)| c).collect();
m.sort_unstable();
m
}
fn match_ending(surface_clusters: &[Cluster], ending: &str) -> Option<usize> {
let mut lead: Vec<char> = Vec::new();
let mut full: Vec<(char, Vec<char>)> = Vec::new();
for c in ending.chars() {
if is_mark(c) {
match full.last_mut() {
Some((_, marks)) => marks.push(c),
None => lead.push(c),
}
} else {
full.push((c, Vec::new()));
}
}
for (_, marks) in &mut full {
marks.sort_unstable();
}
let n = surface_clusters.len().checked_sub(full.len())?;
if n == 0 {
return None;
}
for (ec, sc) in full.iter().zip(&surface_clusters[n..]) {
if ec.0 != sc.base || ec.1 != mark_set(&sc.marks) {
return None;
}
}
let host = &surface_clusters[n - 1];
if lead.is_empty() {
return Some(surface_clusters[n].start);
}
let host_set = mark_set(&host.marks);
if !lead.iter().all(|m| host_set.contains(m)) {
return None;
}
host.marks
.iter()
.find(|(_, c)| lead.contains(c))
.map(|&(i, _)| i)
}
pub fn split_pronoun_suffix(surface: &str) -> Option<SuffixSplit> {
pronoun_suffix_splits(surface).into_iter().next()
}
pub fn pronoun_suffix_splits(surface: &str) -> Vec<SuffixSplit> {
let sc = clusters(surface);
let mut all: Vec<(&'static PronounSuffix, usize, usize)> = Vec::new();
for p in PRONOUN_SUFFIXES {
for e in p.endings {
if let Some(at) = match_ending(&sc, e) {
all.push((p, at, e.len()));
}
}
}
all.sort_by_key(|item| std::cmp::Reverse(item.2));
all.into_iter()
.map(|(p, at, _)| SuffixSplit {
key: p.key,
meaning: p.meaning,
stem: surface[..at].to_string(),
suffix: surface[at..].to_string(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn split(s: &str) -> SuffixSplit {
split_pronoun_suffix(s).unwrap_or_else(|| panic!("no split for {s}"))
}
#[test]
fn splits_rejoin_to_the_surface() {
for s in [
"אֵלַי",
"אֵלָי",
"עָלָיו",
"אֵלֶיךָ",
"עָלֶיהָ",
"לוֹ",
"בָּהּ",
"מִמֶּנּוּ",
"מִמֶּנִּי",
"מִמֶּנָּה",
"מִמֶּךָּ",
"לָהֶם",
"מֵהֶם",
"עֲלֵיהֶם",
"אֶתְכֶם",
"עֲלֵיכֶם",
"לָנוּ",
"אֵלֵינוּ",
"עָלַיִךְ",
"לָךְ",
"אֹתָם",
"לָמוֹ",
"כָּמֹהוּ",
"עֲלֵיהֶן",
"לָהֶן",
"עִמָּדִי",
] {
let sp = split(s);
assert_eq!(format!("{}{}", sp.stem, sp.suffix), s, "rejoin {s}");
}
}
#[test]
fn person_and_span_are_right() {
assert_eq!(split("אֵלַי").key, "1cs");
assert_eq!(split("אֵלָי").key, "1cs");
assert_eq!(split("אֵלָי").stem, "אֵל");
assert_eq!(split("אֵלַי").meaning, "me");
assert_eq!(split("עָלָיו").key, "3ms");
assert_eq!(split("עָלָיו").suffix, "\u{05B8}\u{05D9}\u{05D5}");
assert_eq!(split("לוֹ").key, "3ms");
assert_eq!(split("לָמוֹ").key, "3mp");
assert_eq!(split("עִמּוֹ").key, "3ms");
assert_eq!(split("עֲלֵיהֶם").suffix.chars().count(), 5);
assert_eq!(split("מִמֶּנּוּ").key, "1cp");
assert_eq!(split("מִמֶּנִּי").key, "1cs");
assert_eq!(split("מִמֶּנָּה").key, "3fs");
assert_eq!(split("אָבִינוּ").key, "1cp");
assert_eq!(split("אָבִינוּ").stem, "אָב");
assert_eq!(split("אָבִיהָ").key, "3fs");
assert_eq!(split("אָחִיךְ").key, "2fs");
assert_eq!(split("אָבִיךָ").key, "2ms");
assert_eq!(split("אֲבִיהֶם").key, "3mp");
assert_eq!(split("אֲבִיכֶם").key, "2mp");
assert_eq!(split("אֲבִיהֶן").key, "3fp");
assert_eq!(split("פִּיהוּ").key, "3ms");
}
#[test]
fn meanings_are_distinct_quiz_options() {
let mut seen = std::collections::HashSet::new();
for p in PRONOUN_SUFFIXES {
assert!(seen.insert(p.meaning), "duplicate meaning {}", p.meaning);
assert!(pronoun_suffix(p.key).is_some());
}
}
#[test]
fn unsuffixed_function_words_do_not_split() {
for s in ["אֶת", "אֵת", "עַל", "אֶל", "מִן", "בְּעַד", "כָּל"] {
assert!(split_pronoun_suffix(s).is_none(), "{s} should not split");
}
}
}