1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2#![allow(clippy::missing_inline_in_public_items)]
5
6use serde::{Deserialize, Serialize};
7use std::{fmt, str::FromStr};
8
9mod zone_constants {
11 pub const ASCII_ZONE: u8 = 0x3;
13 pub const EBCDIC_ZONE: u8 = 0xF;
15 pub const ZONE_MASK: u8 = 0x0F;
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
38pub enum ZonedEncodingFormat {
39 Ascii,
41 Ebcdic,
43 #[default]
45 Auto,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ParseZonedEncodingFormatError {
51 input: String,
52}
53
54impl fmt::Display for ParseZonedEncodingFormatError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "unsupported zoned encoding format `{}`", self.input)
57 }
58}
59
60impl std::error::Error for ParseZonedEncodingFormatError {}
61
62impl FromStr for ZonedEncodingFormat {
63 type Err = ParseZonedEncodingFormatError;
64
65 fn from_str(input: &str) -> Result<Self, Self::Err> {
66 match input.to_ascii_lowercase().as_str() {
67 "ascii" => Ok(Self::Ascii),
68 "ebcdic" => Ok(Self::Ebcdic),
69 "auto" => Ok(Self::Auto),
70 _ => Err(ParseZonedEncodingFormatError {
71 input: input.to_owned(),
72 }),
73 }
74 }
75}
76
77impl ZonedEncodingFormat {
78 #[must_use]
80 #[inline]
81 pub const fn is_ascii(self) -> bool {
82 matches!(self, Self::Ascii)
83 }
84
85 #[must_use]
87 #[inline]
88 pub const fn is_ebcdic(self) -> bool {
89 matches!(self, Self::Ebcdic)
90 }
91
92 #[must_use]
94 #[inline]
95 pub const fn is_auto(self) -> bool {
96 matches!(self, Self::Auto)
97 }
98
99 #[must_use]
101 #[inline]
102 pub const fn description(self) -> &'static str {
103 match self {
104 Self::Ascii => "ASCII digit zones (0x30-0x39)",
105 Self::Ebcdic => "EBCDIC digit zones (0xF0-0xF9)",
106 Self::Auto => "Automatic detection based on zone nibbles",
107 }
108 }
109
110 #[must_use]
115 #[inline]
116 pub fn detect_from_byte(byte: u8) -> Option<Self> {
117 let zone_nibble = (byte >> 4) & zone_constants::ZONE_MASK;
118 match zone_nibble {
119 zone_constants::ASCII_ZONE => Some(Self::Ascii),
120 zone_constants::EBCDIC_ZONE => Some(Self::Ebcdic),
121 _ => None,
122 }
123 }
124}
125
126impl fmt::Display for ZonedEncodingFormat {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 match self {
129 Self::Ascii => write!(f, "ascii"),
130 Self::Ebcdic => write!(f, "ebcdic"),
131 Self::Auto => write!(f, "auto"),
132 }
133 }
134}
135
136#[cfg(test)]
137#[allow(clippy::expect_used, clippy::unwrap_used)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn detect_from_byte_known_bytes() {
143 assert_eq!(
144 ZonedEncodingFormat::detect_from_byte(0x35),
145 Some(ZonedEncodingFormat::Ascii)
146 );
147 assert_eq!(
148 ZonedEncodingFormat::detect_from_byte(0xF4),
149 Some(ZonedEncodingFormat::Ebcdic)
150 );
151 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
152 }
153
154 #[test]
155 fn display_and_predicates() {
156 assert!(ZonedEncodingFormat::Ascii.is_ascii());
157 assert!(!ZonedEncodingFormat::Auto.is_ascii());
158 assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
159 assert_eq!(
160 ZonedEncodingFormat::Auto.description(),
161 "Automatic detection based on zone nibbles"
162 );
163 }
164
165 #[test]
168 fn test_is_ebcdic() {
169 assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
170 assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
171 assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
172 }
173
174 #[test]
177 fn test_is_auto() {
178 assert!(ZonedEncodingFormat::Auto.is_auto());
179 assert!(!ZonedEncodingFormat::Ascii.is_auto());
180 assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
181 }
182
183 #[test]
186 fn test_default_is_auto() {
187 assert_eq!(ZonedEncodingFormat::default(), ZonedEncodingFormat::Auto);
188 }
189
190 #[test]
193 fn test_display_all_variants() {
194 assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
195 assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
196 assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
197 }
198
199 #[test]
202 fn test_description_all_variants() {
203 assert_eq!(
204 ZonedEncodingFormat::Ascii.description(),
205 "ASCII digit zones (0x30-0x39)"
206 );
207 assert_eq!(
208 ZonedEncodingFormat::Ebcdic.description(),
209 "EBCDIC digit zones (0xF0-0xF9)"
210 );
211 assert_eq!(
212 ZonedEncodingFormat::Auto.description(),
213 "Automatic detection based on zone nibbles"
214 );
215 }
216
217 #[test]
220 fn test_detect_from_byte_all_ascii_digits() {
221 for byte in 0x30..=0x3F {
222 assert_eq!(
223 ZonedEncodingFormat::detect_from_byte(byte),
224 Some(ZonedEncodingFormat::Ascii),
225 "Failed for byte 0x{byte:02X}"
226 );
227 }
228 }
229
230 #[test]
231 fn test_detect_from_byte_all_ebcdic_digits() {
232 for byte in 0xF0..=0xFF {
233 assert_eq!(
234 ZonedEncodingFormat::detect_from_byte(byte),
235 Some(ZonedEncodingFormat::Ebcdic),
236 "Failed for byte 0x{byte:02X}"
237 );
238 }
239 }
240
241 #[test]
242 fn test_detect_from_byte_invalid_zones() {
243 let invalid_samples: &[u8] = &[
245 0x00, 0x10, 0x20, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0,
246 ];
247 for &byte in invalid_samples {
248 assert_eq!(
249 ZonedEncodingFormat::detect_from_byte(byte),
250 None,
251 "Expected None for byte 0x{byte:02X}"
252 );
253 }
254 }
255
256 #[test]
259 fn test_serde_roundtrip() {
260 for variant in [
261 ZonedEncodingFormat::Ascii,
262 ZonedEncodingFormat::Ebcdic,
263 ZonedEncodingFormat::Auto,
264 ] {
265 let json = serde_json::to_string(&variant).unwrap();
266 let deserialized: ZonedEncodingFormat = serde_json::from_str(&json).unwrap();
267 assert_eq!(
268 variant, deserialized,
269 "Serde round-trip failed for {variant:?}"
270 );
271 }
272 }
273
274 #[test]
277 fn test_clone_and_eq() {
278 let a = ZonedEncodingFormat::Ebcdic;
279 let b = a;
280 assert_eq!(a, b);
281 }
282}