Skip to main content

msime_engine/zhuyin/
layout.rs

1//! The Dachen keyboard: which ASCII key types which bopomofo symbol, the tone keys, and the non-letter keys the scheme claims in each state. This is the standard published Dachen (大千) layout, the same one libchewing's `standard.rs` implements.
2
3/// The slot a bopomofo symbol fills in a syllable. A key of a kind already present replaces that slot.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum Kind {
6    Initial,
7    Medial,
8    Rime,
9}
10
11/// The 37 phonetic keys of the Dachen layout.
12pub 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
52/// Every non-letter key the scheme can claim, phonetic and tone keys together, in the canonical order `spelling_symbols` lists them in.
53pub const DACHEN_SYMBOLS: &str = "1234567890,./;- ";
54
55/// Space is tone 1, then `6 3 4 7` are tones 2, 3, 4 and the neutral tone.
56pub const TONE_KEYS: [u8; 5] = *b" 6347";
57
58/// The mark each of `TONE_KEYS` appends to a syllable; tone 1 is unmarked.
59pub const TONE_MARKS: [&str; 5] = ["", "ˊ", "ˇ", "ˋ", "˙"];
60
61/// The non-letter keys that start a composition from idle: every phonetic non-letter key. The tone digits and space are left out, so they type themselves when nothing is composed.
62pub const IDLE_SYMBOLS: &str = "125890,./;-";
63
64/// Shifted keys that commit the conversion and then insert a full-width mark.
65pub const SHIFT_PUNCTUATION: [(u8, char); 8] = [
66    (b'<', ','),
67    (b'>', '。'),
68    (b'?', '?'),
69    (b':', ':'),
70    (b'[', '「'),
71    (b']', '」'),
72    (b'{', '『'),
73    (b'}', '』'),
74];
75
76/// The bopomofo symbol and slot of a phonetic key.
77pub 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
84/// The mark a tone key appends, `None` for any other key.
85pub 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        // The bopomofo block runs ㄅ (U+3105) to ㄩ (U+3129); the layout covers all of it.
110        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        // Every non-letter phonetic key and every tone key, nothing else, in canonical order.
130        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}