use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use super::layout::TONE_MARKS;
pub const KEYPAD: [(u8, &str); 10] = [
(b'1', "ㄅㄆㄇㄈ"),
(b'2', "ㄉㄊㄋㄌ"),
(b'3', "ㄍㄎㄏ"),
(b'4', "ㄐㄑㄒ"),
(b'5', "ㄓㄔㄕㄖ"),
(b'6', "ㄗㄘㄙ"),
(b'7', "ㄚㄛㄜㄝ"),
(b'8', "ㄧㄨㄩㄦ"),
(b'9', "ㄞㄟㄠㄡ"),
(b'0', "ㄢㄣㄤㄥ"),
];
pub const TONE_KEYS: [u8; 5] = *b"zxcvb";
pub const MAX_DIGITS: usize = 3;
pub const IDLE_SYMBOLS: &str = "1234567890";
pub const SYMBOLS: &str = "1234567890 ";
pub const LIST_OPEN_SYMBOLS: &str = "1234567890";
pub fn digit(symbol: char) -> Option<u8> {
KEYPAD
.iter()
.find(|(_, symbols)| symbols.contains(symbol))
.map(|(key, _)| *key)
}
pub fn tone_mark(key: u8) -> Option<&'static str> {
if key == b' ' {
return Some(TONE_MARKS[0]);
}
TONE_KEYS
.iter()
.position(|tone_key| *tone_key == key)
.map(|index| TONE_MARKS[index])
}
#[derive(Debug)]
pub struct NineKeyIndex {
readings: HashMap<String, Arc<[String]>>,
prefixes: HashSet<String>,
}
impl NineKeyIndex {
pub fn new(syllables: Vec<String>) -> Self {
let mut groups: HashMap<String, Vec<String>> = HashMap::new();
let mut prefixes = HashSet::new();
for syllable in syllables {
let (symbols, mark) = split_tone(&syllable);
let Some(digits) = symbols.chars().map(digit).collect::<Option<Vec<u8>>>() else {
continue;
};
if digits.is_empty() || digits.len() > MAX_DIGITS {
continue;
}
let digits = String::from_utf8(digits).expect("keypad keys are ASCII digits");
for end in 1..=digits.len() {
prefixes.insert(digits[..end].to_owned());
}
groups
.entry(format!("{digits}{mark}"))
.or_default()
.push(syllable);
}
let readings = groups
.into_iter()
.map(|(code, mut readings)| {
readings.sort();
readings.dedup();
(code, Arc::from(readings))
})
.collect();
Self { readings, prefixes }
}
pub fn readings(&self, digits: &[u8], mark: &str) -> Option<Arc<[String]>> {
let digits = std::str::from_utf8(digits).ok()?;
let mut buffer = [0; MAX_DIGITS + char::MAX.len_utf8()];
let length = digits.len().checked_add(mark.len())?;
let code = buffer.get_mut(..length)?;
code[..digits.len()].copy_from_slice(digits.as_bytes());
code[digits.len()..].copy_from_slice(mark.as_bytes());
self.readings.get(std::str::from_utf8(code).ok()?).cloned()
}
pub fn accepts(&self, digits: &[u8]) -> bool {
std::str::from_utf8(digits).is_ok_and(|digits| self.prefixes.contains(digits))
}
#[cfg(test)]
fn group_count(&self) -> usize {
self.readings.len()
}
}
fn split_tone(syllable: &str) -> (&str, &str) {
match syllable.chars().next_back() {
Some(last) if TONE_MARKS[1..].iter().any(|mark| mark.starts_with(last)) => {
syllable.split_at(syllable.len() - last.len_utf8())
}
_ => (syllable, ""),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::zhuyin::layout::DACHEN;
fn owned(syllables: &[&str]) -> Vec<String> {
syllables
.iter()
.map(|syllable| (*syllable).to_owned())
.collect()
}
#[test]
fn every_bopomofo_is_on_exactly_the_patent_key() {
let expected = [
('1', "ㄅㄆㄇㄈ"),
('2', "ㄉㄊㄋㄌ"),
('3', "ㄍㄎㄏ"),
('4', "ㄐㄑㄒ"),
('5', "ㄓㄔㄕㄖ"),
('6', "ㄗㄘㄙ"),
('7', "ㄚㄛㄜㄝ"),
('8', "ㄧㄨㄩㄦ"),
('9', "ㄞㄟㄠㄡ"),
('0', "ㄢㄣㄤㄥ"),
];
for ((key, symbols), (expected_key, expected_symbols)) in KEYPAD.iter().zip(expected) {
assert_eq!(char::from(*key), expected_key);
assert_eq!(*symbols, expected_symbols);
}
let mut seen = HashSet::new();
for (_, symbol, _) in DACHEN {
let key = digit(symbol).unwrap_or_else(|| panic!("{symbol} has no key"));
let holders = KEYPAD
.iter()
.filter(|(_, symbols)| symbols.contains(symbol))
.count();
assert_eq!(holders, 1, "{symbol}");
assert!(KEYPAD.iter().any(|(k, s)| *k == key && s.contains(symbol)));
seen.insert(symbol);
}
assert_eq!(seen.len(), 37);
let total: usize = KEYPAD
.iter()
.map(|(_, symbols)| symbols.chars().count())
.sum();
assert_eq!(total, 37);
assert_eq!(digit('a'), None);
assert_eq!(digit('ˇ'), None);
}
#[test]
fn tone_keys_map_to_marks_and_space_is_the_first_tone() {
assert_eq!(tone_mark(b' '), Some(""));
assert_eq!(tone_mark(b'z'), Some(""));
assert_eq!(tone_mark(b'x'), Some("ˊ"));
assert_eq!(tone_mark(b'c'), Some("ˇ"));
assert_eq!(tone_mark(b'v'), Some("ˋ"));
assert_eq!(tone_mark(b'b'), Some("˙"));
for key in *b"1234567890a6347" {
assert_eq!(tone_mark(key), None, "{}", char::from(key));
}
}
#[test]
fn symbol_strings_hold_digits_and_space_only_while_composing() {
assert_eq!(IDLE_SYMBOLS, "1234567890");
assert_eq!(SYMBOLS, "1234567890 ");
assert_eq!(LIST_OPEN_SYMBOLS, "1234567890");
for symbols in [IDLE_SYMBOLS, SYMBOLS, LIST_OPEN_SYMBOLS] {
assert!(!symbols.contains([',', '.', '/', ';', '-']));
}
}
#[test]
fn index_groups_syllables_by_digits_and_tone() {
let index = NineKeyIndex::new(owned(&[
"ㄋㄧˇ",
"ㄌㄧˇ",
"ㄋㄧ",
"ㄇㄚ˙",
"ㄇㄚ",
"ㄅㄚ",
"ㄏㄠˇ",
"ㄓㄨㄤ",
"ㄢ",
]));
assert_eq!(
index.readings(b"28", "ˇ").as_deref(),
Some(&owned(&["ㄋㄧˇ", "ㄌㄧˇ"])[..])
);
assert_eq!(
index.readings(b"28", "").as_deref(),
Some(&owned(&["ㄋㄧ"])[..])
);
assert_eq!(
index.readings(b"17", "˙").as_deref(),
Some(&owned(&["ㄇㄚ˙"])[..])
);
assert_eq!(
index.readings(b"17", "").as_deref(),
Some(&owned(&["ㄅㄚ", "ㄇㄚ"])[..])
);
assert_eq!(
index.readings(b"580", "").as_deref(),
Some(&owned(&["ㄓㄨㄤ"])[..])
);
assert_eq!(
index.readings(b"0", "").as_deref(),
Some(&owned(&["ㄢ"])[..])
);
assert_eq!(index.readings(b"28", "ˋ"), None);
assert_eq!(index.readings(b"2", "ˇ"), None);
assert_eq!(index.group_count(), 7);
}
#[test]
fn index_reading_lookups_do_not_allocate_temporary_keys() {
let syllables = TONE_MARKS
.iter()
.map(|mark| format!("ㄓㄨㄤ{mark}"))
.collect::<Vec<_>>();
let index = NineKeyIndex::new(syllables.clone());
let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
for (mark, expected) in TONE_MARKS.iter().zip(&syllables) {
let readings = index.readings(b"580", mark).unwrap();
assert_eq!(readings.as_ref(), std::slice::from_ref(expected));
}
assert!(index.readings(b"999", "ˇ").is_none());
});
assert_eq!(allocations, 0);
}
#[test]
fn index_reading_lookups_preserve_key_part_and_invalid_input_behavior() {
let index = NineKeyIndex::new(owned(&["ㄋㄧˇ"]));
let expected = index.readings(b"28", "ˇ").unwrap();
for (digits, mark) in [
(b"2".as_slice(), "8ˇ"),
("28ˇ".as_bytes(), ""),
(b"".as_slice(), "28ˇ"),
] {
let readings = index.readings(digits, mark).unwrap();
assert!(Arc::ptr_eq(&expected, &readings));
}
for (digits, mark) in [
(b"\xff".as_slice(), "ˇ"),
(b"28".as_slice(), "測試長調號"),
(b"2899999999".as_slice(), "ˇ"),
(b"".as_slice(), ""),
] {
assert!(index.readings(digits, mark).is_none());
}
}
#[test]
fn index_accepts_only_prefixes_of_some_syllable() {
let index = NineKeyIndex::new(owned(&["ㄋㄧˇ", "ㄓㄨㄤ", "ㄢ"]));
for accepted in ["2", "28", "5", "58", "580", "0"] {
assert!(index.accepts(accepted.as_bytes()), "{accepted}");
}
for rejected in ["", "1", "29", "282", "5800", "00"] {
assert!(!index.accepts(rejected.as_bytes()), "{rejected}");
}
}
#[test]
fn index_skips_symbols_off_the_keypad() {
let index = NineKeyIndex::new(owned(&["nei", "ㄋㄧˇ", "ㄋㄧㄠㄢ"]));
assert_eq!(index.group_count(), 1);
assert!(!index.accepts(b"2890"));
}
}