const MAP: &[(char, char, char)] = &[
('A', '\u{05D0}', '\u{0710}'),
('B', '\u{05D1}', '\u{0712}'),
('G', '\u{05D2}', '\u{0713}'),
('D', '\u{05D3}', '\u{0715}'),
('H', '\u{05D4}', '\u{0717}'),
('O', '\u{05D5}', '\u{0718}'),
('Z', '\u{05D6}', '\u{0719}'),
('K', '\u{05D7}', '\u{071A}'),
('Y', '\u{05D8}', '\u{071B}'),
(';', '\u{05D9}', '\u{071D}'),
('C', '\u{05DB}', '\u{071F}'),
('L', '\u{05DC}', '\u{0720}'),
('M', '\u{05DE}', '\u{0721}'),
('N', '\u{05E0}', '\u{0722}'),
('S', '\u{05E1}', '\u{0723}'),
('E', '\u{05E2}', '\u{0725}'),
('I', '\u{05E4}', '\u{0726}'),
('/', '\u{05E6}', '\u{0728}'),
('X', '\u{05E7}', '\u{0729}'),
('R', '\u{05E8}', '\u{072A}'),
('W', '\u{05E9}', '\u{072B}'),
('T', '\u{05EA}', '\u{072C}'),
('\'', '\u{05BC}', '\u{0741}'), (',', '\u{05BF}', '\u{0742}'), ('a', '\u{05B7}', '\u{0731}'), ('e', '\u{05B5}', '\u{0737}'), ('i', '\u{05B4}', '\u{073B}'), ('o', '\u{05B8}', '\u{0734}'), ('u', '\u{05BB}', '\u{073E}'), ('*', '\u{0308}', '\u{0308}'), ('_', '_', '\u{0748}'), ('-', '-', '-'),
];
pub fn sedra_to_hebrew(word: &str) -> String {
word.chars()
.map(|c| {
MAP.iter()
.find(|(s, _, _)| *s == c)
.map_or(c, |(_, h, _)| *h)
})
.collect()
}
pub fn hebrew_to_syriac(text: &str) -> String {
text.chars()
.map(|c| {
MAP.iter()
.find(|(_, h, _)| *h == c)
.map_or(c, |(_, _, s)| *s)
})
.collect()
}
pub fn syriac_to_hebrew(text: &str) -> String {
text.chars()
.map(|c| {
MAP.iter()
.find(|(_, _, s)| *s == c)
.map_or(c, |(_, h, _)| *h)
})
.collect()
}
pub fn hebrew_to_sedra(text: &str) -> String {
text.chars()
.map(|c| {
MAP.iter()
.find(|(_, h, _)| *h == c)
.map_or(c, |(s, _, _)| *s)
})
.collect()
}
pub fn hebrew_display(text: &str) -> String {
let stripped: String = text
.chars()
.filter(|&c| c != '\u{05BF}' && c != '_')
.collect();
stripped.split_inclusive(' ').map(final_form_word).collect()
}
fn final_form_word(token: &str) -> String {
let mut chars: Vec<char> = token.chars().collect();
if let Some(i) = chars.iter().rposition(|&c| is_hebrew_consonant(c)) {
chars[i] = final_form(chars[i]);
}
chars.into_iter().collect()
}
fn is_hebrew_consonant(c: char) -> bool {
('\u{05D0}'..='\u{05EA}').contains(&c)
}
pub fn lookup_key(word: &str) -> String {
word.chars()
.filter(|&c| c != '\u{05BF}' && c != '_')
.map(medial_form)
.collect()
}
fn medial_form(c: char) -> char {
match c {
'\u{05DA}' => '\u{05DB}', '\u{05DD}' => '\u{05DE}', '\u{05DF}' => '\u{05E0}', '\u{05E3}' => '\u{05E4}', '\u{05E5}' => '\u{05E6}', other => other,
}
}
fn final_form(c: char) -> char {
match c {
'\u{05DB}' => '\u{05DA}', '\u{05DE}' => '\u{05DD}', '\u{05E0}' => '\u{05DF}', '\u{05E4}' => '\u{05E3}', '\u{05E6}' => '\u{05E5}', other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn columns_are_bijective() {
for (i, a) in MAP.iter().enumerate() {
for b in &MAP[i + 1..] {
assert_ne!(a.1, b.1, "duplicate Hebrew code point {:?}", a.1);
assert_ne!(a.2, b.2, "duplicate Syriac code point {:?}", a.2);
}
}
}
#[test]
fn matthew_1_1_first_word_round_trips() {
let heb = sedra_to_hebrew("C'ToBoA");
let syr = hebrew_to_syriac(&heb);
assert_eq!(syriac_to_hebrew(&syr), heb);
assert!(heb.contains('\u{05BC}'));
assert!(syr.contains('\u{0741}'));
}
#[test]
fn sedra_hebrew_round_trips() {
for word in ["AoAaR", "B'oAAaR", "C'ToBoA", "B,A*", "BR-ABA", "A;XNON"] {
assert_eq!(hebrew_to_sedra(&sedra_to_hebrew(word)), word);
}
}
#[test]
fn display_drops_rafe_and_folds_finals() {
let stored = sedra_to_hebrew("AaB,oHoT,C,uON");
assert!(stored.contains('\u{05BF}'));
let shown = hebrew_display(&stored);
assert!(!shown.contains('\u{05BF}'), "rafe must be gone");
assert!(shown.ends_with('\u{05DF}'), "trailing nun → final nun");
let stored_key: String = stored.chars().filter(|&c| c != '\u{05BF}').collect();
assert_eq!(lookup_key(&shown), stored_key);
}
#[test]
fn linea_occultans_drops_in_hebrew_renders_in_syriac() {
let stored = sedra_to_hebrew("OLaAKaOH_;");
assert!(stored.contains('_'));
assert!(
!hebrew_display(&stored).contains('_'),
"underscore must be gone"
);
let syr = hebrew_to_syriac(&stored);
assert!(syr.contains('\u{0748}') && !syr.contains('_'));
assert_eq!(syriac_to_hebrew(&syr), stored); let key: String = stored
.chars()
.filter(|&c| c != '\u{05BF}' && c != '_')
.collect();
assert_eq!(lookup_key(&hebrew_display(&stored)), key);
}
#[test]
fn lookup_key_matches_raw_and_display() {
let stored = sedra_to_hebrew("C'T,oB,oA");
let shown = hebrew_display(&stored);
let expected: String = stored.chars().filter(|&c| c != '\u{05BF}').collect();
assert_eq!(lookup_key(&shown), expected);
assert_eq!(lookup_key(&stored), expected);
}
#[test]
fn rukkakha_and_seyame_survive() {
let heb = sedra_to_hebrew("B,A*");
assert!(heb.contains('\u{05BF}')); assert!(heb.contains('\u{0308}')); let syr = hebrew_to_syriac(&heb);
assert!(syr.contains('\u{0742}')); assert_eq!(syriac_to_hebrew(&syr), heb);
}
}