1use std::{
2 error::Error,
3 fmt::{Display, Write},
4 str::FromStr,
5};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum BopomofoKind {
18 Initial,
20 Medial,
22 Rime,
24 Tone,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum Bopomofo {
33 B,
35 P,
37 M,
39 F,
41 D,
43 T,
45 N,
47 L,
49 G,
51 K,
53 H,
55 J,
57 Q,
59 X,
61 ZH,
63 CH,
65 SH,
67 R,
69 Z,
71 C,
73 S,
75 I,
77 U,
79 IU,
81 A,
83 O,
85 E,
87 EH,
89 AI,
91 EI,
93 AU,
95 OU,
97 AN,
99 EN,
101 ANG,
103 ENG,
105 ER,
107 TONE5,
109 TONE2,
111 TONE3,
113 TONE4,
115 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 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 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 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
224#[non_exhaustive]
225pub enum BopomofoErrorKind {
226 Empty,
228 InvalidSymbol,
230}
231
232#[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 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}