#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
#![allow(clippy::missing_inline_in_public_items)]
use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
mod zone_constants {
pub const ASCII_ZONE: u8 = 0x3;
pub const EBCDIC_ZONE: u8 = 0xF;
pub const ZONE_MASK: u8 = 0x0F;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ZonedEncodingFormat {
Ascii,
Ebcdic,
#[default]
Auto,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseZonedEncodingFormatError {
input: String,
}
impl fmt::Display for ParseZonedEncodingFormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unsupported zoned encoding format `{}`", self.input)
}
}
impl std::error::Error for ParseZonedEncodingFormatError {}
impl FromStr for ZonedEncodingFormat {
type Err = ParseZonedEncodingFormatError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input.to_ascii_lowercase().as_str() {
"ascii" => Ok(Self::Ascii),
"ebcdic" => Ok(Self::Ebcdic),
"auto" => Ok(Self::Auto),
_ => Err(ParseZonedEncodingFormatError {
input: input.to_owned(),
}),
}
}
}
impl ZonedEncodingFormat {
#[must_use]
#[inline]
pub const fn is_ascii(self) -> bool {
matches!(self, Self::Ascii)
}
#[must_use]
#[inline]
pub const fn is_ebcdic(self) -> bool {
matches!(self, Self::Ebcdic)
}
#[must_use]
#[inline]
pub const fn is_auto(self) -> bool {
matches!(self, Self::Auto)
}
#[must_use]
#[inline]
pub const fn description(self) -> &'static str {
match self {
Self::Ascii => "ASCII digit zones (0x30-0x39)",
Self::Ebcdic => "EBCDIC digit zones (0xF0-0xF9)",
Self::Auto => "Automatic detection based on zone nibbles",
}
}
#[must_use]
#[inline]
pub fn detect_from_byte(byte: u8) -> Option<Self> {
let zone_nibble = (byte >> 4) & zone_constants::ZONE_MASK;
match zone_nibble {
zone_constants::ASCII_ZONE => Some(Self::Ascii),
zone_constants::EBCDIC_ZONE => Some(Self::Ebcdic),
_ => None,
}
}
}
impl fmt::Display for ZonedEncodingFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ascii => write!(f, "ascii"),
Self::Ebcdic => write!(f, "ebcdic"),
Self::Auto => write!(f, "auto"),
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn detect_from_byte_known_bytes() {
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0x35),
Some(ZonedEncodingFormat::Ascii)
);
assert_eq!(
ZonedEncodingFormat::detect_from_byte(0xF4),
Some(ZonedEncodingFormat::Ebcdic)
);
assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
}
#[test]
fn display_and_predicates() {
assert!(ZonedEncodingFormat::Ascii.is_ascii());
assert!(!ZonedEncodingFormat::Auto.is_ascii());
assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
assert_eq!(
ZonedEncodingFormat::Auto.description(),
"Automatic detection based on zone nibbles"
);
}
#[test]
fn test_is_ebcdic() {
assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
}
#[test]
fn test_is_auto() {
assert!(ZonedEncodingFormat::Auto.is_auto());
assert!(!ZonedEncodingFormat::Ascii.is_auto());
assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
}
#[test]
fn test_default_is_auto() {
assert_eq!(ZonedEncodingFormat::default(), ZonedEncodingFormat::Auto);
}
#[test]
fn test_display_all_variants() {
assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
}
#[test]
fn test_description_all_variants() {
assert_eq!(
ZonedEncodingFormat::Ascii.description(),
"ASCII digit zones (0x30-0x39)"
);
assert_eq!(
ZonedEncodingFormat::Ebcdic.description(),
"EBCDIC digit zones (0xF0-0xF9)"
);
assert_eq!(
ZonedEncodingFormat::Auto.description(),
"Automatic detection based on zone nibbles"
);
}
#[test]
fn test_detect_from_byte_all_ascii_digits() {
for byte in 0x30..=0x3F {
assert_eq!(
ZonedEncodingFormat::detect_from_byte(byte),
Some(ZonedEncodingFormat::Ascii),
"Failed for byte 0x{byte:02X}"
);
}
}
#[test]
fn test_detect_from_byte_all_ebcdic_digits() {
for byte in 0xF0..=0xFF {
assert_eq!(
ZonedEncodingFormat::detect_from_byte(byte),
Some(ZonedEncodingFormat::Ebcdic),
"Failed for byte 0x{byte:02X}"
);
}
}
#[test]
fn test_detect_from_byte_invalid_zones() {
let invalid_samples: &[u8] = &[
0x00, 0x10, 0x20, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0,
];
for &byte in invalid_samples {
assert_eq!(
ZonedEncodingFormat::detect_from_byte(byte),
None,
"Expected None for byte 0x{byte:02X}"
);
}
}
#[test]
fn test_serde_roundtrip() {
for variant in [
ZonedEncodingFormat::Ascii,
ZonedEncodingFormat::Ebcdic,
ZonedEncodingFormat::Auto,
] {
let json = serde_json::to_string(&variant).unwrap();
let deserialized: ZonedEncodingFormat = serde_json::from_str(&json).unwrap();
assert_eq!(
variant, deserialized,
"Serde round-trip failed for {variant:?}"
);
}
}
#[test]
fn test_clone_and_eq() {
let a = ZonedEncodingFormat::Ebcdic;
let b = a;
assert_eq!(a, b);
}
}