use super::syllable::Inventory;
pub fn encode(syllable: &str) -> Option<[u8; 2]> {
let syllable = syllable.as_bytes();
if syllable.is_empty() || syllable.iter().any(|byte| !byte.is_ascii_lowercase()) {
return None;
}
if syllable == b"m" {
return Some(*b"om");
}
if syllable == b"ng" {
return Some(*b"ov");
}
let (initial, rest) = if syllable.starts_with(b"gw") {
(b'i', &syllable[2..])
} else if syllable.starts_with(b"kw") {
(b'u', &syllable[2..])
} else if syllable.starts_with(b"ng") {
(b'v', &syllable[2..])
} else if syllable.starts_with(b"j") {
(b'y', &syllable[1..])
} else if b"bpmfdtnlgkhzcsw".contains(&syllable[0]) {
(syllable[0], &syllable[1..])
} else {
(b'o', syllable)
};
let final_key = match rest {
b"a" | b"aa" => b'a',
b"ou" => b'b',
b"aam" | b"aap" => b'c',
b"am" | b"ap" => b'd',
b"e" => b'e',
b"an" | b"at" => b'f',
b"ang" | b"ak" | b"eng" => b'g',
b"aang" | b"aak" => b'h',
b"i" => b'i',
b"aan" | b"aat" => b'j',
b"aau" | b"au" => b'k',
b"aai" | b"ai" => b'l',
b"im" | b"ip" => b'm',
b"in" => b'n',
b"o" | b"oi" => b'o',
b"un" | b"ut" | b"ung" | b"eon" => b'p',
b"iu" => b'q',
b"yun" | b"yut" | b"uk" => b'r',
b"on" | b"ong" | b"ok" | b"ot" => b's',
b"it" | b"ek" => b't',
b"u" => b'u',
b"yu" | b"ui" => b'v',
b"oe" => b'w',
b"oeng" | b"oek" => b'x',
b"eoi" | b"eot" => b'y',
b"ei" => b'z',
b"ing" | b"ik" => b';',
_ => return None,
};
Some([initial, final_key])
}
pub fn decode(inventory: &Inventory, code: [u8; 2]) -> Vec<String> {
let mut values = inventory
.syllables()
.filter(|syllable| encode(syllable) == Some(code))
.map(str::to_owned)
.collect::<Vec<_>>();
values.sort_unstable();
values
}
pub fn initial_prefixes(inventory: &Inventory, key: u8) -> Vec<String> {
let mut prefixes = inventory
.syllables()
.filter(|syllable| encode(syllable).is_some_and(|code| code[0] == key))
.map(|syllable| {
let length = if matches!(syllable, "m" | "ng") {
syllable.len()
} else if matches!(key, b'i' | b'u' | b'v') {
2
} else {
1
};
syllable[..length].to_owned()
})
.collect::<Vec<_>>();
prefixes.sort_unstable();
prefixes.dedup();
prefixes
}
pub fn key_spans(inventory: &Inventory, prefix: &str, key: u8) -> Vec<(String, bool)> {
let on_key = |syllable: &str| encode(syllable).is_some_and(|code| code[0] == key);
let on_other_key = |syllable: &str| encode(syllable).is_some_and(|code| code[0] != key);
let under_prefix = inventory
.syllables()
.filter(|syllable| syllable.starts_with(prefix))
.collect::<Vec<_>>();
let mut spans = Vec::new();
let mut pending = vec![prefix.to_owned()];
while let Some(span) = pending.pop() {
let under = under_prefix
.iter()
.copied()
.filter(|syllable| syllable.starts_with(span.as_str()))
.collect::<Vec<_>>();
if !under.iter().any(|syllable| on_key(syllable)) {
continue;
}
if !under.iter().any(|syllable| on_other_key(syllable)) {
spans.push((span, false));
continue;
}
if under.contains(&span.as_str()) && on_key(&span) {
spans.push((span.clone(), true));
}
let mut next = under
.iter()
.filter(|syllable| syllable.len() > span.len())
.map(|syllable| syllable.as_bytes()[span.len()])
.collect::<Vec<_>>();
next.sort_unstable();
next.dedup();
for byte in next {
let mut longer = span.clone();
longer.push(char::from(byte));
pending.push(longer);
}
}
spans.sort_unstable();
spans
}
#[cfg(test)]
mod tests {
use super::{decode, encode, initial_prefixes, key_spans};
use crate::cantonese::Inventory;
#[test]
fn encodes_examples_without_copying_a_dictionary() {
for (syllable, code) in [
("ngo", "vo"),
("nei", "nz"),
("hou", "hb"),
("keoi", "ky"),
("leng", "lg"),
("gwong", "is"),
("gwaan", "ij"),
("oi", "oo"),
("sek", "st"),
("m", "om"),
("ng", "ov"),
] {
let bytes = code.as_bytes();
assert_eq!(encode(syllable), Some([bytes[0], bytes[1]]), "{syllable}");
}
}
#[test]
fn rejects_unknown_or_malformed_syllables() {
assert_eq!(encode(""), None);
assert_eq!(encode("NG"), None);
assert_eq!(encode("gx"), None);
assert_eq!(encode("gwong2"), None);
}
#[test]
fn reverse_lookup_is_bounded_by_the_loaded_inventory() {
let inventory = Inventory::new(["ngo", "ngoi", "nei", "gaa", "gai"]);
assert_eq!(decode(&inventory, *b"vo"), vec!["ngo", "ngoi"]);
assert_eq!(decode(&inventory, *b"nz"), vec!["nei"]);
assert_eq!(decode(&inventory, *b"ga"), vec!["gaa"]);
assert!(decode(&inventory, *b"zz").is_empty());
}
#[test]
fn a_lone_initial_key_lists_the_jyutping_spellings_it_starts() {
let inventory = Inventory::new([
"gwong", "gaa", "ngo", "nei", "m", "ng", "oi", "aa", "maa", "jan",
]);
assert_eq!(initial_prefixes(&inventory, b'i'), ["gw"]);
assert_eq!(initial_prefixes(&inventory, b'g'), ["g"]);
assert_eq!(initial_prefixes(&inventory, b'v'), ["ng"]);
assert_eq!(initial_prefixes(&inventory, b'n'), ["n"]);
assert_eq!(initial_prefixes(&inventory, b'y'), ["j"]);
assert_eq!(initial_prefixes(&inventory, b'm'), ["m"]);
assert_eq!(initial_prefixes(&inventory, b'o'), ["a", "m", "ng", "o"]);
assert!(initial_prefixes(&inventory, b'x').is_empty());
}
#[test]
fn key_spans_split_a_prefix_around_syllables_of_other_keys() {
let inventory = Inventory::new([
"gaa", "gei", "go", "gwong", "gwaa", "nei", "naa", "ngo", "ng", "m", "maa", "mui",
"daa", "daai", "deu",
]);
assert_eq!(key_spans(&inventory, "d", b'd'), [("d".to_owned(), false)]);
assert_eq!(
key_spans(&inventory, "g", b'g'),
[
("ga".to_owned(), false),
("ge".to_owned(), false),
("go".to_owned(), false)
]
);
assert_eq!(
key_spans(&inventory, "n", b'n'),
[("na".to_owned(), false), ("ne".to_owned(), false)]
);
assert_eq!(key_spans(&inventory, "m", b'o'), [("m".to_owned(), true)]);
assert_eq!(key_spans(&inventory, "ng", b'o'), [("ng".to_owned(), true)]);
assert_eq!(
key_spans(&inventory, "m", b'm'),
[("ma".to_owned(), false), ("mu".to_owned(), false)]
);
assert!(key_spans(&inventory, "g", b'x').is_empty());
}
}