Skip to main content

copybook_zoned_format/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Zoned-decimal encoding detection helpers used by codec options and framing logic.
4#![allow(clippy::missing_inline_in_public_items)]
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// Zone nibble constants for zoned decimal encoding detection.
10mod zone_constants {
11    /// ASCII digit zone nibble (0x30-0x39 range).
12    pub const ASCII_ZONE: u8 = 0x3;
13    /// EBCDIC digit zone nibble (0xF0-0xF9 range).
14    pub const EBCDIC_ZONE: u8 = 0xF;
15    /// Zone nibble mask for extracting upper 4 bits.
16    pub const ZONE_MASK: u8 = 0x0F;
17}
18
19/// Zoned decimal encoding format specification for round-trip fidelity.
20///
21/// This enum controls how zoned decimal fields are encoded and decoded,
22/// enabling preservation of the original encoding format during round-trip
23/// operations for enterprise data consistency.
24///
25/// # Examples
26///
27/// ```
28/// use copybook_zoned_format::ZonedEncodingFormat;
29///
30/// let fmt = ZonedEncodingFormat::default();
31/// assert!(fmt.is_auto());
32/// assert_eq!(fmt.description(), "Automatic detection based on zone nibbles");
33///
34/// let detected = ZonedEncodingFormat::detect_from_byte(0xF5);
35/// assert_eq!(detected, Some(ZonedEncodingFormat::Ebcdic));
36/// ```
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum, Default)]
38pub enum ZonedEncodingFormat {
39    /// ASCII digit zones (0x30-0x39).
40    Ascii,
41    /// EBCDIC digit zones (0xF0-0xF9).
42    Ebcdic,
43    /// Automatic detection based on zone nibbles.
44    #[default]
45    Auto,
46}
47
48impl ZonedEncodingFormat {
49    /// Check if this is ASCII encoding.
50    #[must_use]
51    #[inline]
52    pub const fn is_ascii(self) -> bool {
53        matches!(self, Self::Ascii)
54    }
55
56    /// Check if this is EBCDIC encoding.
57    #[must_use]
58    #[inline]
59    pub const fn is_ebcdic(self) -> bool {
60        matches!(self, Self::Ebcdic)
61    }
62
63    /// Check if this is auto-detection mode.
64    #[must_use]
65    #[inline]
66    pub const fn is_auto(self) -> bool {
67        matches!(self, Self::Auto)
68    }
69
70    /// Get a human-readable description of the encoding format.
71    #[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    /// Detect encoding format from a single byte of zoned decimal data.
82    ///
83    /// Examines the zone nibble (upper 4 bits) to determine the encoding
84    /// format. Returns `None` for invalid zone values.
85    #[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    // --- is_ebcdic ---
137
138    #[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    // --- is_auto ---
146
147    #[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    // --- Default ---
155
156    #[test]
157    fn test_default_is_auto() {
158        assert_eq!(ZonedEncodingFormat::default(), ZonedEncodingFormat::Auto);
159    }
160
161    // --- Display all variants ---
162
163    #[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    // --- description all variants ---
171
172    #[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    // --- detect_from_byte comprehensive ---
189
190    #[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        // Zone nibbles 0x0, 0x1, 0x2, 0x4-0xE should return None
215        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    // --- Serde round-trip ---
228
229    #[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    // --- Clone / Copy / Eq ---
246
247    #[test]
248    fn test_clone_and_eq() {
249        let a = ZonedEncodingFormat::Ebcdic;
250        let b = a;
251        assert_eq!(a, b);
252    }
253}