use core::fmt;
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Stroke {
Heng = 1,
Shu = 2,
Pie = 3,
Na = 4,
Zhe = 5,
}
impl Stroke {
#[inline]
pub const fn from_u8(v: u8) -> Option<Self> {
match v {
1 => Some(Self::Heng),
2 => Some(Self::Shu),
3 => Some(Self::Pie),
4 => Some(Self::Na),
5 => Some(Self::Zhe),
_ => None,
}
}
}
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Shape {
LeftRight = 1,
TopBottom = 2,
Whole = 3,
}
impl Shape {
#[inline]
pub const fn from_u8(v: u8) -> Option<Self> {
match v {
1 => Some(Self::LeftRight),
2 => Some(Self::TopBottom),
3 => Some(Self::Whole),
_ => None,
}
}
}
#[inline]
pub const fn shibie_ma(stroke: Stroke, shape: Shape) -> u8 {
let table: [[u8; 3]; 5] = [
[b'g', b'f', b'd'],
[b'h', b'j', b'k'],
[b't', b'r', b'e'],
[b'y', b'u', b'i'],
[b'n', b'b', b'v'],
];
let s = stroke as usize - 1;
let p = shape as usize - 1;
table[s][p]
}
#[inline]
pub const fn region_letter(stroke: Stroke) -> u8 {
match stroke {
Stroke::Heng => b'g',
Stroke::Shu => b'h',
Stroke::Pie => b't',
Stroke::Na => b'y',
Stroke::Zhe => b'n',
}
}
pub const JIANMING_ZIGEN: &str = "王土大木工目日口田山禾白月人金言立水火之已子女又纟";
pub const DAN_BI_HUA: &[char] = &['一', '丨', '丿', '丶', '乙'];
#[derive(Debug, Clone, Copy)]
pub struct DecompRef<'a> {
pub zigen: &'a [char],
pub strokes: &'a [Stroke],
pub shape: Shape,
}
impl<'a> DecompRef<'a> {
#[inline]
pub fn first_stroke(&self) -> Option<Stroke> {
self.strokes.first().copied()
}
#[inline]
pub fn second_stroke(&self) -> Option<Stroke> {
self.strokes.get(1).copied()
}
#[inline]
pub fn last_stroke(&self) -> Option<Stroke> {
self.strokes.last().copied()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EncodeError {
EmptyZigen,
UnknownZigen(char),
MissingStroke,
}
impl fmt::Display for EncodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EncodeError::EmptyZigen => f.write_str("empty zigen sequence"),
EncodeError::UnknownZigen(c) => write!(f, "unknown zigen: {c}"),
EncodeError::MissingStroke => f.write_str("decomp has no strokes"),
}
}
}
#[inline]
fn is_jianming(c: char) -> bool {
JIANMING_ZIGEN.contains(c)
}
#[inline]
fn is_dan_bi_hua(c: char) -> bool {
let mut i = 0;
while i < DAN_BI_HUA.len() {
if DAN_BI_HUA[i] == c {
return true;
}
i += 1;
}
false
}
pub fn encode_with_lookup<F>(
decomp: &DecompRef,
lookup: F,
out: &mut [u8; 4],
) -> Result<usize, EncodeError>
where
F: Fn(char) -> Option<u8>,
{
if decomp.zigen.is_empty() {
return Err(EncodeError::EmptyZigen);
}
let n = decomp.zigen.len();
match n {
1 => encode_single_zigen(decomp, lookup, out),
2 => {
let l1 = lookup(decomp.zigen[0]).ok_or(EncodeError::UnknownZigen(decomp.zigen[0]))?;
let l2 = lookup(decomp.zigen[1]).ok_or(EncodeError::UnknownZigen(decomp.zigen[1]))?;
let last = decomp.last_stroke().ok_or(EncodeError::MissingStroke)?;
let im = shibie_ma(last, decomp.shape);
out[0] = l1;
out[1] = l2;
out[2] = im;
Ok(3)
}
3 => {
let l1 = lookup(decomp.zigen[0]).ok_or(EncodeError::UnknownZigen(decomp.zigen[0]))?;
let l2 = lookup(decomp.zigen[1]).ok_or(EncodeError::UnknownZigen(decomp.zigen[1]))?;
let l3 = lookup(decomp.zigen[2]).ok_or(EncodeError::UnknownZigen(decomp.zigen[2]))?;
let last = decomp.last_stroke().ok_or(EncodeError::MissingStroke)?;
let im = shibie_ma(last, decomp.shape);
out[0] = l1;
out[1] = l2;
out[2] = l3;
out[3] = im;
Ok(4)
}
_ => {
let l1 = lookup(decomp.zigen[0]).ok_or(EncodeError::UnknownZigen(decomp.zigen[0]))?;
let l2 = lookup(decomp.zigen[1]).ok_or(EncodeError::UnknownZigen(decomp.zigen[1]))?;
let l3 = lookup(decomp.zigen[2]).ok_or(EncodeError::UnknownZigen(decomp.zigen[2]))?;
let ll = lookup(decomp.zigen[n - 1])
.ok_or(EncodeError::UnknownZigen(decomp.zigen[n - 1]))?;
out[0] = l1;
out[1] = l2;
out[2] = l3;
out[3] = ll;
Ok(4)
}
}
}
#[inline]
fn encode_single_zigen<F>(
decomp: &DecompRef,
lookup: F,
out: &mut [u8; 4],
) -> Result<usize, EncodeError>
where
F: Fn(char) -> Option<u8>,
{
let z = decomp.zigen[0];
let l = lookup(z).ok_or(EncodeError::UnknownZigen(z))?;
if is_dan_bi_hua(z) {
out[0] = l;
out[1] = l;
out[2] = b'l';
out[3] = b'l';
return Ok(4);
}
if is_jianming(z) {
out[0] = l;
out[1] = l;
out[2] = l;
out[3] = l;
return Ok(4);
}
let first = decomp.first_stroke().ok_or(EncodeError::MissingStroke)?;
let last = decomp.last_stroke().ok_or(EncodeError::MissingStroke)?;
let stroke_count = decomp.strokes.len();
if stroke_count == 2 {
out[0] = l;
out[1] = region_letter(first);
out[2] = region_letter(last);
return Ok(3);
}
let second = decomp.second_stroke().ok_or(EncodeError::MissingStroke)?;
out[0] = l;
out[1] = region_letter(first);
out[2] = region_letter(second);
out[3] = region_letter(last);
Ok(4)
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy(c: char) -> Option<u8> {
match c {
'王' => Some(b'g'),
'土' => Some(b'f'),
'大' => Some(b'd'),
'人' => Some(b'w'),
'一' => Some(b'g'),
'丨' => Some(b'h'),
'丿' => Some(b't'),
'丶' => Some(b'y'),
'乙' => Some(b'n'),
_ => None,
}
}
#[test]
fn shibie_grid() {
assert_eq!(shibie_ma(Stroke::Heng, Shape::LeftRight), b'g');
assert_eq!(shibie_ma(Stroke::Heng, Shape::Whole), b'd');
assert_eq!(shibie_ma(Stroke::Zhe, Shape::Whole), b'v');
}
#[test]
fn region_letters() {
assert_eq!(region_letter(Stroke::Heng), b'g');
assert_eq!(region_letter(Stroke::Zhe), b'n');
}
#[test]
fn jianming_letter_x4() {
let d = DecompRef {
zigen: &['王'],
strokes: &[Stroke::Heng],
shape: Shape::Whole,
};
let mut out = [0u8; 4];
let n = encode_with_lookup(&d, dummy, &mut out).unwrap();
assert_eq!(&out[..n], b"gggg");
}
#[test]
fn dan_bi_hua_rule() {
for (c, stroke, expected) in &[
('一', Stroke::Heng, b"ggll"),
('丨', Stroke::Shu, b"hhll"),
('丿', Stroke::Pie, b"ttll"),
('丶', Stroke::Na, b"yyll"),
('乙', Stroke::Zhe, b"nnll"),
] {
let d = DecompRef {
zigen: &[*c],
strokes: &[*stroke],
shape: Shape::Whole,
};
let mut out = [0u8; 4];
let n = encode_with_lookup(&d, dummy, &mut out).unwrap();
assert_eq!(&out[..n], *expected, "{c} mismatch");
}
}
#[test]
fn unknown_zigen_errors_out() {
let d = DecompRef {
zigen: &['🦀'],
strokes: &[Stroke::Heng],
shape: Shape::Whole,
};
let mut out = [0u8; 4];
assert!(matches!(
encode_with_lookup(&d, dummy, &mut out),
Err(EncodeError::UnknownZigen('🦀'))
));
}
#[test]
fn empty_zigen_errors_out() {
let d = DecompRef {
zigen: &[],
strokes: &[Stroke::Heng],
shape: Shape::Whole,
};
let mut out = [0u8; 4];
assert!(matches!(
encode_with_lookup(&d, dummy, &mut out),
Err(EncodeError::EmptyZigen)
));
}
}