1use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use super::layout::TONE_MARKS;
7
8pub const KEYPAD: [(u8, &str); 10] = [
10 (b'1', "ㄅㄆㄇㄈ"),
11 (b'2', "ㄉㄊㄋㄌ"),
12 (b'3', "ㄍㄎㄏ"),
13 (b'4', "ㄐㄑㄒ"),
14 (b'5', "ㄓㄔㄕㄖ"),
15 (b'6', "ㄗㄘㄙ"),
16 (b'7', "ㄚㄛㄜㄝ"),
17 (b'8', "ㄧㄨㄩㄦ"),
18 (b'9', "ㄞㄟㄠㄡ"),
19 (b'0', "ㄢㄣㄤㄥ"),
20];
21
22pub const TONE_KEYS: [u8; 5] = *b"zxcvb";
24
25pub const MAX_DIGITS: usize = 3;
27
28pub const IDLE_SYMBOLS: &str = "1234567890";
30
31pub const SYMBOLS: &str = "1234567890 ";
33
34pub const LIST_OPEN_SYMBOLS: &str = "1234567890";
36
37pub fn digit(symbol: char) -> Option<u8> {
39 KEYPAD
40 .iter()
41 .find(|(_, symbols)| symbols.contains(symbol))
42 .map(|(key, _)| *key)
43}
44
45pub fn tone_mark(key: u8) -> Option<&'static str> {
47 if key == b' ' {
48 return Some(TONE_MARKS[0]);
49 }
50 TONE_KEYS
51 .iter()
52 .position(|tone_key| *tone_key == key)
53 .map(|index| TONE_MARKS[index])
54}
55
56#[derive(Debug)]
58pub struct NineKeyIndex {
59 readings: HashMap<String, Arc<[String]>>,
61 prefixes: HashSet<String>,
63}
64
65impl NineKeyIndex {
66 pub fn new(syllables: Vec<String>) -> Self {
68 let mut groups: HashMap<String, Vec<String>> = HashMap::new();
69 let mut prefixes = HashSet::new();
70 for syllable in syllables {
71 let (symbols, mark) = split_tone(&syllable);
72 let Some(digits) = symbols.chars().map(digit).collect::<Option<Vec<u8>>>() else {
73 continue;
74 };
75 if digits.is_empty() || digits.len() > MAX_DIGITS {
76 continue;
77 }
78 let digits = String::from_utf8(digits).expect("keypad keys are ASCII digits");
79 for end in 1..=digits.len() {
80 prefixes.insert(digits[..end].to_owned());
81 }
82 groups
83 .entry(format!("{digits}{mark}"))
84 .or_default()
85 .push(syllable);
86 }
87 let readings = groups
88 .into_iter()
89 .map(|(code, mut readings)| {
90 readings.sort();
91 readings.dedup();
92 (code, Arc::from(readings))
93 })
94 .collect();
95 Self { readings, prefixes }
96 }
97
98 pub fn readings(&self, digits: &[u8], mark: &str) -> Option<Arc<[String]>> {
100 let digits = std::str::from_utf8(digits).ok()?;
101 let mut buffer = [0; MAX_DIGITS + char::MAX.len_utf8()];
103 let length = digits.len().checked_add(mark.len())?;
104 let code = buffer.get_mut(..length)?;
105 code[..digits.len()].copy_from_slice(digits.as_bytes());
106 code[digits.len()..].copy_from_slice(mark.as_bytes());
107 self.readings.get(std::str::from_utf8(code).ok()?).cloned()
108 }
109
110 pub fn accepts(&self, digits: &[u8]) -> bool {
112 std::str::from_utf8(digits).is_ok_and(|digits| self.prefixes.contains(digits))
113 }
114
115 #[cfg(test)]
117 fn group_count(&self) -> usize {
118 self.readings.len()
119 }
120}
121
122fn split_tone(syllable: &str) -> (&str, &str) {
124 match syllable.chars().next_back() {
125 Some(last) if TONE_MARKS[1..].iter().any(|mark| mark.starts_with(last)) => {
126 syllable.split_at(syllable.len() - last.len_utf8())
127 }
128 _ => (syllable, ""),
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::zhuyin::layout::DACHEN;
136
137 fn owned(syllables: &[&str]) -> Vec<String> {
138 syllables
139 .iter()
140 .map(|syllable| (*syllable).to_owned())
141 .collect()
142 }
143
144 #[test]
145 fn every_bopomofo_is_on_exactly_the_patent_key() {
146 let expected = [
148 ('1', "ㄅㄆㄇㄈ"),
149 ('2', "ㄉㄊㄋㄌ"),
150 ('3', "ㄍㄎㄏ"),
151 ('4', "ㄐㄑㄒ"),
152 ('5', "ㄓㄔㄕㄖ"),
153 ('6', "ㄗㄘㄙ"),
154 ('7', "ㄚㄛㄜㄝ"),
155 ('8', "ㄧㄨㄩㄦ"),
156 ('9', "ㄞㄟㄠㄡ"),
157 ('0', "ㄢㄣㄤㄥ"),
158 ];
159 for ((key, symbols), (expected_key, expected_symbols)) in KEYPAD.iter().zip(expected) {
160 assert_eq!(char::from(*key), expected_key);
161 assert_eq!(*symbols, expected_symbols);
162 }
163 let mut seen = HashSet::new();
165 for (_, symbol, _) in DACHEN {
166 let key = digit(symbol).unwrap_or_else(|| panic!("{symbol} has no key"));
167 let holders = KEYPAD
168 .iter()
169 .filter(|(_, symbols)| symbols.contains(symbol))
170 .count();
171 assert_eq!(holders, 1, "{symbol}");
172 assert!(KEYPAD.iter().any(|(k, s)| *k == key && s.contains(symbol)));
173 seen.insert(symbol);
174 }
175 assert_eq!(seen.len(), 37);
176 let total: usize = KEYPAD
177 .iter()
178 .map(|(_, symbols)| symbols.chars().count())
179 .sum();
180 assert_eq!(total, 37);
181 assert_eq!(digit('a'), None);
182 assert_eq!(digit('ˇ'), None);
183 }
184
185 #[test]
186 fn tone_keys_map_to_marks_and_space_is_the_first_tone() {
187 assert_eq!(tone_mark(b' '), Some(""));
188 assert_eq!(tone_mark(b'z'), Some(""));
189 assert_eq!(tone_mark(b'x'), Some("ˊ"));
190 assert_eq!(tone_mark(b'c'), Some("ˇ"));
191 assert_eq!(tone_mark(b'v'), Some("ˋ"));
192 assert_eq!(tone_mark(b'b'), Some("˙"));
193 for key in *b"1234567890a6347" {
194 assert_eq!(tone_mark(key), None, "{}", char::from(key));
195 }
196 }
197
198 #[test]
199 fn symbol_strings_hold_digits_and_space_only_while_composing() {
200 assert_eq!(IDLE_SYMBOLS, "1234567890");
201 assert_eq!(SYMBOLS, "1234567890 ");
202 assert_eq!(LIST_OPEN_SYMBOLS, "1234567890");
203 for symbols in [IDLE_SYMBOLS, SYMBOLS, LIST_OPEN_SYMBOLS] {
204 assert!(!symbols.contains([',', '.', '/', ';', '-']));
205 }
206 }
207
208 #[test]
209 fn index_groups_syllables_by_digits_and_tone() {
210 let index = NineKeyIndex::new(owned(&[
211 "ㄋㄧˇ",
212 "ㄌㄧˇ",
213 "ㄋㄧ",
214 "ㄇㄚ˙",
215 "ㄇㄚ",
216 "ㄅㄚ",
217 "ㄏㄠˇ",
218 "ㄓㄨㄤ",
219 "ㄢ",
220 ]));
221 assert_eq!(
222 index.readings(b"28", "ˇ").as_deref(),
223 Some(&owned(&["ㄋㄧˇ", "ㄌㄧˇ"])[..])
224 );
225 assert_eq!(
227 index.readings(b"28", "").as_deref(),
228 Some(&owned(&["ㄋㄧ"])[..])
229 );
230 assert_eq!(
231 index.readings(b"17", "˙").as_deref(),
232 Some(&owned(&["ㄇㄚ˙"])[..])
233 );
234 assert_eq!(
235 index.readings(b"17", "").as_deref(),
236 Some(&owned(&["ㄅㄚ", "ㄇㄚ"])[..])
237 );
238 assert_eq!(
239 index.readings(b"580", "").as_deref(),
240 Some(&owned(&["ㄓㄨㄤ"])[..])
241 );
242 assert_eq!(
243 index.readings(b"0", "").as_deref(),
244 Some(&owned(&["ㄢ"])[..])
245 );
246 assert_eq!(index.readings(b"28", "ˋ"), None);
247 assert_eq!(index.readings(b"2", "ˇ"), None);
248 assert_eq!(index.group_count(), 7);
249 }
250
251 #[test]
252 fn index_reading_lookups_do_not_allocate_temporary_keys() {
253 let syllables = TONE_MARKS
254 .iter()
255 .map(|mark| format!("ㄓㄨㄤ{mark}"))
256 .collect::<Vec<_>>();
257 let index = NineKeyIndex::new(syllables.clone());
258 let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
259 for (mark, expected) in TONE_MARKS.iter().zip(&syllables) {
260 let readings = index.readings(b"580", mark).unwrap();
261 assert_eq!(readings.as_ref(), std::slice::from_ref(expected));
262 }
263 assert!(index.readings(b"999", "ˇ").is_none());
264 });
265 assert_eq!(allocations, 0);
266 }
267
268 #[test]
269 fn index_reading_lookups_preserve_key_part_and_invalid_input_behavior() {
270 let index = NineKeyIndex::new(owned(&["ㄋㄧˇ"]));
271 let expected = index.readings(b"28", "ˇ").unwrap();
272 for (digits, mark) in [
273 (b"2".as_slice(), "8ˇ"),
274 ("28ˇ".as_bytes(), ""),
275 (b"".as_slice(), "28ˇ"),
276 ] {
277 let readings = index.readings(digits, mark).unwrap();
278 assert!(Arc::ptr_eq(&expected, &readings));
279 }
280 for (digits, mark) in [
281 (b"\xff".as_slice(), "ˇ"),
282 (b"28".as_slice(), "測試長調號"),
283 (b"2899999999".as_slice(), "ˇ"),
284 (b"".as_slice(), ""),
285 ] {
286 assert!(index.readings(digits, mark).is_none());
287 }
288 }
289
290 #[test]
291 fn index_accepts_only_prefixes_of_some_syllable() {
292 let index = NineKeyIndex::new(owned(&["ㄋㄧˇ", "ㄓㄨㄤ", "ㄢ"]));
293 for accepted in ["2", "28", "5", "58", "580", "0"] {
294 assert!(index.accepts(accepted.as_bytes()), "{accepted}");
295 }
296 for rejected in ["", "1", "29", "282", "5800", "00"] {
297 assert!(!index.accepts(rejected.as_bytes()), "{rejected}");
298 }
299 }
300
301 #[test]
302 fn index_skips_symbols_off_the_keypad() {
303 let index = NineKeyIndex::new(owned(&["nei", "ㄋㄧˇ", "ㄋㄧㄠㄢ"]));
304 assert_eq!(index.group_count(), 1);
305 assert!(!index.accepts(b"2890"));
306 }
307}