1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum Kind {
6 Initial,
7 Medial,
8 Rime,
9}
10
11pub const DACHEN: [(u8, char, Kind); 37] = [
13 (b'1', 'ㄅ', Kind::Initial),
14 (b'q', 'ㄆ', Kind::Initial),
15 (b'a', 'ㄇ', Kind::Initial),
16 (b'z', 'ㄈ', Kind::Initial),
17 (b'2', 'ㄉ', Kind::Initial),
18 (b'w', 'ㄊ', Kind::Initial),
19 (b's', 'ㄋ', Kind::Initial),
20 (b'x', 'ㄌ', Kind::Initial),
21 (b'e', 'ㄍ', Kind::Initial),
22 (b'd', 'ㄎ', Kind::Initial),
23 (b'c', 'ㄏ', Kind::Initial),
24 (b'r', 'ㄐ', Kind::Initial),
25 (b'f', 'ㄑ', Kind::Initial),
26 (b'v', 'ㄒ', Kind::Initial),
27 (b'5', 'ㄓ', Kind::Initial),
28 (b't', 'ㄔ', Kind::Initial),
29 (b'g', 'ㄕ', Kind::Initial),
30 (b'b', 'ㄖ', Kind::Initial),
31 (b'y', 'ㄗ', Kind::Initial),
32 (b'h', 'ㄘ', Kind::Initial),
33 (b'n', 'ㄙ', Kind::Initial),
34 (b'u', 'ㄧ', Kind::Medial),
35 (b'j', 'ㄨ', Kind::Medial),
36 (b'm', 'ㄩ', Kind::Medial),
37 (b'8', 'ㄚ', Kind::Rime),
38 (b'i', 'ㄛ', Kind::Rime),
39 (b'k', 'ㄜ', Kind::Rime),
40 (b',', 'ㄝ', Kind::Rime),
41 (b'9', 'ㄞ', Kind::Rime),
42 (b'o', 'ㄟ', Kind::Rime),
43 (b'l', 'ㄠ', Kind::Rime),
44 (b'.', 'ㄡ', Kind::Rime),
45 (b'0', 'ㄢ', Kind::Rime),
46 (b'p', 'ㄣ', Kind::Rime),
47 (b';', 'ㄤ', Kind::Rime),
48 (b'/', 'ㄥ', Kind::Rime),
49 (b'-', 'ㄦ', Kind::Rime),
50];
51
52pub const DACHEN_SYMBOLS: &str = "1234567890,./;- ";
54
55pub const TONE_KEYS: [u8; 5] = *b" 6347";
57
58pub const TONE_MARKS: [&str; 5] = ["", "ˊ", "ˇ", "ˋ", "˙"];
60
61pub const IDLE_SYMBOLS: &str = "125890,./;-";
63
64pub const SHIFT_PUNCTUATION: [(u8, char); 8] = [
66 (b'<', ','),
67 (b'>', '。'),
68 (b'?', '?'),
69 (b':', ':'),
70 (b'[', '「'),
71 (b']', '」'),
72 (b'{', '『'),
73 (b'}', '』'),
74];
75
76pub fn symbol(key: u8) -> Option<(char, Kind)> {
78 DACHEN
79 .iter()
80 .find(|(dachen_key, _, _)| *dachen_key == key)
81 .map(|(_, symbol, kind)| (*symbol, *kind))
82}
83
84pub fn tone_mark(key: u8) -> Option<&'static str> {
86 TONE_KEYS
87 .iter()
88 .position(|tone_key| *tone_key == key)
89 .map(|index| TONE_MARKS[index])
90}
91
92#[cfg(test)]
93mod tests {
94 use std::collections::HashSet;
95
96 use super::*;
97
98 #[test]
99 fn dachen_has_37_unique_keys_and_symbols() {
100 let keys: HashSet<u8> = DACHEN.iter().map(|(key, _, _)| *key).collect();
101 let symbols: HashSet<char> = DACHEN.iter().map(|(_, symbol, _)| *symbol).collect();
102 assert_eq!(keys.len(), 37);
103 assert_eq!(symbols.len(), 37);
104 let count = |kind| DACHEN.iter().filter(|entry| entry.2 == kind).count();
105 assert_eq!(
106 (count(Kind::Initial), count(Kind::Medial), count(Kind::Rime)),
107 (21, 3, 13)
108 );
109 let block: HashSet<char> = ('\u{3105}'..='\u{3129}').collect();
111 assert_eq!(symbols, block);
112 }
113
114 #[test]
115 fn tone_keys_are_not_phonetic_keys() {
116 for key in TONE_KEYS {
117 assert_eq!(symbol(key), None, "{}", key as char);
118 }
119 assert_eq!(tone_mark(b' '), Some(""));
120 assert_eq!(tone_mark(b'6'), Some("ˊ"));
121 assert_eq!(tone_mark(b'7'), Some("˙"));
122 assert_eq!(tone_mark(b'1'), None);
123 assert_eq!(symbol(b'1'), Some(('ㄅ', Kind::Initial)));
124 assert_eq!(symbol(b'-'), Some(('ㄦ', Kind::Rime)));
125 }
126
127 #[test]
128 fn symbol_strings_follow_the_layout() {
129 let non_letters: HashSet<u8> = DACHEN
131 .iter()
132 .map(|(key, _, _)| *key)
133 .filter(|key| !key.is_ascii_alphabetic())
134 .chain(TONE_KEYS)
135 .collect();
136 assert_eq!(DACHEN_SYMBOLS.len(), non_letters.len());
137 assert!(DACHEN_SYMBOLS.bytes().all(|key| non_letters.contains(&key)));
138
139 let idle: String = DACHEN_SYMBOLS
140 .chars()
141 .filter(|key| !TONE_KEYS.contains(&(*key as u8)))
142 .collect();
143 assert_eq!(IDLE_SYMBOLS, idle);
144 for excluded in *b"3467 " {
145 assert!(!IDLE_SYMBOLS.as_bytes().contains(&excluded));
146 }
147 assert!(IDLE_SYMBOLS.bytes().all(|key| symbol(key).is_some()));
148 }
149
150 #[test]
151 fn shift_punctuation_keys_are_not_phonetic() {
152 for (key, _) in SHIFT_PUNCTUATION {
153 assert_eq!(symbol(key), None);
154 assert_eq!(tone_mark(key), None);
155 }
156 }
157}