copybook_zoned_format/
lib.rs1#![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;
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, clap::ValueEnum, Default)]
38pub enum ZonedEncodingFormat {
39 Ascii,
41 Ebcdic,
43 #[default]
45 Auto,
46}
47
48impl ZonedEncodingFormat {
49 #[must_use]
51 #[inline]
52 pub const fn is_ascii(self) -> bool {
53 matches!(self, Self::Ascii)
54 }
55
56 #[must_use]
58 #[inline]
59 pub const fn is_ebcdic(self) -> bool {
60 matches!(self, Self::Ebcdic)
61 }
62
63 #[must_use]
65 #[inline]
66 pub const fn is_auto(self) -> bool {
67 matches!(self, Self::Auto)
68 }
69
70 #[must_use]
72 #[inline]
73 pub const fn description(self) -> &'static str {
74 match self {
75 Self::Ascii => "ASCII digit zones (0x30-0x39)",
76 Self::Ebcdic => "EBCDIC digit zones (0xF0-0xF9)",
77 Self::Auto => "Automatic detection based on zone nibbles",
78 }
79 }
80
81 #[must_use]
86 #[inline]
87 pub fn detect_from_byte(byte: u8) -> Option<Self> {
88 let zone_nibble = (byte >> 4) & zone_constants::ZONE_MASK;
89 match zone_nibble {
90 zone_constants::ASCII_ZONE => Some(Self::Ascii),
91 zone_constants::EBCDIC_ZONE => Some(Self::Ebcdic),
92 _ => None,
93 }
94 }
95}
96
97impl fmt::Display for ZonedEncodingFormat {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::Ascii => write!(f, "ascii"),
101 Self::Ebcdic => write!(f, "ebcdic"),
102 Self::Auto => write!(f, "auto"),
103 }
104 }
105}
106
107#[cfg(test)]
108#[allow(clippy::expect_used, clippy::unwrap_used)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn detect_from_byte_known_bytes() {
114 assert_eq!(
115 ZonedEncodingFormat::detect_from_byte(0x35),
116 Some(ZonedEncodingFormat::Ascii)
117 );
118 assert_eq!(
119 ZonedEncodingFormat::detect_from_byte(0xF4),
120 Some(ZonedEncodingFormat::Ebcdic)
121 );
122 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
123 }
124
125 #[test]
126 fn display_and_predicates() {
127 assert!(ZonedEncodingFormat::Ascii.is_ascii());
128 assert!(!ZonedEncodingFormat::Auto.is_ascii());
129 assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
130 assert_eq!(
131 ZonedEncodingFormat::Auto.description(),
132 "Automatic detection based on zone nibbles"
133 );
134 }
135
136 #[test]
139 fn test_is_ebcdic() {
140 assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
141 assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
142 assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
143 }
144
145 #[test]
148 fn test_is_auto() {
149 assert!(ZonedEncodingFormat::Auto.is_auto());
150 assert!(!ZonedEncodingFormat::Ascii.is_auto());
151 assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
152 }
153
154 #[test]
157 fn test_default_is_auto() {
158 assert_eq!(ZonedEncodingFormat::default(), ZonedEncodingFormat::Auto);
159 }
160
161 #[test]
164 fn test_display_all_variants() {
165 assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
166 assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
167 assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
168 }
169
170 #[test]
173 fn test_description_all_variants() {
174 assert_eq!(
175 ZonedEncodingFormat::Ascii.description(),
176 "ASCII digit zones (0x30-0x39)"
177 );
178 assert_eq!(
179 ZonedEncodingFormat::Ebcdic.description(),
180 "EBCDIC digit zones (0xF0-0xF9)"
181 );
182 assert_eq!(
183 ZonedEncodingFormat::Auto.description(),
184 "Automatic detection based on zone nibbles"
185 );
186 }
187
188 #[test]
191 fn test_detect_from_byte_all_ascii_digits() {
192 for byte in 0x30..=0x3F {
193 assert_eq!(
194 ZonedEncodingFormat::detect_from_byte(byte),
195 Some(ZonedEncodingFormat::Ascii),
196 "Failed for byte 0x{byte:02X}"
197 );
198 }
199 }
200
201 #[test]
202 fn test_detect_from_byte_all_ebcdic_digits() {
203 for byte in 0xF0..=0xFF {
204 assert_eq!(
205 ZonedEncodingFormat::detect_from_byte(byte),
206 Some(ZonedEncodingFormat::Ebcdic),
207 "Failed for byte 0x{byte:02X}"
208 );
209 }
210 }
211
212 #[test]
213 fn test_detect_from_byte_invalid_zones() {
214 let invalid_samples: &[u8] = &[
216 0x00, 0x10, 0x20, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0,
217 ];
218 for &byte in invalid_samples {
219 assert_eq!(
220 ZonedEncodingFormat::detect_from_byte(byte),
221 None,
222 "Expected None for byte 0x{byte:02X}"
223 );
224 }
225 }
226
227 #[test]
230 fn test_serde_roundtrip() {
231 for variant in [
232 ZonedEncodingFormat::Ascii,
233 ZonedEncodingFormat::Ebcdic,
234 ZonedEncodingFormat::Auto,
235 ] {
236 let json = serde_json::to_string(&variant).unwrap();
237 let deserialized: ZonedEncodingFormat = serde_json::from_str(&json).unwrap();
238 assert_eq!(
239 variant, deserialized,
240 "Serde round-trip failed for {variant:?}"
241 );
242 }
243 }
244
245 #[test]
248 fn test_clone_and_eq() {
249 let a = ZonedEncodingFormat::Ebcdic;
250 let b = a;
251 assert_eq!(a, b);
252 }
253}