use crate::memory::ScratchBuffers;
use crate::options::{Codepage, ZonedEncodingFormat};
use crate::zoned_overpunch::{ZeroSignPolicy, encode_overpunch_byte};
use copybook_core::{Error, ErrorCode, Result, SignPlacement, SignSeparateInfo};
use std::convert::TryFrom;
use tracing::warn;
mod alphanumeric;
mod binary;
mod branch;
mod decimal;
mod float;
pub use alphanumeric::encode_alphanumeric;
pub use binary::{
decode_binary_int, decode_binary_int_fast, encode_binary_int, get_binary_width_from_digits,
validate_explicit_binary_width,
};
use branch::{likely, unlikely};
pub use decimal::{SmallDecimal, ZonedEncodingInfo};
use decimal::{create_normalized_decimal, digit_from_value, scale_abs_to_u32};
pub use float::*;
const ASCII_DIGIT_ZONE: u8 = 0x3; const EBCDIC_DIGIT_ZONE: u8 = 0xF;
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn decode_zoned_decimal(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
codepage: Codepage,
blank_when_zero: bool,
) -> Result<SmallDecimal> {
if unlikely(data.len() != usize::from(digits)) {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Zoned decimal data length mismatch".to_string(),
));
}
let is_all_spaces = data.iter().all(|&b| {
match codepage {
Codepage::ASCII => b == b' ',
_ => b == 0x40, }
});
if is_all_spaces {
if blank_when_zero {
warn!("CBKD412_ZONED_BLANK_IS_ZERO: Zoned field is blank, decoding as zero");
crate::lib_api::increment_warning_counter();
return Ok(SmallDecimal::zero(scale));
}
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Zoned field contains all spaces but BLANK WHEN ZERO not specified",
));
}
let mut value = 0i64;
let mut is_negative = false;
let expected_zone = match codepage {
Codepage::ASCII => ASCII_DIGIT_ZONE,
_ => EBCDIC_DIGIT_ZONE,
};
for (i, &byte) in data.iter().enumerate() {
if i == data.len() - 1 {
let (digit, negative) = crate::zoned_overpunch::decode_overpunch_byte(byte, codepage)?;
if signed {
is_negative = negative;
} else {
let zone = (byte >> 4) & 0x0F;
let zone_label = match codepage {
Codepage::ASCII => "ASCII",
_ => "EBCDIC",
};
if zone != expected_zone {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!(
"Unsigned {zone_label} zoned decimal cannot contain sign zone 0x{zone:X} in last byte"
),
));
}
if negative {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Unsigned zoned decimal contains negative overpunch",
));
}
}
value = value.saturating_mul(10).saturating_add(i64::from(digit));
} else {
let zone = (byte >> 4) & 0x0F;
let digit = byte & 0x0F;
if digit > 9 {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!("Invalid digit nibble 0x{digit:X} at position {i}"),
));
}
if zone != expected_zone {
let zone_label = match codepage {
Codepage::ASCII => "ASCII",
_ => "EBCDIC",
};
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!(
"Invalid {zone_label} zone 0x{zone:X} at position {i}, expected 0x{expected_zone:X}"
),
));
}
value = value.saturating_mul(10).saturating_add(i64::from(digit));
}
}
let mut decimal = SmallDecimal::new(value, scale, is_negative);
decimal.normalize(); Ok(decimal)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn decode_zoned_decimal_sign_separate(
data: &[u8],
digits: u16,
scale: i16,
sign_separate: &SignSeparateInfo,
codepage: Codepage,
) -> Result<SmallDecimal> {
let expected_len = usize::from(digits) + 1;
if unlikely(data.len() != expected_len) {
return Err(Error::new(
ErrorCode::CBKD301_RECORD_TOO_SHORT,
format!(
"SIGN SEPARATE zoned decimal data length mismatch: expected {} bytes, got {}",
expected_len,
data.len()
),
));
}
let (sign_byte, digit_bytes) = match sign_separate.placement {
SignPlacement::Leading => {
if data.is_empty() {
return Err(Error::new(
ErrorCode::CBKD301_RECORD_TOO_SHORT,
"SIGN SEPARATE field is empty",
));
}
(data[0], &data[1..])
}
SignPlacement::Trailing => {
if data.is_empty() {
return Err(Error::new(
ErrorCode::CBKD301_RECORD_TOO_SHORT,
"SIGN SEPARATE field is empty",
));
}
(data[data.len() - 1], &data[..data.len() - 1])
}
};
let is_negative = if codepage.is_ascii() {
match sign_byte {
b'-' => true,
b'+' | b' ' | b'0' => false,
_ => {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!("Invalid sign byte in SIGN SEPARATE field: 0x{sign_byte:02X} (ASCII)"),
));
}
}
} else {
match sign_byte {
0x60 => true,
0x4E | 0x40 | 0xF0 => false,
_ => {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!("Invalid sign byte in SIGN SEPARATE field: 0x{sign_byte:02X} (EBCDIC)"),
));
}
}
};
let mut value: i64 = 0;
for &byte in digit_bytes {
let digit = if codepage.is_ascii() {
if !byte.is_ascii_digit() {
return Err(Error::new(
ErrorCode::CBKD301_RECORD_TOO_SHORT,
format!("Invalid digit byte in SIGN SEPARATE field: 0x{byte:02X} (ASCII)"),
));
}
byte - b'0'
} else {
if !(0xF0..=0xF9).contains(&byte) {
return Err(Error::new(
ErrorCode::CBKD301_RECORD_TOO_SHORT,
format!("Invalid digit byte in SIGN SEPARATE field: 0x{byte:02X} (EBCDIC)"),
));
}
byte - 0xF0
};
value = value
.checked_mul(10)
.and_then(|v| v.checked_add(i64::from(digit)))
.ok_or_else(|| {
Error::new(
ErrorCode::CBKD410_ZONED_OVERFLOW,
format!("SIGN SEPARATE zoned decimal value overflow for {digits} digits"),
)
})?;
}
let mut decimal = SmallDecimal::new(value, scale, is_negative);
decimal.normalize(); Ok(decimal)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_zoned_decimal_sign_separate(
value: &str,
digits: u16,
scale: i16,
sign_separate: &SignSeparateInfo,
codepage: Codepage,
buffer: &mut [u8],
) -> Result<()> {
let expected_len = usize::from(digits) + 1;
if buffer.len() < expected_len {
return Err(Error::new(
ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
format!(
"SIGN SEPARATE encode buffer too small: need {expected_len} bytes, got {}",
buffer.len()
),
));
}
let trimmed = value.trim();
let (is_negative, abs_str) = if let Some(rest) = trimmed.strip_prefix('-') {
(true, rest)
} else if let Some(rest) = trimmed.strip_prefix('+') {
(false, rest)
} else {
(false, trimmed)
};
for ch in abs_str.chars() {
if !ch.is_ascii_digit() && ch != '.' {
return Err(Error::new(
ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
format!("Unexpected character '{ch}' in numeric value '{value}'"),
));
}
}
let dot_count = abs_str.chars().filter(|&c| c == '.').count();
let digit_count = abs_str.chars().filter(char::is_ascii_digit).count();
if digit_count == 0 {
return Err(Error::new(
ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
format!("No digits found in numeric value '{value}'"),
));
}
if dot_count > 1 {
return Err(Error::new(
ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
format!("Multiple decimal points in numeric value '{value}'"),
));
}
if scale <= 0 && dot_count == 1 {
return Err(Error::new(
ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
format!("Unexpected decimal point for scale {scale} in value '{value}'"),
));
}
let scaled = build_scaled_digit_string(abs_str, scale);
let digits_usize = usize::from(digits);
let padded = match scaled.len().cmp(&digits_usize) {
std::cmp::Ordering::Less => {
format!("{scaled:0>digits_usize$}")
}
std::cmp::Ordering::Greater => {
return Err(Error::new(
ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
format!(
"SIGN SEPARATE overflow: value requires {} digits but field allows {}",
scaled.len(),
digits_usize
),
));
}
std::cmp::Ordering::Equal => scaled,
};
let (sign_byte, digit_base): (u8, u8) = if codepage.is_ascii() {
(if is_negative { b'-' } else { b'+' }, b'0')
} else {
(if is_negative { 0x60 } else { 0x4E }, 0xF0)
};
let digit_offset = match sign_separate.placement {
SignPlacement::Leading => {
buffer[0] = sign_byte;
1
}
SignPlacement::Trailing => 0,
};
for (i, byte) in padded.bytes().enumerate() {
let digit = byte.wrapping_sub(b'0');
if digit > 9 {
return Err(Error::new(
ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
format!("Invalid digit byte 0x{byte:02X} in value"),
));
}
buffer[digit_offset + i] = digit_base + digit;
}
if matches!(sign_separate.placement, SignPlacement::Trailing) {
buffer[digits_usize] = sign_byte;
}
Ok(())
}
fn build_scaled_digit_string(abs_str: &str, scale: i16) -> String {
let digit_str: String = abs_str.chars().filter(char::is_ascii_digit).collect();
if scale <= 0 {
return digit_str;
}
let scale_usize = usize::try_from(scale).unwrap_or(0);
let (integer_part, fractional_part) = if let Some(pos) = abs_str.find('.') {
(&abs_str[..pos], &abs_str[pos + 1..])
} else {
(abs_str, "")
};
let int_digits: String = integer_part.chars().filter(char::is_ascii_digit).collect();
let frac_digits: String = fractional_part
.chars()
.filter(char::is_ascii_digit)
.collect();
let padded_frac = if frac_digits.len() >= scale_usize {
frac_digits[..scale_usize].to_string()
} else {
format!("{frac_digits:0<scale_usize$}")
};
format!("{int_digits}{padded_frac}")
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn decode_zoned_decimal_with_encoding(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
codepage: Codepage,
blank_when_zero: bool,
preserve_encoding: bool,
) -> Result<(SmallDecimal, Option<ZonedEncodingInfo>)> {
if data.len() != usize::from(digits) {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!(
"Zoned decimal data length {} doesn't match digits {}",
data.len(),
digits
),
));
}
let is_all_spaces = data.iter().all(|&b| {
match codepage {
Codepage::ASCII => b == b' ',
_ => b == 0x40, }
});
if is_all_spaces {
if blank_when_zero {
warn!("CBKD412_ZONED_BLANK_IS_ZERO: Zoned field is blank, decoding as zero");
crate::lib_api::increment_warning_counter();
return Ok((SmallDecimal::zero(scale), None));
}
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Zoned field contains all spaces but BLANK WHEN ZERO not specified",
));
}
let encoding_info = if preserve_encoding {
Some(ZonedEncodingInfo::detect_from_data(data)?)
} else {
None
};
if let Some(ref info) = encoding_info
&& info.has_mixed_encoding
{
return Err(Error::new(
ErrorCode::CBKD414_ZONED_MIXED_ENCODING,
"Mixed ASCII/EBCDIC encoding detected within zoned decimal field",
));
}
let (value, is_negative) =
zoned_decode_digits_with_encoding(data, signed, codepage, preserve_encoding)?;
let mut decimal = SmallDecimal::new(value, scale, is_negative);
decimal.normalize(); Ok((decimal, encoding_info))
}
#[inline]
fn zoned_decode_digits_with_encoding(
data: &[u8],
signed: bool,
codepage: Codepage,
preserve_encoding: bool,
) -> Result<(i64, bool)> {
let mut value = 0i64;
let mut is_negative = false;
for (index, &byte) in data.iter().enumerate() {
let zone = (byte >> 4) & 0x0F;
if index == data.len() - 1 {
let (digit, negative) = crate::zoned_overpunch::decode_overpunch_byte(byte, codepage)?;
if signed {
is_negative = negative;
} else {
let zone_valid = if preserve_encoding {
matches!(zone, 0x3 | 0xF)
} else {
match codepage {
Codepage::ASCII => zone == 0x3,
_ => zone == 0xF,
}
};
if !zone_valid {
let message = if preserve_encoding {
format!(
"Invalid zone 0x{zone:X} in unsigned zoned decimal, expected 0x3 (ASCII) or 0xF (EBCDIC)"
)
} else {
let zone_label = zoned_zone_label(codepage);
format!(
"Unsigned {zone_label} zoned decimal cannot contain sign zone 0x{zone:X} in last byte"
)
};
let code = if preserve_encoding {
ErrorCode::CBKD413_ZONED_INVALID_ENCODING
} else {
ErrorCode::CBKD411_ZONED_BAD_SIGN
};
return Err(Error::new(code, message));
}
if negative {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Unsigned zoned decimal contains negative overpunch",
));
}
}
value = value.saturating_mul(10).saturating_add(i64::from(digit));
} else {
let digit = byte & 0x0F;
if digit > 9 {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!("Invalid digit nibble 0x{digit:X} at position {index}"),
));
}
if preserve_encoding {
match zone {
0x3 | 0xF => {}
_ => {
return Err(Error::new(
ErrorCode::CBKD413_ZONED_INVALID_ENCODING,
format!(
"Invalid zone 0x{zone:X} at position {index}, expected 0x3 (ASCII) or 0xF (EBCDIC)"
),
));
}
}
} else {
match codepage {
Codepage::ASCII => {
if zone != 0x3 {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!(
"Invalid ASCII zone 0x{zone:X} at position {index}, expected 0x3"
),
));
}
}
_ => {
if zone != 0xF {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!(
"Invalid EBCDIC zone 0x{zone:X} at position {index}, expected 0xF"
),
));
}
}
}
}
value = value.saturating_mul(10).saturating_add(i64::from(digit));
}
}
Ok((value, is_negative))
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn decode_packed_decimal(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
) -> Result<SmallDecimal> {
let expected_bytes = usize::from((digits + 1).div_ceil(2));
if likely(data.len() == expected_bytes && !data.is_empty() && digits <= 18) {
return decode_packed_decimal_fast_path(data, digits, scale, signed);
}
if data.len() != expected_bytes {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Packed decimal data length mismatch".to_string(),
));
}
if data.is_empty() {
return Ok(SmallDecimal::zero(scale));
}
if digits > 18 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!(
"COMP-3 field with {digits} digits exceeds maximum supported precision (18 digits max for current implementation)"
),
));
}
decode_packed_decimal_fast_path(data, digits, scale, signed)
}
#[inline]
fn decode_packed_decimal_fast_path(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
) -> Result<SmallDecimal> {
match data.len() {
1 => decode_packed_fast_len1(data[0], digits, scale, signed),
2 => decode_packed_fast_len2(data, digits, scale, signed),
3 => decode_packed_fast_len3(data, scale, signed),
_ => decode_packed_fast_general(data, digits, scale, signed),
}
}
#[inline]
fn decode_packed_fast_len1(
byte: u8,
digits: u16,
scale: i16,
signed: bool,
) -> Result<SmallDecimal> {
let high_nibble = (byte >> 4) & 0x0F;
let low_nibble = byte & 0x0F;
let mut value = 0i64;
if !digits.is_multiple_of(2) {
if unlikely(high_nibble > 9) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid digit nibble in packed decimal".to_string(),
));
}
value = i64::from(high_nibble);
}
if signed {
let is_negative = match low_nibble {
0xA | 0xC | 0xE | 0xF => false,
0xB | 0xD => true,
_ => {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid sign nibble in packed decimal".to_string(),
));
}
};
return Ok(create_normalized_decimal(value, scale, is_negative));
}
if unlikely(low_nibble != 0xF) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid unsigned sign nibble, expected 0xF".to_string(),
));
}
Ok(create_normalized_decimal(value, scale, false))
}
#[inline]
fn decode_packed_fast_len2(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
) -> Result<SmallDecimal> {
let byte0 = data[0];
let byte1 = data[1];
let d1 = (byte0 >> 4) & 0x0F;
let d2 = byte0 & 0x0F;
let d3 = (byte1 >> 4) & 0x0F;
let sign_nibble = byte1 & 0x0F;
let value = if digits == 2 {
if unlikely(d1 != 0) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Expected padding nibble 0 for 2-digit field, got 0x{d1:X}"),
));
}
if unlikely(d2 > 9 || d3 > 9) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid digit in 2-digit COMP-3 field".to_string(),
));
}
i64::from(d2) * 10 + i64::from(d3)
} else {
if unlikely(d1 > 9 || d2 > 9 || d3 > 9) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid digit in 3-digit COMP-3 field".to_string(),
));
}
i64::from(d1) * 100 + i64::from(d2) * 10 + i64::from(d3)
};
let is_negative = if signed {
match sign_nibble {
0xA | 0xC | 0xE | 0xF => false,
0xB | 0xD => true,
_ => {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid sign nibble in packed decimal".to_string(),
));
}
}
} else {
if unlikely(sign_nibble != 0xF) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid unsigned sign nibble, expected 0xF".to_string(),
));
}
false
};
Ok(create_normalized_decimal(value, scale, is_negative))
}
#[inline]
fn decode_packed_fast_len3(data: &[u8], scale: i16, signed: bool) -> Result<SmallDecimal> {
let byte0 = data[0];
let byte1 = data[1];
let byte2 = data[2];
let d1 = (byte0 >> 4) & 0x0F;
let d2 = byte0 & 0x0F;
let d3 = (byte1 >> 4) & 0x0F;
let d4 = byte1 & 0x0F;
let d5 = (byte2 >> 4) & 0x0F;
let sign_nibble = byte2 & 0x0F;
if unlikely(d1 > 9 || d2 > 9 || d3 > 9 || d4 > 9 || d5 > 9) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid digit in 3-byte COMP-3 field".to_string(),
));
}
let value = i64::from(d1) * 10000
+ i64::from(d2) * 1000
+ i64::from(d3) * 100
+ i64::from(d4) * 10
+ i64::from(d5);
let is_negative = if signed {
match sign_nibble {
0xA | 0xC | 0xE | 0xF => false,
0xB | 0xD => true,
_ => {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid sign nibble in packed decimal".to_string(),
));
}
}
} else {
if unlikely(sign_nibble != 0xF) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid unsigned sign nibble, expected 0xF".to_string(),
));
}
false
};
Ok(create_normalized_decimal(value, scale, is_negative))
}
#[inline]
fn decode_packed_fast_general(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
) -> Result<SmallDecimal> {
let total_nibbles = digits + 1;
let has_padding = (total_nibbles & 1) == 1;
let digit_count = usize::from(digits);
let Some((last_byte, prefix_bytes)) = data.split_last() else {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Packed decimal data is empty".to_string(),
));
};
let mut value = 0i64;
let mut digit_pos = 0;
for &byte in prefix_bytes {
let high_nibble = (byte >> 4) & 0x0F;
let low_nibble = byte & 0x0F;
if likely(!(digit_pos == 0 && has_padding)) {
if unlikely(high_nibble > 9) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid digit nibble".to_string(),
));
}
value = value * 10 + i64::from(high_nibble);
digit_pos += 1;
}
if unlikely(low_nibble > 9) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid digit nibble".to_string(),
));
}
value = value * 10 + i64::from(low_nibble);
digit_pos += 1;
}
let last_high = (*last_byte >> 4) & 0x0F;
let sign_nibble = *last_byte & 0x0F;
if likely(digit_pos < digit_count) {
if unlikely(last_high > 9) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid digit nibble".to_string(),
));
}
value = value * 10 + i64::from(last_high);
}
let is_negative = if signed {
match sign_nibble {
0xA | 0xC | 0xE | 0xF => false,
0xB | 0xD => true,
_ => {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid sign nibble".to_string(),
));
}
}
} else {
if unlikely(sign_nibble != 0xF) {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Invalid unsigned sign nibble".to_string(),
));
}
false
};
Ok(create_normalized_decimal(value, scale, is_negative))
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_zoned_decimal(
value: &str,
digits: u16,
scale: i16,
signed: bool,
codepage: Codepage,
) -> Result<Vec<u8>> {
let zero_policy = if codepage.is_ascii() {
ZeroSignPolicy::Positive
} else {
ZeroSignPolicy::Preferred
};
encode_zoned_decimal_with_format_and_policy(
value,
digits,
scale,
signed,
codepage,
None,
zero_policy,
)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_zoned_decimal_with_format(
value: &str,
digits: u16,
scale: i16,
signed: bool,
codepage: Codepage,
encoding_override: Option<ZonedEncodingFormat>,
) -> Result<Vec<u8>> {
let zero_policy = match encoding_override {
Some(ZonedEncodingFormat::Ascii) => ZeroSignPolicy::Positive,
Some(ZonedEncodingFormat::Ebcdic) => ZeroSignPolicy::Preferred,
Some(ZonedEncodingFormat::Auto) | None => {
if codepage.is_ascii() {
ZeroSignPolicy::Positive
} else {
ZeroSignPolicy::Preferred
}
}
};
encode_zoned_decimal_with_format_and_policy(
value,
digits,
scale,
signed,
codepage,
encoding_override,
zero_policy,
)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_zoned_decimal_with_format_and_policy(
value: &str,
digits: u16,
scale: i16,
signed: bool,
codepage: Codepage,
encoding_override: Option<ZonedEncodingFormat>,
zero_policy: ZeroSignPolicy,
) -> Result<Vec<u8>> {
let decimal = SmallDecimal::from_str(value, scale)?;
let abs_value = decimal.value.abs();
let width = usize::from(digits);
let digit_str = format!("{abs_value:0width$}");
if digit_str.len() > width {
return Err(Error::new(
ErrorCode::CBKE510_NUMERIC_OVERFLOW,
format!("Value too large for {digits} digits"),
));
}
let mut target_format = encoding_override.unwrap_or(match codepage {
Codepage::ASCII => ZonedEncodingFormat::Ascii,
_ => ZonedEncodingFormat::Ebcdic,
});
if target_format == ZonedEncodingFormat::Auto {
target_format = if codepage.is_ascii() {
ZonedEncodingFormat::Ascii
} else {
ZonedEncodingFormat::Ebcdic
};
}
let mut result = Vec::with_capacity(width);
let digit_bytes = digit_str.as_bytes();
for (i, &ascii_digit) in digit_bytes.iter().enumerate() {
let digit = ascii_digit - b'0';
if digit > 9 {
return Err(Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Invalid digit character: {}", ascii_digit as char),
));
}
if i == digit_bytes.len() - 1 && signed {
if target_format == ZonedEncodingFormat::Ascii {
let overpunch_byte = encode_overpunch_byte(
digit,
decimal.negative,
Codepage::ASCII,
ZeroSignPolicy::Positive,
)?;
result.push(overpunch_byte);
} else {
let encode_codepage = if codepage == Codepage::ASCII {
Codepage::CP037
} else {
codepage
};
let overpunch_byte =
encode_overpunch_byte(digit, decimal.negative, encode_codepage, zero_policy)?;
result.push(overpunch_byte);
}
} else {
let zone = match target_format {
ZonedEncodingFormat::Ascii => ASCII_DIGIT_ZONE,
_ => EBCDIC_DIGIT_ZONE,
};
result.push((zone << 4) | digit);
}
}
Ok(result)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_packed_decimal(
value: &str,
digits: u16,
scale: i16,
signed: bool,
) -> Result<Vec<u8>> {
let decimal = SmallDecimal::from_str(value, scale)?;
let abs_value = decimal.value.abs();
if abs_value == 0 {
let expected_bytes = usize::from((digits + 1).div_ceil(2));
let mut result = vec![0u8; expected_bytes];
let sign_nibble = if signed {
if decimal.negative { 0x0D } else { 0x0C }
} else {
0x0F
};
result[expected_bytes - 1] = sign_nibble;
return Ok(result);
}
let mut digit_buffer: [u8; 20] = [0; 20];
let mut digit_count = 0;
let mut temp_value = abs_value;
while temp_value > 0 {
digit_buffer[digit_count] = digit_from_value(temp_value % 10);
temp_value /= 10;
digit_count += 1;
}
let digits_usize = usize::from(digits);
if unlikely(digit_count > digits_usize) {
return Err(Error::new(
ErrorCode::CBKE510_NUMERIC_OVERFLOW,
format!("Value too large for {digits} digits"),
));
}
let expected_bytes = usize::from((digits + 1).div_ceil(2));
let mut result = Vec::with_capacity(expected_bytes);
let has_padding = digits.is_multiple_of(2); let total_nibbles = digits_usize + 1 + usize::from(has_padding);
for byte_idx in 0..expected_bytes {
let mut byte_val = 0u8;
let nibble_offset = byte_idx * 2;
let high_nibble_idx = nibble_offset;
if high_nibble_idx < total_nibbles - 1 {
if has_padding && high_nibble_idx == 0 {
byte_val |= 0x00 << 4;
} else {
let digit_idx = if has_padding {
high_nibble_idx - 1
} else {
high_nibble_idx
};
if digit_idx >= (digits_usize - digit_count) {
let actual_digit_idx = digit_idx - (digits_usize - digit_count);
if actual_digit_idx < digit_count {
let digit_pos_from_right = digit_count - 1 - actual_digit_idx;
let digit = digit_buffer[digit_pos_from_right];
byte_val |= digit << 4;
}
}
}
}
let low_nibble_idx = nibble_offset + 1;
if low_nibble_idx == total_nibbles - 1 {
byte_val |= if signed {
if decimal.negative { 0x0D } else { 0x0C }
} else {
0x0F
};
} else if low_nibble_idx < total_nibbles - 1 {
let digit_idx = if has_padding {
low_nibble_idx - 1
} else {
low_nibble_idx
};
if digit_idx >= (digits_usize - digit_count) {
let actual_digit_idx = digit_idx - (digits_usize - digit_count);
if actual_digit_idx < digit_count {
let digit_pos_from_right = digit_count - 1 - actual_digit_idx;
let digit = digit_buffer[digit_pos_from_right];
byte_val |= digit;
}
}
}
result.push(byte_val);
}
Ok(result)
}
#[inline]
#[must_use]
pub fn should_encode_as_blank_when_zero(value: &str, bwz_encode: bool) -> bool {
if !bwz_encode {
return false;
}
let trimmed = value.trim();
if trimmed.is_empty() || trimmed == "0" {
return true;
}
if let Some(dot_pos) = trimmed.find('.') {
let integer_part = &trimmed[..dot_pos];
let fractional_part = &trimmed[dot_pos + 1..];
if integer_part == "0" && fractional_part.chars().all(|c| c == '0') {
return true;
}
}
false
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_zoned_decimal_with_bwz(
value: &str,
digits: u16,
scale: i16,
signed: bool,
codepage: Codepage,
bwz_encode: bool,
) -> Result<Vec<u8>> {
if should_encode_as_blank_when_zero(value, bwz_encode) {
let space_byte = match codepage {
Codepage::ASCII => b' ',
_ => 0x40, };
return Ok(vec![space_byte; usize::from(digits)]);
}
encode_zoned_decimal(value, digits, scale, signed, codepage)
}
#[inline]
const fn zoned_space_byte(codepage: Codepage) -> u8 {
match codepage {
Codepage::ASCII => b' ',
_ => 0x40,
}
}
#[inline]
const fn zoned_expected_zone(codepage: Codepage) -> u8 {
match codepage {
Codepage::ASCII => ASCII_DIGIT_ZONE,
_ => EBCDIC_DIGIT_ZONE,
}
}
#[inline]
const fn zoned_zone_label(codepage: Codepage) -> &'static str {
match codepage {
Codepage::ASCII => "ASCII",
_ => "EBCDIC",
}
}
#[inline]
fn zoned_validate_non_final_byte(
byte: u8,
index: usize,
expected_zone: u8,
codepage: Codepage,
) -> Result<u8> {
let zone = (byte >> 4) & 0x0F;
let digit = byte & 0x0F;
if digit > 9 {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!("Invalid digit nibble 0x{digit:X} at position {index}"),
));
}
if zone != expected_zone {
let zone_label = zoned_zone_label(codepage);
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!(
"Invalid {zone_label} zone 0x{zone:X} at position {index}, expected 0x{expected_zone:X}"
),
));
}
Ok(digit)
}
#[inline]
fn zoned_process_non_final_digits(
data: &[u8],
expected_zone: u8,
codepage: Codepage,
scratch: &mut ScratchBuffers,
) -> Result<i64> {
let mut value = 0i64;
for (index, &byte) in data.iter().enumerate() {
let digit = zoned_validate_non_final_byte(byte, index, expected_zone, codepage)?;
scratch.digit_buffer.push(digit);
value = value.saturating_mul(10).saturating_add(i64::from(digit));
}
Ok(value)
}
#[inline]
fn zoned_decode_last_byte(byte: u8, codepage: Codepage) -> Result<(u8, bool)> {
crate::zoned_overpunch::decode_overpunch_byte(byte, codepage)
}
#[inline]
fn zoned_ensure_unsigned(
last_byte: u8,
expected_zone: u8,
codepage: Codepage,
negative: bool,
) -> Result<bool> {
let zone = (last_byte >> 4) & 0x0F;
if zone != expected_zone {
let zone_label = zoned_zone_label(codepage);
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!(
"Unsigned {zone_label} zoned decimal cannot contain sign zone 0x{zone:X} in last byte"
),
));
}
if negative {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Unsigned zoned decimal contains negative overpunch",
));
}
Ok(false)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn decode_zoned_decimal_with_scratch(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
codepage: Codepage,
blank_when_zero: bool,
scratch: &mut ScratchBuffers,
) -> Result<SmallDecimal> {
if data.len() != usize::from(digits) {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
format!(
"Zoned decimal data length {} doesn't match digits {}",
data.len(),
digits
),
));
}
let space_byte = zoned_space_byte(codepage);
let is_all_spaces = data.iter().all(|&b| b == space_byte);
if is_all_spaces {
if blank_when_zero {
warn!("CBKD412_ZONED_BLANK_IS_ZERO: Zoned field is blank, decoding as zero");
crate::lib_api::increment_warning_counter();
return Ok(SmallDecimal::zero(scale));
}
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Zoned field contains all spaces but BLANK WHEN ZERO not specified",
));
}
scratch.digit_buffer.clear();
scratch.digit_buffer.reserve(usize::from(digits));
let expected_zone = zoned_expected_zone(codepage);
let Some((&last_byte, non_final)) = data.split_last() else {
return Err(Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Zoned decimal field is empty",
));
};
let partial_value =
zoned_process_non_final_digits(non_final, expected_zone, codepage, scratch)?;
let (last_digit, negative) = zoned_decode_last_byte(last_byte, codepage)?;
scratch.digit_buffer.push(last_digit);
let value = partial_value
.saturating_mul(10)
.saturating_add(i64::from(last_digit));
let is_negative = if signed {
negative
} else {
zoned_ensure_unsigned(last_byte, expected_zone, codepage, negative)?
};
let mut decimal = SmallDecimal::new(value, scale, is_negative);
decimal.normalize();
debug_assert!(
scratch.digit_buffer.iter().all(|&d| d <= 9),
"scratch digit buffer must contain only logical digits"
);
Ok(decimal)
}
#[inline]
fn packed_decode_single_byte(
byte: u8,
digits: u16,
scale: i16,
signed: bool,
) -> Result<SmallDecimal> {
let high_nibble = (byte >> 4) & 0x0F;
let low_nibble = byte & 0x0F;
let mut value = 0i64;
if digits == 1 {
if high_nibble > 9 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid digit nibble 0x{high_nibble:X}"),
));
}
value = i64::from(high_nibble);
}
let is_negative = if signed {
match low_nibble {
0xA | 0xC | 0xE | 0xF => false,
0xB | 0xD => true,
_ => {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid sign nibble 0x{low_nibble:X}"),
));
}
}
} else {
if low_nibble != 0xF {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid unsigned sign nibble 0x{low_nibble:X}, expected 0xF"),
));
}
false
};
Ok(create_normalized_decimal(value, scale, is_negative))
}
#[inline]
fn packed_push_digit(value: &mut i64, digit: u8) -> Result<()> {
*value = value
.checked_mul(10)
.and_then(|v| v.checked_add(i64::from(digit)))
.ok_or_else(|| {
Error::new(
ErrorCode::CBKD411_ZONED_BAD_SIGN,
"Numeric overflow during zoned decimal conversion",
)
})?;
Ok(())
}
#[inline]
fn packed_process_non_last_bytes(
bytes: &[u8],
digits: u16,
has_padding: bool,
) -> Result<(i64, u16)> {
let mut value = 0i64;
let mut digit_count: u16 = 0;
for (index, &byte) in bytes.iter().enumerate() {
let high_nibble = (byte >> 4) & 0x0F;
let low_nibble = byte & 0x0F;
if index == 0 && has_padding {
if high_nibble != 0 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Expected padding nibble 0, got 0x{high_nibble:X}"),
));
}
} else {
if high_nibble > 9 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid digit nibble 0x{high_nibble:X}"),
));
}
packed_push_digit(&mut value, high_nibble)?;
digit_count += 1;
}
if digit_count >= digits {
break;
}
if low_nibble > 9 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid digit nibble 0x{low_nibble:X}"),
));
}
packed_push_digit(&mut value, low_nibble)?;
digit_count += 1;
if digit_count >= digits {
break;
}
}
Ok((value, digit_count))
}
#[inline]
fn packed_finish_last_byte(
mut value: i64,
last_byte: u8,
digits: u16,
digit_count: u16,
scale: i16,
signed: bool,
) -> Result<SmallDecimal> {
let high_nibble = (last_byte >> 4) & 0x0F;
let low_nibble = last_byte & 0x0F;
if digit_count < digits {
if high_nibble > 9 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid digit nibble 0x{high_nibble:X}"),
));
}
packed_push_digit(&mut value, high_nibble)?;
}
let is_negative = if signed {
match low_nibble {
0xA | 0xC | 0xE | 0xF => false,
0xB | 0xD => true,
_ => {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid sign nibble 0x{low_nibble:X}"),
));
}
}
} else {
if low_nibble != 0xF {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid unsigned sign nibble 0x{low_nibble:X}, expected 0xF"),
));
}
false
};
Ok(create_normalized_decimal(value, scale, is_negative))
}
#[inline]
fn packed_decode_multi_byte(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
) -> Result<SmallDecimal> {
let total_nibbles = digits + 1;
let has_padding = (total_nibbles & 1) == 1;
let Some((&last_byte, non_last)) = data.split_last() else {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Packed decimal input is empty",
));
};
let (value, digit_count) = packed_process_non_last_bytes(non_last, digits, has_padding)?;
packed_finish_last_byte(value, last_byte, digits, digit_count, scale, signed)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn decode_packed_decimal_with_scratch(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
scratch: &mut ScratchBuffers,
) -> Result<SmallDecimal> {
let expected_bytes = usize::from((digits + 1).div_ceil(2));
if data.len() != expected_bytes {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!(
"Packed decimal data length {} doesn't match expected {} bytes for {} digits",
data.len(),
expected_bytes,
digits
),
));
}
if data.is_empty() {
return Ok(SmallDecimal::zero(scale));
}
scratch.digit_buffer.clear();
scratch.digit_buffer.reserve(usize::from(digits));
let decimal = if data.len() == 1 {
packed_decode_single_byte(data[0], digits, scale, signed)?
} else {
packed_decode_multi_byte(data, digits, scale, signed)?
};
debug_assert!(
scratch.digit_buffer.iter().all(|&d| d <= 9),
"scratch digit buffer must contain only logical digits"
);
Ok(decimal)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_zoned_decimal_with_scratch(
decimal: &SmallDecimal,
digits: u16,
signed: bool,
codepage: Codepage,
_bwz_encode: bool,
scratch: &mut ScratchBuffers,
) -> Result<Vec<u8>> {
scratch.digit_buffer.clear();
scratch.byte_buffer.clear();
scratch.byte_buffer.reserve(usize::from(digits));
scratch.string_buffer.clear();
scratch.string_buffer.push_str(&decimal.to_string());
encode_zoned_decimal(
&scratch.string_buffer,
digits,
decimal.scale,
signed,
codepage,
)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_packed_decimal_with_scratch(
decimal: &SmallDecimal,
digits: u16,
signed: bool,
scratch: &mut ScratchBuffers,
) -> Result<Vec<u8>> {
scratch.digit_buffer.clear();
scratch.byte_buffer.clear();
let expected_bytes = usize::from((digits + 1).div_ceil(2));
scratch.byte_buffer.reserve(expected_bytes);
scratch.string_buffer.clear();
scratch.string_buffer.push_str(&decimal.to_string());
encode_packed_decimal(&scratch.string_buffer, digits, decimal.scale, signed)
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn decode_packed_decimal_to_string_with_scratch(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
scratch: &mut ScratchBuffers,
) -> Result<String> {
const SIGN_TABLE: [u8; 16] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 1, 1, ];
if data.is_empty() {
return Ok("0".to_string());
}
if data.len() == 1 && digits == 1 {
let byte = data[0];
let high_nibble = (byte >> 4) & 0x0F;
let low_nibble = byte & 0x0F;
let mut is_negative = false;
if high_nibble > 9 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid digit nibble 0x{high_nibble:X}"),
));
}
let value = i64::from(high_nibble);
if signed {
let sign_code = SIGN_TABLE[usize::from(low_nibble)];
if sign_code == 0 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid sign nibble 0x{low_nibble:X}"),
));
}
is_negative = sign_code == 2;
} else if low_nibble != 0xF {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Invalid unsigned sign nibble 0x{low_nibble:X}, expected 0xF"),
));
}
scratch.string_buffer.clear();
if is_negative && value != 0 {
scratch.string_buffer.push('-');
}
if scale <= 0 {
let scaled_value = if scale < 0 {
value * 10_i64.pow(scale_abs_to_u32(scale))
} else {
value
};
format_integer_to_buffer(scaled_value, &mut scratch.string_buffer);
} else {
let divisor = 10_i64.pow(scale_abs_to_u32(scale));
let integer_part = value / divisor;
let fractional_part = value % divisor;
format_integer_to_buffer(integer_part, &mut scratch.string_buffer);
scratch.string_buffer.push('.');
format_integer_with_leading_zeros_to_buffer(
fractional_part,
scale_abs_to_u32(scale),
&mut scratch.string_buffer,
);
}
let result = std::mem::take(&mut scratch.string_buffer);
return Ok(result);
}
let decimal = decode_packed_decimal_with_scratch(data, digits, scale, signed, scratch)?;
decimal.format_to_scratch_buffer(scale, &mut scratch.string_buffer);
let result = std::mem::take(&mut scratch.string_buffer);
Ok(result)
}
#[inline]
#[must_use = "Use the formatted string or continue mutating the scratch buffer"]
pub fn format_binary_int_to_string_with_scratch(
value: i64,
scratch: &mut ScratchBuffers,
) -> String {
scratch.string_buffer.clear();
if value < 0 {
scratch.string_buffer.push('-');
if value == i64::MIN {
scratch.string_buffer.push_str("9223372036854775808");
return std::mem::take(&mut scratch.string_buffer);
}
format_integer_to_buffer(-value, &mut scratch.string_buffer);
} else {
format_integer_to_buffer(value, &mut scratch.string_buffer);
}
std::mem::take(&mut scratch.string_buffer)
}
#[inline]
fn format_integer_to_buffer(value: i64, buffer: &mut String) {
SmallDecimal::format_integer_manual(value, buffer);
}
#[inline]
fn format_integer_with_leading_zeros_to_buffer(value: i64, width: u32, buffer: &mut String) {
SmallDecimal::format_integer_with_leading_zeros(value, width, buffer);
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn decode_zoned_decimal_to_string_with_scratch(
data: &[u8],
digits: u16,
scale: i16,
signed: bool,
codepage: Codepage,
blank_when_zero: bool,
scratch: &mut ScratchBuffers,
) -> Result<String> {
let decimal = decode_zoned_decimal_with_scratch(
data,
digits,
scale,
signed,
codepage,
blank_when_zero,
scratch,
)?;
if scale == 0 && !blank_when_zero {
if decimal.value == 0 {
scratch.string_buffer.clear();
scratch.string_buffer.push('0');
} else {
scratch.string_buffer.clear();
if decimal.negative && decimal.value != 0 {
scratch.string_buffer.push('-');
}
let magnitude = if decimal.scale < 0 {
decimal.value * 10_i64.pow(scale_abs_to_u32(decimal.scale))
} else {
decimal.value
};
SmallDecimal::format_integer_with_leading_zeros(
magnitude,
u32::from(digits),
&mut scratch.string_buffer,
);
}
return Ok(std::mem::take(&mut scratch.string_buffer));
}
decimal.format_to_scratch_buffer(scale, &mut scratch.string_buffer);
Ok(std::mem::take(&mut scratch.string_buffer))
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::zoned_overpunch::{ZeroSignPolicy, encode_overpunch_byte, is_valid_overpunch};
use proptest::prelude::*;
use proptest::test_runner::RngSeed;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn proptest_case_count() -> u32 {
option_env!("PROPTEST_CASES")
.and_then(|s| s.parse().ok())
.unwrap_or(256)
}
fn numeric_proptest_config() -> ProptestConfig {
let mut cfg = ProptestConfig {
cases: proptest_case_count(),
max_shrink_time: 0,
..ProptestConfig::default()
};
if let Ok(seed_value) = std::env::var("PROPTEST_SEED")
&& !seed_value.is_empty()
{
let parsed_seed = seed_value.parse::<u64>().unwrap_or_else(|_| {
let mut hasher = DefaultHasher::new();
seed_value.hash(&mut hasher);
hasher.finish()
});
cfg.rng_seed = RngSeed::Fixed(parsed_seed);
}
cfg
}
#[test]
fn test_small_decimal_normalization() {
let mut decimal = SmallDecimal::new(0, 2, true);
decimal.normalize();
assert!(!decimal.negative); }
#[test]
fn test_small_decimal_formatting() {
let decimal = SmallDecimal::new(123, 0, false);
assert_eq!(decimal.to_string(), "123");
let decimal = SmallDecimal::new(12345, 2, false);
assert_eq!(decimal.to_string(), "123.45");
let decimal = SmallDecimal::new(12345, 2, true);
assert_eq!(decimal.to_string(), "-123.45");
}
#[test]
fn test_zero_with_scale_preserves_decimal_places() {
let decimal = SmallDecimal::new(0, 2, false);
assert_eq!(decimal.to_string(), "0.00");
let decimal = SmallDecimal::new(0, 1, false);
assert_eq!(decimal.to_string(), "0.0");
let decimal = SmallDecimal::new(0, 4, true);
assert_eq!(decimal.to_string(), "0.0000");
}
proptest! {
#![proptest_config(numeric_proptest_config())]
#[test]
fn prop_zoned_digit_buffer_contains_only_digits(
digits_vec in prop::collection::vec(0u8..=9, 1..=12),
signed in any::<bool>(),
allow_negative in any::<bool>(),
codepage in prop_oneof![
Just(Codepage::ASCII),
Just(Codepage::CP037),
Just(Codepage::CP273),
Just(Codepage::CP500),
Just(Codepage::CP1047),
Just(Codepage::CP1140),
],
policy in prop_oneof![Just(ZeroSignPolicy::Positive), Just(ZeroSignPolicy::Preferred)],
) {
let digit_count = u16::try_from(digits_vec.len()).expect("vector length <= 12");
let mut bytes = Vec::with_capacity(digits_vec.len());
for digit in digits_vec.iter().take(digits_vec.len().saturating_sub(1)) {
let byte = if codepage.is_ascii() {
0x30 + digit
} else {
0xF0 + digit
};
bytes.push(byte);
}
let is_negative = signed && allow_negative;
let last_digit = *digits_vec.last().expect("vector is non-empty");
let last_byte = if signed {
let encoded = encode_overpunch_byte(last_digit, is_negative, codepage, policy)
.expect("valid overpunch for digit 0-9");
prop_assume!(is_valid_overpunch(encoded, codepage));
encoded
} else if codepage.is_ascii() {
0x30 + last_digit
} else {
0xF0 + last_digit
};
bytes.push(last_byte);
let mut scratch = ScratchBuffers::new();
let _ = decode_zoned_decimal_with_scratch(
&bytes,
digit_count,
0,
signed,
codepage,
false,
&mut scratch,
).expect("decoding constructed zoned bytes should succeed");
prop_assert_eq!(scratch.digit_buffer.len(), digits_vec.len());
prop_assert!(scratch.digit_buffer.iter().all(|&d| d <= 9));
prop_assert_eq!(&scratch.digit_buffer[..], &digits_vec[..]);
}
}
#[test]
fn test_zoned_decimal_blank_when_zero() {
let data = vec![0x40, 0x40, 0x40];
let result = decode_zoned_decimal(&data, 3, 0, false, Codepage::CP037, true).unwrap();
assert_eq!(result.to_string(), "0");
let data = vec![b' ', b' ', b' '];
let result = decode_zoned_decimal(&data, 3, 0, false, Codepage::ASCII, true).unwrap();
assert_eq!(result.to_string(), "0");
}
#[test]
fn test_packed_decimal_signs() {
let data = vec![0x12, 0x3C];
let result = decode_packed_decimal(&data, 3, 0, true).unwrap();
assert_eq!(result.to_string(), "123");
let data = vec![0x12, 0x3D];
let result = decode_packed_decimal(&data, 3, 0, true).unwrap();
assert_eq!(result.to_string(), "-123");
let encoded = encode_packed_decimal("-11", 2, 0, true).unwrap();
let result = decode_packed_decimal(&encoded, 2, 0, true).unwrap();
assert_eq!(
result.to_string(),
"-11",
"Failed to round-trip -11 correctly"
);
let data = vec![0x11, 0xDD]; let result = decode_packed_decimal(&data, 2, 0, true);
assert!(
result.is_err(),
"Should reject invalid format with sign in both nibbles"
);
}
#[test]
fn test_binary_int_big_endian() {
let data = vec![0x01, 0x23];
let result = decode_binary_int(&data, 16, false).unwrap();
assert_eq!(result, 291);
let data = vec![0x01, 0x23, 0x45, 0x67];
let result = decode_binary_int(&data, 32, false).unwrap();
assert_eq!(result, 19_088_743);
}
#[test]
fn test_alphanumeric_encoding() {
let result = encode_alphanumeric("HELLO", 10, Codepage::ASCII).unwrap();
assert_eq!(result, b"HELLO ");
let result = encode_alphanumeric("HELLO WORLD", 5, Codepage::ASCII);
assert!(result.is_err());
}
#[test]
fn test_bwz_policy() {
assert!(should_encode_as_blank_when_zero("0", true));
assert!(should_encode_as_blank_when_zero("0.00", true));
assert!(should_encode_as_blank_when_zero("0.000", true));
assert!(!should_encode_as_blank_when_zero("1", true));
assert!(!should_encode_as_blank_when_zero("0.01", true));
assert!(!should_encode_as_blank_when_zero("0", false));
}
#[test]
fn test_binary_width_mapping() {
assert_eq!(get_binary_width_from_digits(1), 16); assert_eq!(get_binary_width_from_digits(4), 16); assert_eq!(get_binary_width_from_digits(5), 32); assert_eq!(get_binary_width_from_digits(9), 32); assert_eq!(get_binary_width_from_digits(10), 64); assert_eq!(get_binary_width_from_digits(18), 64); }
#[test]
fn test_explicit_binary_width_validation() {
assert_eq!(validate_explicit_binary_width(1).unwrap(), 8);
assert_eq!(validate_explicit_binary_width(2).unwrap(), 16);
assert_eq!(validate_explicit_binary_width(4).unwrap(), 32);
assert_eq!(validate_explicit_binary_width(8).unwrap(), 64);
assert!(validate_explicit_binary_width(3).is_err());
assert!(validate_explicit_binary_width(16).is_err());
}
#[test]
fn test_zoned_decimal_with_bwz() {
let result =
encode_zoned_decimal_with_bwz("0", 3, 0, false, Codepage::ASCII, true).unwrap();
assert_eq!(result, vec![b' ', b' ', b' ']);
let result =
encode_zoned_decimal_with_bwz("0", 3, 0, false, Codepage::ASCII, false).unwrap();
assert_eq!(result, vec![0x30, 0x30, 0x30]);
let result =
encode_zoned_decimal_with_bwz("123", 3, 0, false, Codepage::ASCII, true).unwrap();
assert_eq!(result, vec![0x31, 0x32, 0x33]); }
#[test]
fn test_error_handling_invalid_numeric_inputs() {
let invalid_data = vec![0xFF]; let result = decode_packed_decimal(&invalid_data, 2, 0, false);
assert!(
result.is_err(),
"Invalid packed decimal should return error"
);
let error = result.unwrap_err();
assert!(
error.to_string().contains("CBKD"),
"Error should be CBKD code"
);
let short_data = vec![0x01]; let result = decode_binary_int(&short_data, 32, false);
assert!(
result.is_err(),
"Insufficient binary data should return error"
);
let invalid_zoned = b"12X"; let result = decode_zoned_decimal(invalid_zoned, 3, 0, false, Codepage::ASCII, false);
assert!(result.is_err(), "Invalid zoned decimal should return error");
let result = encode_alphanumeric("TOOLONGFORFIELD", 5, Codepage::ASCII);
assert!(
result.is_err(),
"Oversized alphanumeric should return error"
);
let error = result.unwrap_err();
assert!(
error.to_string().contains("CBKE"),
"Error should be CBKE code"
);
}
#[test]
fn test_boundary_conditions_numeric_operations() {
let max_packed_bytes = vec![0x99, 0x9C]; let result = decode_packed_decimal(&max_packed_bytes, 3, 0, true);
assert!(
result.is_ok(),
"Valid maximum packed decimal should succeed"
);
let zero_packed = vec![0x00, 0x0C]; let result = decode_packed_decimal(&zero_packed, 2, 0, true);
assert!(result.is_ok(), "Zero packed decimal should succeed");
let max_u16_bytes = vec![0xFF, 0xFF];
let result = decode_binary_int(&max_u16_bytes, 16, false);
assert!(result.is_ok(), "Maximum unsigned 16-bit should succeed");
let max_signed_16_bytes = vec![0x7F, 0xFF];
let result = decode_binary_int(&max_signed_16_bytes, 16, true);
assert!(result.is_ok(), "Maximum signed 16-bit should succeed");
let min_i16_bytes = vec![0x80, 0x00];
let result = decode_binary_int(&min_i16_bytes, 16, true);
assert!(result.is_ok(), "Minimum signed 16-bit should succeed");
}
#[test]
fn test_comp3_decimal_scale_fix() {
let input_value = "123.45";
let digits = 9; let scale = 2; let signed = true;
let encoded_data = encode_packed_decimal(input_value, digits, scale, signed).unwrap();
let decoded = decode_packed_decimal(&encoded_data, digits, scale, signed).unwrap();
assert_eq!(decoded.to_string(), "123.45", "COMP-3 round-trip failed");
let negative_value = "-999.99";
let encoded_neg = encode_packed_decimal(negative_value, digits, scale, signed).unwrap();
let decoded_neg = decode_packed_decimal(&encoded_neg, digits, scale, signed).unwrap();
assert_eq!(
decoded_neg.to_string(),
"-999.99",
"Negative COMP-3 round-trip failed"
);
}
#[test]
fn test_error_path_coverage_arithmetic_operations() {
let decimal = SmallDecimal::new(i64::MAX, 0, false);
assert_eq!(decimal.value, i64::MAX);
assert_eq!(decimal.scale, 0);
assert!(!decimal.negative);
let large_decimal = SmallDecimal::new(999_999_999, 0, false);
assert_eq!(large_decimal.value, 999_999_999);
let mut small_decimal = SmallDecimal::new(1, 10, false);
small_decimal.normalize(); assert!(small_decimal.scale >= 0);
let negative_decimal = SmallDecimal::new(-1, 0, true);
assert!(
negative_decimal.is_negative(),
"Signed negative should be negative"
);
let positive_decimal = SmallDecimal::new(1, 0, false);
assert!(
!positive_decimal.is_negative(),
"Unsigned should not be negative"
);
}
}