use crate::Error;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ModChar(char);
impl Default for ModChar {
fn default() -> Self {
ModChar::new('N')
}
}
impl ModChar {
#[must_use]
pub fn new(val: char) -> Self {
ModChar(val)
}
#[must_use]
pub fn val(&self) -> char {
self.0
}
}
impl From<char> for ModChar {
fn from(value: char) -> Self {
ModChar::new(value)
}
}
impl From<u8> for ModChar {
fn from(value: u8) -> Self {
ModChar::new(char::from(value))
}
}
impl FromStr for ModChar {
type Err = Error;
fn from_str(mod_type: &str) -> Result<Self, Self::Err> {
let first_char = mod_type
.chars()
.next()
.ok_or(Error::EmptyModType(String::new()))?;
match first_char {
'A'..='Z' | 'a'..='z' if mod_type.len() == 1 => Ok(ModChar(first_char)),
'0'..='9' => {
let val = char::from_u32(mod_type.parse()?)
.ok_or(Error::InvalidModType(mod_type.to_owned()))?;
Ok(ModChar(val))
}
_ => Err(Error::InvalidModType(mod_type.to_owned())),
}
}
}
impl fmt::Display for ModChar {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.val() {
w @ ('A'..='Z' | 'a'..='z') => w.to_string(),
w => (w as u32).to_string(),
}
.fmt(f)
}
}
impl Serialize for ModChar {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for ModChar {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
ModChar::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_mod_char() {
assert_eq!(
format!("{}", ModChar::from_str("a").expect("no failure")),
"a"
);
assert_eq!(
format!("{}", ModChar::from_str("T").expect("no failure")),
"T"
);
assert_eq!(
format!("{}", ModChar::from_str("77000").expect("no failure")),
"77000"
);
}
#[expect(
clippy::shadow_unrelated,
reason = "repetition is fine; each block is clearly separated"
)]
#[test]
fn modchar_numeric_conversion() {
let mod_char = ModChar::from_str("m").expect("should parse");
assert_eq!(mod_char.val(), 'm');
assert_eq!(format!("{mod_char}"), "m");
let mod_char = ModChar::from_str("T").expect("should parse");
assert_eq!(mod_char.val(), 'T');
assert_eq!(format!("{mod_char}"), "T");
let mod_char = ModChar::from_str("123").expect("should parse");
assert_eq!(format!("{mod_char}"), "123");
let mod_char = ModChar::from_str("472232").expect("should parse");
assert_eq!(format!("{mod_char}"), "472232");
let mod_char = ModChar::from_str("97").expect("should parse");
assert_eq!(mod_char.val(), 'a');
assert_eq!(format!("{mod_char}"), "a");
let mod_char = ModChar::from_str("65536").expect("should parse");
assert_eq!(format!("{mod_char}"), "65536");
}
#[test]
#[should_panic(expected = "EmptyModType")]
fn modchar_empty_string_panics() {
let _: ModChar = ModChar::from_str("").unwrap();
}
#[test]
#[should_panic(expected = "InvalidModType")]
fn modchar_special_char_at_panics() {
let _: ModChar = ModChar::from_str("@123").unwrap();
}
#[test]
#[should_panic(expected = "InvalidModType")]
fn modchar_special_char_hash_panics() {
let _: ModChar = ModChar::from_str("#abc").unwrap();
}
#[test]
fn modchar_display_consistency() {
for letter in ['a', 'b', 'z', 'A', 'B', 'Z'] {
let mod_char = ModChar::new(letter);
assert_eq!(format!("{mod_char}"), letter.to_string());
}
let test_numbers = vec![123, 456, 789, 472_232];
for num in test_numbers {
let mod_char = ModChar::from_str(&num.to_string()).expect("should parse");
assert_eq!(format!("{mod_char}"), num.to_string());
}
}
#[expect(
clippy::shadow_unrelated,
reason = "repetition is fine; each block is clearly separated"
)]
#[test]
fn from_char() {
let mod_char = ModChar::from('a');
assert_eq!(mod_char.val(), 'a');
let mod_char = ModChar::from('z');
assert_eq!(mod_char.val(), 'z');
let mod_char = ModChar::from('A');
assert_eq!(mod_char.val(), 'A');
let mod_char = ModChar::from('Z');
assert_eq!(mod_char.val(), 'Z');
let mod_char = ModChar::from('0');
assert_eq!(mod_char.val(), '0');
let mod_char = ModChar::from('9');
assert_eq!(mod_char.val(), '9');
let mod_char = ModChar::from('@');
assert_eq!(mod_char.val(), '@');
let mod_char = ModChar::from('#');
assert_eq!(mod_char.val(), '#');
let mod_char = ModChar::from('\u{D000}');
assert_eq!(mod_char.val(), '\u{D000}');
let mod_char = ModChar::from('\u{1F600}'); assert_eq!(mod_char.val(), '\u{1F600}');
}
#[test]
fn from_u8() {
for byte_val in 0u8..=255u8 {
let mod_char = ModChar::from(byte_val);
assert_eq!(mod_char.val(), char::from(byte_val));
}
}
}