use std::{
error::Error,
fmt::{Display, Write},
str::FromStr,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BopomofoKind {
Initial,
Medial,
Rime,
Tone,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Bopomofo {
B,
P,
M,
F,
D,
T,
N,
L,
G,
K,
H,
J,
Q,
X,
ZH,
CH,
SH,
R,
Z,
C,
S,
I,
U,
IU,
A,
O,
E,
EH,
AI,
EI,
AU,
OU,
AN,
EN,
ANG,
ENG,
ER,
TONE5,
TONE2,
TONE3,
TONE4,
TONE1,
}
use self::Bopomofo::*;
const INITIAL_MAP: [Bopomofo; 21] = [
B, P, M, F, D, T, N, L, G, K, H, J, Q, X, ZH, CH, SH, R, Z, C, S,
];
const MEDIAL_MAP: [Bopomofo; 3] = [I, U, IU];
const RIME_MAP: [Bopomofo; 13] = [A, O, E, EH, AI, EI, AU, OU, AN, EN, ANG, ENG, ER];
const TONE_MAP: [Bopomofo; 4] = [TONE5, TONE2, TONE3, TONE4];
impl Bopomofo {
pub const fn kind(&self) -> BopomofoKind {
match self {
B | P | M | F | D | T | N | L | G | K | H | J | Q | X | ZH | CH | SH | R | Z | C
| S => BopomofoKind::Initial,
I | U | IU => BopomofoKind::Medial,
A | O | E | EH | AI | EI | AU | OU | AN | EN | ANG | ENG | ER => BopomofoKind::Rime,
TONE1 | TONE2 | TONE3 | TONE4 | TONE5 => BopomofoKind::Tone,
}
}
pub(super) const fn from_initial(index: u16) -> Option<Bopomofo> {
if index as usize >= INITIAL_MAP.len() {
return None;
}
Some(INITIAL_MAP[index as usize])
}
pub(super) const fn from_medial(index: u16) -> Option<Bopomofo> {
if index as usize >= MEDIAL_MAP.len() {
return None;
}
Some(MEDIAL_MAP[index as usize])
}
pub(super) const fn from_rime(index: u16) -> Option<Bopomofo> {
if index as usize >= RIME_MAP.len() {
return None;
}
Some(RIME_MAP[index as usize])
}
pub(super) const fn from_tone(index: u16) -> Option<Bopomofo> {
if index as usize >= TONE_MAP.len() {
return None;
}
Some(TONE_MAP[index as usize])
}
pub(super) const fn index(&self) -> u16 {
match self {
B | I | A | TONE5 => 1,
P | U | O | TONE2 => 2,
M | IU | E | TONE3 => 3,
F | EH | TONE4 => 4,
D | AI | TONE1 => 5,
T | EI => 6,
N | AU => 7,
L | OU => 8,
G | AN => 9,
K | EN => 10,
H | ANG => 11,
J | ENG => 12,
Q | ER => 13,
X => 14,
ZH => 15,
CH => 16,
SH => 17,
R => 18,
Z => 19,
C => 20,
S => 21,
}
}
}
impl Display for Bopomofo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_char((*self).into())
}
}
impl FromStr for Bopomofo {
type Err = ParseBopomofoError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars();
let Some(bopomofo) = chars.next().map(|ch| ch.try_into()) else {
return Err(ParseBopomofoError::empty());
};
if let Some(ch) = chars.next() {
return Err(ParseBopomofoError::invalid_symbol(ch));
}
bopomofo
}
}
impl From<Bopomofo> for char {
fn from(bopomofo: Bopomofo) -> Self {
match bopomofo {
B => 'ㄅ',
P => 'ㄆ',
M => 'ㄇ',
F => 'ㄈ',
D => 'ㄉ',
T => 'ㄊ',
N => 'ㄋ',
L => 'ㄌ',
G => 'ㄍ',
K => 'ㄎ',
H => 'ㄏ',
J => 'ㄐ',
Q => 'ㄑ',
X => 'ㄒ',
ZH => 'ㄓ',
CH => 'ㄔ',
SH => 'ㄕ',
R => 'ㄖ',
Z => 'ㄗ',
C => 'ㄘ',
S => 'ㄙ',
A => 'ㄚ',
O => 'ㄛ',
E => 'ㄜ',
EH => 'ㄝ',
AI => 'ㄞ',
EI => 'ㄟ',
AU => 'ㄠ',
OU => 'ㄡ',
AN => 'ㄢ',
EN => 'ㄣ',
ANG => 'ㄤ',
ENG => 'ㄥ',
ER => 'ㄦ',
I => 'ㄧ',
U => 'ㄨ',
IU => 'ㄩ',
TONE1 => 'ˉ',
TONE5 => '˙',
TONE2 => 'ˊ',
TONE3 => 'ˇ',
TONE4 => 'ˋ',
}
}
}
impl TryFrom<char> for Bopomofo {
type Error = ParseBopomofoError;
fn try_from(c: char) -> Result<Bopomofo, ParseBopomofoError> {
match c {
'ㄅ' => Ok(B),
'ㄆ' => Ok(P),
'ㄇ' => Ok(M),
'ㄈ' => Ok(F),
'ㄉ' => Ok(D),
'ㄊ' => Ok(T),
'ㄋ' => Ok(N),
'ㄌ' => Ok(L),
'ㄍ' => Ok(G),
'ㄎ' => Ok(K),
'ㄏ' => Ok(H),
'ㄐ' => Ok(J),
'ㄑ' => Ok(Q),
'ㄒ' => Ok(X),
'ㄓ' => Ok(ZH),
'ㄔ' => Ok(CH),
'ㄕ' => Ok(SH),
'ㄖ' => Ok(R),
'ㄗ' => Ok(Z),
'ㄘ' => Ok(C),
'ㄙ' => Ok(S),
'ㄚ' => Ok(A),
'ㄛ' => Ok(O),
'ㄜ' => Ok(E),
'ㄝ' => Ok(EH),
'ㄞ' => Ok(AI),
'ㄟ' => Ok(EI),
'ㄠ' => Ok(AU),
'ㄡ' => Ok(OU),
'ㄢ' => Ok(AN),
'ㄣ' => Ok(EN),
'ㄤ' => Ok(ANG),
'ㄥ' => Ok(ENG),
'ㄦ' => Ok(ER),
'ㄧ' => Ok(I),
'ㄨ' => Ok(U),
'ㄩ' => Ok(IU),
'ˉ' => Ok(TONE1),
'˙' => Ok(TONE5),
'ˊ' => Ok(TONE2),
'ˇ' => Ok(TONE3),
'ˋ' => Ok(TONE4),
_ => Err(ParseBopomofoError::invalid_symbol(c)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum BopomofoErrorKind {
Empty,
InvalidSymbol(char),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseBopomofoError {
kind: BopomofoErrorKind,
}
impl ParseBopomofoError {
fn empty() -> ParseBopomofoError {
Self {
kind: BopomofoErrorKind::Empty,
}
}
fn invalid_symbol(ch: char) -> ParseBopomofoError {
Self {
kind: BopomofoErrorKind::InvalidSymbol(ch),
}
}
pub fn kind(&self) -> &BopomofoErrorKind {
&self.kind
}
}
impl Display for ParseBopomofoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Parse bopomofo error: {:?}", self.kind)
}
}
impl Error for ParseBopomofoError {}
#[cfg(test)]
mod tests {
use super::Bopomofo;
use crate::zhuyin::{BopomofoErrorKind, ParseBopomofoError};
#[test]
fn parse() {
assert_eq!(Ok(Bopomofo::B), "ㄅ".parse())
}
#[test]
fn parse_empty() {
assert_eq!(Err(ParseBopomofoError::empty()), "".parse::<Bopomofo>());
assert_eq!(
&BopomofoErrorKind::Empty,
ParseBopomofoError::empty().kind()
);
}
#[test]
fn parse_invalid() {
assert_eq!(
Err(ParseBopomofoError::invalid_symbol('b')),
"abc".parse::<Bopomofo>()
);
assert_eq!(
&BopomofoErrorKind::InvalidSymbol('c'),
ParseBopomofoError::invalid_symbol('c').kind()
);
}
#[test]
fn to_string() {
assert_eq!(Bopomofo::B.to_string(), "ㄅ")
}
}