nanalogue_core/utils/
mod_char.rs1use crate::Error;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::str::FromStr;
8
9#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
25pub struct ModChar(char);
26
27impl Default for ModChar {
29 fn default() -> Self {
30 ModChar::new('N')
31 }
32}
33
34impl ModChar {
35 #[must_use]
37 pub fn new(val: char) -> Self {
38 ModChar(val)
39 }
40 #[must_use]
50 pub fn val(&self) -> char {
51 self.0
52 }
53}
54
55impl From<char> for ModChar {
56 fn from(value: char) -> Self {
57 ModChar::new(value)
58 }
59}
60
61impl From<u8> for ModChar {
62 fn from(value: u8) -> Self {
63 ModChar::new(char::from(value))
64 }
65}
66
67impl FromStr for ModChar {
68 type Err = Error;
69
70 fn from_str(mod_type: &str) -> Result<Self, Self::Err> {
111 let first_char = mod_type
112 .chars()
113 .next()
114 .ok_or(Error::EmptyModType(String::new()))?;
115 match first_char {
116 'A'..='Z' | 'a'..='z' if mod_type.len() == 1 => Ok(ModChar(first_char)),
117 '0'..='9' => {
118 let val = char::from_u32(mod_type.parse()?)
119 .ok_or(Error::InvalidModType(mod_type.to_owned()))?;
120 Ok(ModChar(val))
121 }
122 _ => Err(Error::InvalidModType(mod_type.to_owned())),
123 }
124 }
125}
126
127impl fmt::Display for ModChar {
128 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
131 match self.val() {
132 w @ ('A'..='Z' | 'a'..='z') => w.to_string(),
133 w => (w as u32).to_string(),
134 }
135 .fmt(f)
136 }
137}
138
139impl Serialize for ModChar {
140 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141 where
142 S: serde::Serializer,
143 {
144 serializer.serialize_str(&self.to_string())
146 }
147}
148
149impl<'de> Deserialize<'de> for ModChar {
150 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
151 where
152 D: serde::Deserializer<'de>,
153 {
154 let s = String::deserialize(deserializer)?;
155 ModChar::from_str(&s).map_err(serde::de::Error::custom)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
165 fn display_mod_char() {
166 assert_eq!(
167 format!("{}", ModChar::from_str("a").expect("no failure")),
168 "a"
169 );
170 assert_eq!(
171 format!("{}", ModChar::from_str("T").expect("no failure")),
172 "T"
173 );
174 assert_eq!(
175 format!("{}", ModChar::from_str("77000").expect("no failure")),
176 "77000"
177 );
178 }
179
180 #[expect(
182 clippy::shadow_unrelated,
183 reason = "repetition is fine; each block is clearly separated"
184 )]
185 #[test]
186 fn modchar_numeric_conversion() {
187 let mod_char = ModChar::from_str("m").expect("should parse");
189 assert_eq!(mod_char.val(), 'm');
190 assert_eq!(format!("{mod_char}"), "m");
191
192 let mod_char = ModChar::from_str("T").expect("should parse");
193 assert_eq!(mod_char.val(), 'T');
194 assert_eq!(format!("{mod_char}"), "T");
195
196 let mod_char = ModChar::from_str("123").expect("should parse");
198 assert_eq!(format!("{mod_char}"), "123");
199
200 let mod_char = ModChar::from_str("472232").expect("should parse");
202 assert_eq!(format!("{mod_char}"), "472232");
203
204 let mod_char = ModChar::from_str("97").expect("should parse");
206 assert_eq!(mod_char.val(), 'a');
207 assert_eq!(format!("{mod_char}"), "a");
209
210 let mod_char = ModChar::from_str("65536").expect("should parse");
212 assert_eq!(format!("{mod_char}"), "65536");
213 }
214
215 #[test]
216 #[should_panic(expected = "EmptyModType")]
217 fn modchar_empty_string_panics() {
218 let _: ModChar = ModChar::from_str("").unwrap();
219 }
220
221 #[test]
222 #[should_panic(expected = "InvalidModType")]
223 fn modchar_special_char_at_panics() {
224 let _: ModChar = ModChar::from_str("@123").unwrap();
225 }
226
227 #[test]
228 #[should_panic(expected = "InvalidModType")]
229 fn modchar_special_char_hash_panics() {
230 let _: ModChar = ModChar::from_str("#abc").unwrap();
231 }
232
233 #[test]
235 fn modchar_display_consistency() {
236 for letter in ['a', 'b', 'z', 'A', 'B', 'Z'] {
238 let mod_char = ModChar::new(letter);
239 assert_eq!(format!("{mod_char}"), letter.to_string());
240 }
241
242 let test_numbers = vec![123, 456, 789, 472_232];
244 for num in test_numbers {
245 let mod_char = ModChar::from_str(&num.to_string()).expect("should parse");
246 assert_eq!(format!("{mod_char}"), num.to_string());
247 }
248 }
249
250 #[expect(
252 clippy::shadow_unrelated,
253 reason = "repetition is fine; each block is clearly separated"
254 )]
255 #[test]
256 fn from_char() {
257 let mod_char = ModChar::from('a');
259 assert_eq!(mod_char.val(), 'a');
260
261 let mod_char = ModChar::from('z');
262 assert_eq!(mod_char.val(), 'z');
263
264 let mod_char = ModChar::from('A');
266 assert_eq!(mod_char.val(), 'A');
267
268 let mod_char = ModChar::from('Z');
269 assert_eq!(mod_char.val(), 'Z');
270
271 let mod_char = ModChar::from('0');
273 assert_eq!(mod_char.val(), '0');
274
275 let mod_char = ModChar::from('9');
276 assert_eq!(mod_char.val(), '9');
277
278 let mod_char = ModChar::from('@');
280 assert_eq!(mod_char.val(), '@');
281
282 let mod_char = ModChar::from('#');
283 assert_eq!(mod_char.val(), '#');
284
285 let mod_char = ModChar::from('\u{D000}');
287 assert_eq!(mod_char.val(), '\u{D000}');
288
289 let mod_char = ModChar::from('\u{1F600}'); assert_eq!(mod_char.val(), '\u{1F600}');
291 }
292
293 #[test]
295 fn from_u8() {
296 for byte_val in 0u8..=255u8 {
298 let mod_char = ModChar::from(byte_val);
299 assert_eq!(mod_char.val(), char::from(byte_val));
300 }
301 }
302}