use rusqlite::Connection;
#[derive(Debug, Clone)]
pub struct CuratedGloss {
pub gloss: String,
pub note: Option<String>,
}
pub fn vocab_key(surface: &str) -> String {
let mut out = String::new();
let mut marks: Vec<u32> = Vec::new();
for c in surface.chars() {
let u = c as u32;
let is_mark = (0x0591..=0x05C7).contains(&u);
if !is_mark {
flush_marks(&mut out, &mut marks);
out.push(c);
} else if u != 0x05BC && u != 0x05BD && !(0x0591..=0x05AF).contains(&u) {
marks.push(u);
}
}
flush_marks(&mut out, &mut marks);
out
}
fn flush_marks(out: &mut String, marks: &mut Vec<u32>) {
marks.sort_unstable();
for &m in marks.iter() {
if let Some(ch) = char::from_u32(m) {
out.push(ch);
}
}
marks.clear();
}
pub fn curated_gloss(db: &Connection, surface: &str) -> Option<CuratedGloss> {
let key = vocab_key(surface);
let mut stmt = db
.prepare("SELECT surface, gloss, note FROM lexdb.word_glosses")
.ok()?;
stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
))
})
.ok()?
.flatten()
.find_map(|(stored, gloss, note)| {
(vocab_key(&stored) == key && !gloss.is_empty()).then_some(CuratedGloss { gloss, note })
})
}
pub fn curated_name(db: &Connection, surface: &str) -> bool {
let key = vocab_key(surface);
let Ok(mut stmt) = db.prepare("SELECT surface FROM lexdb.word_glosses WHERE is_name = 1")
else {
return false;
};
stmt.query_map([], |row| row.get::<_, String>(0))
.ok()
.into_iter()
.flatten()
.flatten()
.any(|stored| vocab_key(&stored) == key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vocab_key_drops_dagesh_and_sorts_marks() {
assert_eq!(vocab_key("בֶּן"), vocab_key("בֶן"));
assert_eq!(vocab_key("כָּל"), vocab_key("כָל"));
assert_eq!(vocab_key("דָּבָר"), vocab_key("דָּבָר\u{0591}"));
let nfc: String = ['\u{05DE}', '\u{05B6}', '\u{05BC}'].iter().collect();
let traditional: String = ['\u{05DE}', '\u{05BC}', '\u{05B6}'].iter().collect();
assert_eq!(vocab_key(&nfc), vocab_key(&traditional));
assert_ne!(vocab_key("עַם"), vocab_key("עִם"));
assert_ne!(vocab_key("שׁ"), vocab_key("שׂ"));
}
#[test]
fn curated_gloss_matches_dagesh_variants() {
if !std::path::Path::new("data/lexicon.db").exists() {
return;
}
let db = Connection::open_in_memory().unwrap();
db.execute_batch("ATTACH DATABASE 'data/lexicon.db' AS lexdb")
.unwrap();
assert!(curated_gloss(&db, "הַדָּבָר").is_some());
assert_eq!(curated_gloss(&db, "אֶת").unwrap().gloss, "←");
assert_eq!(curated_gloss(&db, "אֵת").unwrap().gloss, "←");
assert_eq!(curated_gloss(&db, "וְאֶת").unwrap().gloss, "and ←");
assert_eq!(curated_gloss(&db, "וְאֵת").unwrap().gloss, "and ←");
assert!(curated_gloss(&db, "כִּי").unwrap().note.is_none());
assert!(curated_gloss(&db, "בָּרָא").is_none());
}
}