chewing/zhuyin/
bopomofo.rs

1use std::{
2    error::Error,
3    fmt::{Display, Write},
4    str::FromStr,
5};
6
7/// The category of the phonetic symbols
8///
9/// Zhuyin, or Bopomofo, consists of 37 letters and 4 tone marks. They are
10/// categorized into one of the four categories:
11///
12/// 1. Initial sounds: ㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙ
13/// 2. Medial glides: ㄧㄨㄩ
14/// 3. Rimes: ㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦ
15/// 4. Tonal marks: ˙ˊˇˋ
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum BopomofoKind {
18    /// Initial sounds: ㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙ
19    Initial,
20    /// Medial glides: ㄧㄨㄩ
21    Medial,
22    /// Rimes: ㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦ
23    Rime,
24    /// Tonal marks: ˙ˊˇˋ
25    Tone,
26}
27
28/// Zhuyin Fuhao, often shortened as zhuyin and commonly called bopomofo
29///
30/// <https://simple.m.wikipedia.org/wiki/Zhuyin>
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum Bopomofo {
33    /// Zhuyin Fuhao: ㄅ
34    B,
35    /// Zhuyin Fuhao: ㄆ
36    P,
37    /// Zhuyin Fuhao: ㄇ
38    M,
39    /// Zhuyin Fuhao: ㄈ
40    F,
41    /// Zhuyin Fuhao: ㄉ
42    D,
43    /// Zhuyin Fuhao: ㄊ
44    T,
45    /// Zhuyin Fuhao: ㄋ
46    N,
47    /// Zhuyin Fuhao: ㄌ
48    L,
49    /// Zhuyin Fuhao: ㄍ
50    G,
51    /// Zhuyin Fuhao: ㄎ
52    K,
53    /// Zhuyin Fuhao: ㄏ
54    H,
55    /// Zhuyin Fuhao: ㄐ
56    J,
57    /// Zhuyin Fuhao: ㄑ
58    Q,
59    /// Zhuyin Fuhao: ㄒ
60    X,
61    /// Zhuyin Fuhao: ㄓ
62    ZH,
63    /// Zhuyin Fuhao: ㄔ
64    CH,
65    /// Zhuyin Fuhao: ㄕ
66    SH,
67    /// Zhuyin Fuhao: ㄖ
68    R,
69    /// Zhuyin Fuhao: ㄗ
70    Z,
71    /// Zhuyin Fuhao: ㄘ
72    C,
73    /// Zhuyin Fuhao: ㄙ
74    S,
75    /// Zhuyin Fuhao: 一
76    I,
77    /// Zhuyin Fuhao: ㄨ
78    U,
79    /// Zhuyin Fuhao: ㄩ
80    IU,
81    /// Zhuyin Fuhao: ㄚ
82    A,
83    /// Zhuyin Fuhao: ㄛ
84    O,
85    /// Zhuyin Fuhao: ㄜ
86    E,
87    /// Zhuyin Fuhao: ㄝ
88    EH,
89    /// Zhuyin Fuhao: ㄞ
90    AI,
91    /// Zhuyin Fuhao: ㄟ
92    EI,
93    /// Zhuyin Fuhao: ㄠ
94    AU,
95    /// Zhuyin Fuhao: ㄡ
96    OU,
97    /// Zhuyin Fuhao: ㄢ
98    AN,
99    /// Zhuyin Fuhao: ㄣ
100    EN,
101    /// Zhuyin Fuhao: ㄤ
102    ANG,
103    /// Zhuyin Fuhao: ㄥ
104    ENG,
105    /// Zhuyin Fuhao: ㄦ
106    ER,
107    /// Tonal mark: ˙
108    TONE5,
109    /// Tonal mark: ˊ
110    TONE2,
111    /// Tonal mark: ˇ
112    TONE3,
113    /// Tonal mark: ˋ
114    TONE4,
115    /// Tonal mark: ˉ
116    TONE1,
117}
118
119use Bopomofo::*;
120
121const INITIAL_MAP: [Bopomofo; 21] = [
122    B, P, M, F, D, T, N, L, G, K, H, J, Q, X, ZH, CH, SH, R, Z, C, S,
123];
124const MEDIAL_MAP: [Bopomofo; 3] = [I, U, IU];
125const RIME_MAP: [Bopomofo; 13] = [A, O, E, EH, AI, EI, AU, OU, AN, EN, ANG, ENG, ER];
126const TONE_MAP: [Bopomofo; 4] = [TONE5, TONE2, TONE3, TONE4];
127
128impl Bopomofo {
129    /// Returns [`BopomofoKind`] of the [`Bopomofo`] symbol. See [`BopomofoKind`] to know more about
130    /// each kind category.
131    pub const fn kind(&self) -> BopomofoKind {
132        match self {
133            B | P | M | F | D | T | N | L | G | K | H | J | Q | X | ZH | CH | SH | R | Z | C
134            | S => BopomofoKind::Initial,
135            I | U | IU => BopomofoKind::Medial,
136            A | O | E | EH | AI | EI | AU | OU | AN | EN | ANG | ENG | ER => BopomofoKind::Rime,
137            TONE1 | TONE2 | TONE3 | TONE4 | TONE5 => BopomofoKind::Tone,
138        }
139    }
140    /// Returns a [`Bopomofo`] that is categorized as initial sounds based on the index. It will
141    /// return [`None`] if the index is larger than 20. The index order is listed below starting
142    /// from 0.
143    ///
144    /// - Initial sounds: ㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙ
145    pub(super) const fn from_initial(index: u16) -> Option<Bopomofo> {
146        if index as usize >= INITIAL_MAP.len() {
147            return None;
148        }
149        Some(INITIAL_MAP[index as usize])
150    }
151    /// Returns a [`Bopomofo`] that is categorized as medial glides based on the index. It will
152    /// return [`None`] if the index is larger than 2. The index order is listed below starting
153    /// from 0.
154    ///
155    /// - Medial glides: ㄧㄨㄩ
156    pub(super) const fn from_medial(index: u16) -> Option<Bopomofo> {
157        if index as usize >= MEDIAL_MAP.len() {
158            return None;
159        }
160        Some(MEDIAL_MAP[index as usize])
161    }
162    /// Returns a [`Bopomofo`] that is categorized as rimes based on the index. It will
163    /// return [`None`] if the index is larger than 12. The index order is listed below starting
164    /// from 0.
165    ///
166    /// - Rimes: ㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦ
167    pub(super) const fn from_rime(index: u16) -> Option<Bopomofo> {
168        if index as usize >= RIME_MAP.len() {
169            return None;
170        }
171        Some(RIME_MAP[index as usize])
172    }
173    /// Returns a [`Bopomofo`] that is categorized as tonal marks based on the index. It will
174    /// return [`None`] if the index is larger than 3. The index order is listed below starting
175    /// from 0.
176    ///
177    /// - Tonal marks: ˙ˊˇˋ
178    pub(super) const fn from_tone(index: u16) -> Option<Bopomofo> {
179        if index as usize >= TONE_MAP.len() {
180            return None;
181        }
182        Some(TONE_MAP[index as usize])
183    }
184    pub(super) const fn index(&self) -> u16 {
185        match self {
186            B | I | A | TONE5 => 1,
187            P | U | O | TONE2 => 2,
188            M | IU | E | TONE3 => 3,
189            F | EH | TONE4 => 4,
190            D | AI | TONE1 => 5,
191            T | EI => 6,
192            N | AU => 7,
193            L | OU => 8,
194            G | AN => 9,
195            K | EN => 10,
196            H | ANG => 11,
197            J | ENG => 12,
198            Q | ER => 13,
199            X => 14,
200            ZH => 15,
201            CH => 16,
202            SH => 17,
203            R => 18,
204            Z => 19,
205            C => 20,
206            S => 21,
207        }
208    }
209}
210
211/// Enum to store the various types of errors that can cause parsing a bopomofo
212/// symbol to fail.
213///
214/// # Example
215///
216/// ```
217/// # use std::str::FromStr;
218/// # use chewing::zhuyin::Bopomofo;
219/// if let Err(e) = Bopomofo::from_str("a12") {
220///     println!("Failed conversion to bopomofo: {e}");
221/// }
222/// ```
223#[derive(Clone, Copy, Debug, PartialEq, Eq)]
224#[non_exhaustive]
225pub enum BopomofoErrorKind {
226    /// Value being parsed is empty.
227    Empty,
228    /// Contains an invalid symbol.
229    InvalidSymbol,
230}
231
232/// An error which can be returned when parsing an bopomofo symbol.
233///
234/// # Potential causes
235///
236/// Among other causes, `ParseBopomofoError` can be thrown because of leading or trailing whitespace
237/// in the string e.g., when it is obtained from the standard input.
238/// Using the [`str::trim()`] method ensures that no whitespace remains before parsing.
239///
240/// # Example
241///
242/// ```
243/// # use std::str::FromStr;
244/// # use chewing::zhuyin::Bopomofo;
245/// if let Err(e) = Bopomofo::from_str("a12") {
246///     println!("Failed conversion to bopomofo: {e}");
247/// }
248/// ```
249#[derive(Clone, Debug, PartialEq, Eq)]
250pub struct ParseBopomofoError {
251    kind: BopomofoErrorKind,
252}
253
254impl ParseBopomofoError {
255    fn empty() -> ParseBopomofoError {
256        Self {
257            kind: BopomofoErrorKind::Empty,
258        }
259    }
260    fn invalid_symbol() -> ParseBopomofoError {
261        Self {
262            kind: BopomofoErrorKind::InvalidSymbol,
263        }
264    }
265    /// Outputs the detailed cause of parsing an bopomofo failing.
266    pub fn kind(&self) -> &BopomofoErrorKind {
267        &self.kind
268    }
269}
270
271impl Display for ParseBopomofoError {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        write!(f, "Parse bopomofo error: {:?}", self.kind)
274    }
275}
276
277impl Error for ParseBopomofoError {}
278
279impl Display for Bopomofo {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        f.write_char((*self).into())
282    }
283}
284
285impl FromStr for Bopomofo {
286    type Err = ParseBopomofoError;
287
288    fn from_str(s: &str) -> Result<Self, Self::Err> {
289        if s.is_empty() {
290            return Err(ParseBopomofoError::empty());
291        }
292        if s.chars().count() != 1 {
293            return Err(ParseBopomofoError::invalid_symbol());
294        }
295
296        s.chars().next().unwrap().try_into()
297    }
298}
299
300impl From<Bopomofo> for char {
301    fn from(bopomofo: Bopomofo) -> Self {
302        match bopomofo {
303            B => 'ㄅ',
304            P => 'ㄆ',
305            M => 'ㄇ',
306            F => 'ㄈ',
307            D => 'ㄉ',
308            T => 'ㄊ',
309            N => 'ㄋ',
310            L => 'ㄌ',
311            G => 'ㄍ',
312            K => 'ㄎ',
313            H => 'ㄏ',
314            J => 'ㄐ',
315            Q => 'ㄑ',
316            X => 'ㄒ',
317            ZH => 'ㄓ',
318            CH => 'ㄔ',
319            SH => 'ㄕ',
320            R => 'ㄖ',
321            Z => 'ㄗ',
322            C => 'ㄘ',
323            S => 'ㄙ',
324            A => 'ㄚ',
325            O => 'ㄛ',
326            E => 'ㄜ',
327            EH => 'ㄝ',
328            AI => 'ㄞ',
329            EI => 'ㄟ',
330            AU => 'ㄠ',
331            OU => 'ㄡ',
332            AN => 'ㄢ',
333            EN => 'ㄣ',
334            ANG => 'ㄤ',
335            ENG => 'ㄥ',
336            ER => 'ㄦ',
337            I => 'ㄧ',
338            U => 'ㄨ',
339            IU => 'ㄩ',
340            TONE1 => 'ˉ',
341            TONE5 => '˙',
342            TONE2 => 'ˊ',
343            TONE3 => 'ˇ',
344            TONE4 => 'ˋ',
345        }
346    }
347}
348
349impl TryFrom<char> for Bopomofo {
350    type Error = ParseBopomofoError;
351
352    fn try_from(c: char) -> Result<Bopomofo, ParseBopomofoError> {
353        match c {
354            'ㄅ' => Ok(B),
355            'ㄆ' => Ok(P),
356            'ㄇ' => Ok(M),
357            'ㄈ' => Ok(F),
358            'ㄉ' => Ok(D),
359            'ㄊ' => Ok(T),
360            'ㄋ' => Ok(N),
361            'ㄌ' => Ok(L),
362            'ㄍ' => Ok(G),
363            'ㄎ' => Ok(K),
364            'ㄏ' => Ok(H),
365            'ㄐ' => Ok(J),
366            'ㄑ' => Ok(Q),
367            'ㄒ' => Ok(X),
368            'ㄓ' => Ok(ZH),
369            'ㄔ' => Ok(CH),
370            'ㄕ' => Ok(SH),
371            'ㄖ' => Ok(R),
372            'ㄗ' => Ok(Z),
373            'ㄘ' => Ok(C),
374            'ㄙ' => Ok(S),
375            'ㄚ' => Ok(A),
376            'ㄛ' => Ok(O),
377            'ㄜ' => Ok(E),
378            'ㄝ' => Ok(EH),
379            'ㄞ' => Ok(AI),
380            'ㄟ' => Ok(EI),
381            'ㄠ' => Ok(AU),
382            'ㄡ' => Ok(OU),
383            'ㄢ' => Ok(AN),
384            'ㄣ' => Ok(EN),
385            'ㄤ' => Ok(ANG),
386            'ㄥ' => Ok(ENG),
387            'ㄦ' => Ok(ER),
388            'ㄧ' => Ok(I),
389            'ㄨ' => Ok(U),
390            'ㄩ' => Ok(IU),
391            'ˉ' => Ok(TONE1),
392            '˙' => Ok(TONE5),
393            'ˊ' => Ok(TONE2),
394            'ˇ' => Ok(TONE3),
395            'ˋ' => Ok(TONE4),
396            _ => Err(ParseBopomofoError::invalid_symbol()),
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use crate::zhuyin::{BopomofoErrorKind, ParseBopomofoError};
404
405    use super::Bopomofo;
406
407    #[test]
408    fn parse() {
409        assert_eq!(Ok(Bopomofo::B), "ㄅ".parse())
410    }
411
412    #[test]
413    fn parse_empty() {
414        assert_eq!(Err(ParseBopomofoError::empty()), "".parse::<Bopomofo>());
415        assert_eq!(
416            &BopomofoErrorKind::Empty,
417            ParseBopomofoError::empty().kind()
418        );
419    }
420
421    #[test]
422    fn parse_invalid() {
423        assert_eq!(
424            Err(ParseBopomofoError::invalid_symbol()),
425            "abc".parse::<Bopomofo>()
426        );
427        assert_eq!(
428            &BopomofoErrorKind::InvalidSymbol,
429            ParseBopomofoError::invalid_symbol().kind()
430        );
431    }
432
433    #[test]
434    fn to_string() {
435        assert_eq!(Bopomofo::B.to_string(), "ㄅ")
436    }
437}