use crate::memory::ScratchBuffers;
use crate::options::{Codepage, FloatFormat, ZonedEncodingFormat};
use crate::zoned_overpunch::{ZeroSignPolicy, encode_overpunch_byte};
use copybook_core::{Error, ErrorCode, Result, SignPlacement, SignSeparateInfo};
use std::convert::TryFrom;
use std::fmt::Write;
use tracing::warn;
const ASCII_DIGIT_ZONE: u8 = 0x3; const EBCDIC_DIGIT_ZONE: u8 = 0xF;
#[inline]
pub(crate) fn likely(b: bool) -> bool {
if b {
true
} else {
cold_branch_hint();
false
}
}
#[inline]
pub(crate) fn unlikely(b: bool) -> bool {
if b {
cold_branch_hint();
true
} else {
false
}
}
#[cold]
#[inline(never)]
fn cold_branch_hint() {
}
#[inline]
fn create_normalized_decimal(value: i64, scale: i16, is_negative: bool) -> SmallDecimal {
let mut decimal = SmallDecimal::new(value, scale, is_negative);
decimal.normalize();
decimal
}
#[derive(Debug, Default)]
#[allow(clippy::struct_field_names)] struct EncodingAnalysisStats {
ascii_count: usize,
ebcdic_count: usize,
invalid_count: usize,
}
impl EncodingAnalysisStats {
fn new() -> Self {
Self::default()
}
fn record_format(&mut self, format: Option<ZonedEncodingFormat>) {
match format {
Some(ZonedEncodingFormat::Ascii) => self.ascii_count += 1,
Some(ZonedEncodingFormat::Ebcdic) => self.ebcdic_count += 1,
Some(ZonedEncodingFormat::Auto) => { }
None => self.invalid_count += 1,
}
}
fn determine_overall_format(&self) -> (ZonedEncodingFormat, bool) {
if self.invalid_count > 0 {
(ZonedEncodingFormat::Auto, true)
} else if self.ascii_count > 0 && self.ebcdic_count > 0 {
(ZonedEncodingFormat::Auto, true)
} else if self.ascii_count > 0 {
(ZonedEncodingFormat::Ascii, false)
} else if self.ebcdic_count > 0 {
(ZonedEncodingFormat::Ebcdic, false)
} else {
(ZonedEncodingFormat::Auto, false)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZonedEncodingInfo {
pub detected_format: ZonedEncodingFormat,
pub has_mixed_encoding: bool,
pub byte_formats: Vec<Option<ZonedEncodingFormat>>,
}
impl ZonedEncodingInfo {
#[inline]
#[must_use]
pub fn new(detected_format: ZonedEncodingFormat, has_mixed_encoding: bool) -> Self {
Self {
detected_format,
has_mixed_encoding,
byte_formats: Vec::new(),
}
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn detect_from_data(data: &[u8]) -> Result<Self> {
if data.is_empty() {
return Ok(Self::new(ZonedEncodingFormat::Auto, false));
}
let mut byte_formats = Vec::with_capacity(data.len());
let mut encoding_stats = EncodingAnalysisStats::new();
for &byte in data {
let format = Self::analyze_zone_nibble(byte);
byte_formats.push(format);
encoding_stats.record_format(format);
}
let (detected_format, has_mixed_encoding) = encoding_stats.determine_overall_format();
Ok(Self {
detected_format,
has_mixed_encoding,
byte_formats,
})
}
fn analyze_zone_nibble(byte: u8) -> Option<ZonedEncodingFormat> {
const ASCII_ZONE: u8 = 0x3;
const EBCDIC_ZONE: u8 = 0xF;
const ZONE_MASK: u8 = 0x0F;
match byte {
0x7B | 0x7D | 0x41..=0x52 => return Some(ZonedEncodingFormat::Ascii),
_ => {}
}
let zone_nibble = (byte >> 4) & ZONE_MASK;
match zone_nibble {
ASCII_ZONE => Some(ZonedEncodingFormat::Ascii),
EBCDIC_ZONE | 0xC | 0xD => Some(ZonedEncodingFormat::Ebcdic),
_ => None, }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SmallDecimal {
pub value: i64,
pub scale: i16,
pub negative: bool,
}
impl SmallDecimal {
#[inline]
#[must_use]
pub fn new(value: i64, scale: i16, negative: bool) -> Self {
Self {
value,
scale,
negative,
}
}
#[inline]
#[must_use]
pub fn zero(scale: i16) -> Self {
Self {
value: 0,
scale,
negative: false,
}
}
#[inline]
pub fn normalize(&mut self) {
if self.value == 0 {
self.negative = false;
}
}
#[allow(clippy::inherent_to_string)] #[inline]
#[must_use = "Use the formatted string output"]
pub fn to_string(&self) -> String {
let mut result = String::new();
self.append_sign_if_negative(&mut result);
self.append_formatted_value(&mut result);
result
}
fn is_zero_value(&self) -> bool {
self.value == 0
}
fn append_sign_if_negative(&self, result: &mut String) {
if self.negative && !self.is_zero_value() {
result.push('-');
}
}
fn append_formatted_value(&self, result: &mut String) {
if self.scale <= 0 {
self.append_integer_format(result);
} else {
self.append_decimal_format(result);
}
}
fn append_integer_format(&self, result: &mut String) {
let scaled_value = if self.scale < 0 {
self.value * 10_i64.pow(scale_abs_to_u32(self.scale))
} else {
self.value
};
if write!(result, "{scaled_value}").is_err() {
result.push_str("ERR");
}
}
fn append_decimal_format(&self, result: &mut String) {
let divisor = 10_i64.pow(scale_abs_to_u32(self.scale));
let integer_part = self.value / divisor;
let fractional_part = self.value % divisor;
let width = usize::try_from(self.scale).unwrap_or_else(|_| {
debug_assert!(false, "scale should be positive when formatting decimal");
0
});
if write!(result, "{integer_part}.{fractional_part:0width$}").is_err() {
result.push_str("ERR");
}
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn from_str(s: &str, expected_scale: i16) -> Result<Self> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Ok(Self::zero(expected_scale));
}
let (negative, numeric_part) = Self::extract_sign(trimmed);
if let Some(dot_pos) = numeric_part.find('.') {
Self::parse_decimal_format(numeric_part, dot_pos, expected_scale, negative)
} else {
Self::parse_integer_format(numeric_part, expected_scale, negative)
}
}
fn extract_sign(s: &str) -> (bool, &str) {
if let Some(without_minus) = s.strip_prefix('-') {
(true, without_minus)
} else {
(false, s)
}
}
fn parse_decimal_format(
numeric_part: &str,
dot_pos: usize,
expected_scale: i16,
negative: bool,
) -> Result<Self> {
let integer_part = &numeric_part[..dot_pos];
let fractional_part = &numeric_part[dot_pos + 1..];
let expected_len = usize::try_from(expected_scale).map_err(|_| {
Error::new(
ErrorCode::CBKE505_SCALE_MISMATCH,
format!(
"Scale mismatch: expected {expected_scale} decimal places, got {}",
fractional_part.len()
),
)
})?;
if fractional_part.len() != expected_len {
return Err(Error::new(
ErrorCode::CBKE505_SCALE_MISMATCH,
format!(
"Scale mismatch: expected {expected_scale} decimal places, got {}",
fractional_part.len()
),
));
}
let integer_value = Self::parse_integer_component(integer_part)?;
let fractional_value = Self::parse_integer_component(fractional_part)?;
let total_value =
Self::combine_integer_and_fractional(integer_value, fractional_value, expected_scale)?;
let mut result = Self::new(total_value, expected_scale, negative);
result.normalize();
Ok(result)
}
fn parse_integer_format(
numeric_part: &str,
expected_scale: i16,
negative: bool,
) -> Result<Self> {
if expected_scale != 0 {
let value = Self::parse_integer_component(numeric_part)?;
if value == 0 {
let mut result = Self::new(0, expected_scale, negative);
result.normalize();
return Ok(result);
}
return Err(Error::new(
ErrorCode::CBKE505_SCALE_MISMATCH,
format!("Scale mismatch: expected {expected_scale} decimal places, got integer"),
));
}
let value = Self::parse_integer_component(numeric_part)?;
let mut result = Self::new(value, expected_scale, negative);
result.normalize();
Ok(result)
}
fn parse_integer_component(s: &str) -> Result<i64> {
s.parse().map_err(|_| {
Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Invalid numeric component: '{s}'"),
)
})
}
fn combine_integer_and_fractional(
integer_value: i64,
fractional_value: i64,
scale: i16,
) -> Result<i64> {
let divisor = 10_i64.pow(scale_abs_to_u32(scale));
integer_value
.checked_mul(divisor)
.and_then(|v| v.checked_add(fractional_value))
.ok_or_else(|| {
Error::new(
ErrorCode::CBKE510_NUMERIC_OVERFLOW,
"Numeric value too large - would cause overflow",
)
})
}
#[inline]
#[must_use]
pub fn to_fixed_scale_string(&self, scale: i16) -> String {
let mut result = String::new();
if self.negative && self.value != 0 {
result.push('-');
}
if scale <= 0 {
let scaled_value = if scale < 0 {
self.value * 10_i64.pow(scale_abs_to_u32(scale))
} else {
self.value
};
if write!(result, "{scaled_value}").is_err() {
result.push_str("ERR");
}
} else {
let divisor = 10_i64.pow(scale_abs_to_u32(scale));
let integer_part = self.value / divisor;
let fractional_part = self.value % divisor;
let width = usize::try_from(scale).unwrap_or_else(|_| {
debug_assert!(false, "scale should be positive in decimal formatting");
0
});
if write!(result, "{integer_part}.{fractional_part:0width$}").is_err() {
result.push_str("ERR");
}
}
result
}
#[inline]
pub fn format_to_scratch_buffer(&self, scale: i16, scratch_buffer: &mut String) {
scratch_buffer.clear();
if self.negative && self.value != 0 {
scratch_buffer.push('-');
}
if scale <= 0 {
let scaled_value = if scale < 0 {
self.value * 10_i64.pow(scale_abs_to_u32(scale))
} else {
self.value
};
Self::format_integer_manual(scaled_value, scratch_buffer);
} else {
let divisor = 10_i64.pow(scale_abs_to_u32(scale));
let integer_part = self.value / divisor;
let fractional_part = self.value % divisor;
Self::format_integer_manual(integer_part, scratch_buffer);
scratch_buffer.push('.');
Self::format_integer_with_leading_zeros(
fractional_part,
scale_abs_to_u32(scale),
scratch_buffer,
);
}
}
#[inline]
fn format_integer_manual(mut value: i64, buffer: &mut String) {
if value == 0 {
buffer.push('0');
return;
}
if value < 100 {
if value < 10 {
push_digit(buffer, value);
} else {
let tens = value / 10;
let ones = value % 10;
push_digit(buffer, tens);
push_digit(buffer, ones);
}
return;
}
let mut digits = [0u8; 20]; let mut count = 0;
while value > 0 && count < 20 {
digits[count] = digit_from_value(value % 10);
value /= 10;
count += 1;
}
for i in (0..count).rev() {
buffer.push(char::from(b'0' + digits[i]));
}
}
#[inline]
fn format_integer_with_leading_zeros(mut value: i64, width: u32, buffer: &mut String) {
if width <= 4 && value < 10000 {
match width {
1 => {
push_digit(buffer, value);
}
2 => {
push_digit(buffer, value / 10);
push_digit(buffer, value % 10);
}
3 => {
push_digit(buffer, value / 100);
push_digit(buffer, (value / 10) % 10);
push_digit(buffer, value % 10);
}
4 => {
push_digit(buffer, value / 1000);
push_digit(buffer, (value / 100) % 10);
push_digit(buffer, (value / 10) % 10);
push_digit(buffer, value % 10);
}
_ => {}
}
return;
}
let mut digits = [0u8; 20]; let mut count = 0;
let target_width = usize::try_from(width).unwrap_or(usize::MAX).min(20);
loop {
digits[count] = digit_from_value(value % 10);
value /= 10;
count += 1;
if value == 0 && count >= target_width {
break;
}
if count >= 20 {
break;
}
}
while count < target_width {
digits[count] = 0;
count += 1;
}
for i in (0..count).rev() {
buffer.push(char::from(b'0' + digits[i]));
}
}
#[inline]
#[must_use]
pub fn scale(&self) -> i16 {
self.scale
}
#[inline]
#[must_use]
pub fn is_negative(&self) -> bool {
self.negative && self.value != 0
}
#[inline]
#[must_use]
pub fn total_digits(&self) -> u16 {
if self.value == 0 {
return 1;
}
let mut count = 0;
let mut val = self.value.abs();
while val > 0 {
count += 1;
val /= 10;
}
count
}
}
#[inline]
fn digit_from_value(value: i64) -> u8 {
match u8::try_from(value) {
Ok(digit) if digit <= 9 => digit,
_ => {
debug_assert!(false, "digit out of range: {value}");
0
}
}
}
#[inline]
fn push_digit(buffer: &mut String, digit: i64) {
buffer.push(char::from(b'0' + digit_from_value(digit)));
}
#[inline]
fn scale_abs_to_u32(scale: i16) -> u32 {
u32::from(scale.unsigned_abs())
}
#[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]
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 decode_binary_int(data: &[u8], bits: u16, signed: bool) -> Result<i64> {
let expected_bytes = usize::from(bits / 8);
if data.len() != expected_bytes {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, format!(
"Binary data length {} doesn't match expected {} bytes for {} bits",
data.len(),
expected_bytes,
bits
),
));
}
match bits {
16 => {
if data.len() != 2 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"16-bit binary field requires exactly 2 bytes",
));
}
let value = u16::from_be_bytes([data[0], data[1]]);
if signed {
Ok(i64::from(i16::from_be_bytes([data[0], data[1]])))
} else {
Ok(i64::from(value))
}
}
32 => {
if data.len() != 4 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"32-bit binary field requires exactly 4 bytes",
));
}
let value = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
if signed {
Ok(i64::from(i32::from_be_bytes([
data[0], data[1], data[2], data[3],
])))
} else {
Ok(i64::from(value))
}
}
64 => {
if data.len() != 8 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"64-bit binary field requires exactly 8 bytes",
));
}
let bytes: [u8; 8] = data.try_into().map_err(|_| {
Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
"Failed to convert data to 8-byte array",
)
})?;
if signed {
Ok(i64::from_be_bytes(bytes))
} else {
let value = u64::from_be_bytes(bytes);
let max_i64 = u64::try_from(i64::MAX).unwrap_or(u64::MAX);
if value > max_i64 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Unsigned 64-bit value {value} exceeds i64::MAX"),
));
}
i64::try_from(value).map_err(|_| {
Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Unsigned 64-bit value {value} exceeds i64::MAX"),
)
})
}
}
_ => Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Unsupported binary field width: {bits} bits"),
)),
}
}
#[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 = "Handle the Result or propagate the error"]
pub fn encode_binary_int(value: i64, bits: u16, signed: bool) -> Result<Vec<u8>> {
match bits {
16 => {
if signed {
let int_value = i16::try_from(value).map_err(|_| {
Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Value {value} out of range for signed 16-bit integer"),
)
})?;
Ok(int_value.to_be_bytes().to_vec())
} else {
let int_value = u16::try_from(value).map_err(|_| {
Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Value {value} out of range for unsigned 16-bit integer"),
)
})?;
Ok(int_value.to_be_bytes().to_vec())
}
}
32 => {
if signed {
let int_value = i32::try_from(value).map_err(|_| {
Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Value {value} out of range for signed 32-bit integer"),
)
})?;
Ok(int_value.to_be_bytes().to_vec())
} else {
let int_value = u32::try_from(value).map_err(|_| {
Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Value {value} out of range for unsigned 32-bit integer"),
)
})?;
Ok(int_value.to_be_bytes().to_vec())
}
}
64 => {
if signed {
Ok(value.to_be_bytes().to_vec())
} else {
let int_value = u64::try_from(value).map_err(|_| {
Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Value {value} cannot be negative for unsigned 64-bit integer"),
)
})?;
Ok(int_value.to_be_bytes().to_vec())
}
}
_ => Err(Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Unsupported binary field width: {bits} bits"),
)),
}
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn encode_alphanumeric(text: &str, field_len: usize, codepage: Codepage) -> Result<Vec<u8>> {
let encoded_bytes = crate::charset::utf8_to_ebcdic(text, codepage)?;
if encoded_bytes.len() > field_len {
return Err(Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!(
"Text length {} exceeds field length {}",
encoded_bytes.len(),
field_len
),
));
}
let mut result = encoded_bytes;
let space_byte = match codepage {
Codepage::ASCII => b' ',
_ => 0x40, };
result.resize(field_len, space_byte);
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]
#[must_use]
pub fn get_binary_width_from_digits(digits: u16) -> u16 {
match digits {
1..=4 => 16, 5..=9 => 32, _ => 64, }
}
#[inline]
#[must_use = "Handle the Result or propagate the error"]
pub fn validate_explicit_binary_width(width_bytes: u8) -> Result<u16> {
match width_bytes {
1 => Ok(8), 2 => Ok(16), 4 => Ok(32), 8 => Ok(64), _ => Err(Error::new(
ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
format!("Invalid explicit binary width: {width_bytes} bytes. Must be 1, 2, 4, or 8"),
)),
}
}
#[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 decode_binary_int_fast(data: &[u8], bits: u16, signed: bool) -> Result<i64> {
match (bits, data.len()) {
(16, 2) => {
let bytes = [data[0], data[1]];
if signed {
Ok(i64::from(i16::from_be_bytes(bytes)))
} else {
Ok(i64::from(u16::from_be_bytes(bytes)))
}
}
(32, 4) => {
let bytes = [data[0], data[1], data[2], data[3]];
if signed {
Ok(i64::from(i32::from_be_bytes(bytes)))
} else {
Ok(i64::from(u32::from_be_bytes(bytes)))
}
}
(64, 8) => {
let bytes = [
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
];
if signed {
Ok(i64::from_be_bytes(bytes))
} else {
let value = u64::from_be_bytes(bytes);
let max_i64 = u64::try_from(i64::MAX).unwrap_or(u64::MAX);
if value > max_i64 {
return Err(Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Unsigned 64-bit value {value} exceeds i64::MAX"),
));
}
i64::try_from(value).map_err(|_| {
Error::new(
ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
format!("Unsigned 64-bit value {value} exceeds i64::MAX"),
)
})
}
}
_ => {
decode_binary_int(data, bits, signed)
}
}
}
#[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))
}
const IBM_HEX_EXPONENT_BIAS: i32 = 64;
const IBM_HEX_FRACTION_MIN: f64 = 1.0 / 16.0;
const IBM_HEX_SINGLE_FRACTION_BITS: u32 = 24;
const IBM_HEX_DOUBLE_FRACTION_BITS: u32 = 56;
#[inline]
fn validate_float_buffer_len(data: &[u8], required: usize, usage: &str) -> Result<()> {
if data.len() < required {
return Err(Error::new(
ErrorCode::CBKD301_RECORD_TOO_SHORT,
format!("{usage} requires {required} bytes, got {}", data.len()),
));
}
Ok(())
}
#[inline]
fn validate_float_encode_buffer_len(buffer: &[u8], required: usize, usage: &str) -> Result<()> {
if buffer.len() < required {
return Err(Error::new(
ErrorCode::CBKE510_NUMERIC_OVERFLOW,
format!(
"{usage} requires {required} bytes, buffer has {}",
buffer.len()
),
));
}
Ok(())
}
#[inline]
#[allow(clippy::cast_precision_loss)]
fn decode_ibm_hex_to_f64(sign: bool, exponent_raw: u8, fraction_bits: u64, bits: u32) -> f64 {
if exponent_raw == 0 && fraction_bits == 0 {
return if sign { -0.0 } else { 0.0 };
}
let exponent = i32::from(exponent_raw) - IBM_HEX_EXPONENT_BIAS;
let divisor = 2_f64.powi(i32::try_from(bits).unwrap_or(0));
let fraction = (fraction_bits as f64) / divisor;
let magnitude = fraction * 16_f64.powi(exponent);
if sign { -magnitude } else { magnitude }
}
#[inline]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn encode_f64_to_ibm_hex_parts(value: f64, bits: u32) -> Result<(u8, u64)> {
if !value.is_finite() {
return Err(Error::new(
ErrorCode::CBKE510_NUMERIC_OVERFLOW,
"IBM hex float encoding requires finite values",
));
}
if value == 0.0 {
return Ok((0, 0));
}
let mut exponent = IBM_HEX_EXPONENT_BIAS;
let mut fraction = value.abs();
while fraction < IBM_HEX_FRACTION_MIN {
fraction *= 16.0;
exponent -= 1;
if exponent <= 0 {
return Ok((0, 0));
}
}
while fraction >= 1.0 {
fraction /= 16.0;
exponent += 1;
if exponent >= 128 {
return Err(Error::new(
ErrorCode::CBKE510_NUMERIC_OVERFLOW,
"IBM hex float exponent overflow",
));
}
}
let scale = 2_f64.powi(i32::try_from(bits).unwrap_or(0));
let mut fraction_bits = (fraction * scale).round() as u64;
let full_scale = 1_u64 << bits;
if fraction_bits >= full_scale {
fraction_bits = 1_u64 << (bits - 4);
exponent += 1;
if exponent >= 128 {
return Err(Error::new(
ErrorCode::CBKE510_NUMERIC_OVERFLOW,
"IBM hex float exponent overflow",
));
}
}
Ok((u8::try_from(exponent).unwrap_or(0), fraction_bits))
}
#[inline]
pub fn decode_float_single_ieee_be(data: &[u8]) -> Result<f32> {
validate_float_buffer_len(data, 4, "COMP-1")?;
let bytes: [u8; 4] = [data[0], data[1], data[2], data[3]];
Ok(f32::from_be_bytes(bytes))
}
#[inline]
pub fn decode_float_double_ieee_be(data: &[u8]) -> Result<f64> {
validate_float_buffer_len(data, 8, "COMP-2")?;
let bytes: [u8; 8] = [
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
];
Ok(f64::from_be_bytes(bytes))
}
#[inline]
pub fn decode_float_single_ibm_hex(data: &[u8]) -> Result<f32> {
validate_float_buffer_len(data, 4, "COMP-1")?;
let word = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let sign = (word & 0x8000_0000) != 0;
let exponent_raw = ((word >> 24) & 0x7F) as u8;
let fraction_bits = u64::from(word & 0x00FF_FFFF);
let value = decode_ibm_hex_to_f64(
sign,
exponent_raw,
fraction_bits,
IBM_HEX_SINGLE_FRACTION_BITS,
);
#[allow(clippy::cast_possible_truncation)]
{
Ok(value as f32)
}
}
#[inline]
pub fn decode_float_double_ibm_hex(data: &[u8]) -> Result<f64> {
validate_float_buffer_len(data, 8, "COMP-2")?;
let word = u64::from_be_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
]);
let sign = (word & 0x8000_0000_0000_0000) != 0;
let exponent_raw = ((word >> 56) & 0x7F) as u8;
let fraction_bits = word & 0x00FF_FFFF_FFFF_FFFF;
Ok(decode_ibm_hex_to_f64(
sign,
exponent_raw,
fraction_bits,
IBM_HEX_DOUBLE_FRACTION_BITS,
))
}
#[inline]
pub fn decode_float_single_with_format(data: &[u8], format: FloatFormat) -> Result<f32> {
match format {
FloatFormat::IeeeBigEndian => decode_float_single_ieee_be(data),
FloatFormat::IbmHex => decode_float_single_ibm_hex(data),
}
}
#[inline]
pub fn decode_float_double_with_format(data: &[u8], format: FloatFormat) -> Result<f64> {
match format {
FloatFormat::IeeeBigEndian => decode_float_double_ieee_be(data),
FloatFormat::IbmHex => decode_float_double_ibm_hex(data),
}
}
#[inline]
pub fn decode_float_single(data: &[u8]) -> Result<f32> {
decode_float_single_ieee_be(data)
}
#[inline]
pub fn decode_float_double(data: &[u8]) -> Result<f64> {
decode_float_double_ieee_be(data)
}
#[inline]
pub fn encode_float_single_ieee_be(value: f32, buffer: &mut [u8]) -> Result<()> {
validate_float_encode_buffer_len(buffer, 4, "COMP-1")?;
let bytes = value.to_be_bytes();
buffer[..4].copy_from_slice(&bytes);
Ok(())
}
#[inline]
pub fn encode_float_double_ieee_be(value: f64, buffer: &mut [u8]) -> Result<()> {
validate_float_encode_buffer_len(buffer, 8, "COMP-2")?;
let bytes = value.to_be_bytes();
buffer[..8].copy_from_slice(&bytes);
Ok(())
}
#[inline]
pub fn encode_float_single_ibm_hex(value: f32, buffer: &mut [u8]) -> Result<()> {
validate_float_encode_buffer_len(buffer, 4, "COMP-1")?;
let sign = value.is_sign_negative();
let (exponent_raw, fraction_bits) =
encode_f64_to_ibm_hex_parts(f64::from(value), IBM_HEX_SINGLE_FRACTION_BITS)?;
let sign_bit = if sign { 0x8000_0000 } else { 0 };
let fraction_low = u32::try_from(fraction_bits & 0x00FF_FFFF).map_err(|_| {
Error::new(
ErrorCode::CBKE510_NUMERIC_OVERFLOW,
"IBM hex fraction overflow for COMP-1",
)
})?;
let word = sign_bit | (u32::from(exponent_raw) << 24) | fraction_low;
buffer[..4].copy_from_slice(&word.to_be_bytes());
Ok(())
}
#[inline]
pub fn encode_float_double_ibm_hex(value: f64, buffer: &mut [u8]) -> Result<()> {
validate_float_encode_buffer_len(buffer, 8, "COMP-2")?;
let sign = value.is_sign_negative();
let (exponent_raw, fraction_bits) =
encode_f64_to_ibm_hex_parts(value, IBM_HEX_DOUBLE_FRACTION_BITS)?;
let sign_bit = if sign { 0x8000_0000_0000_0000 } else { 0 };
let word = sign_bit | (u64::from(exponent_raw) << 56) | (fraction_bits & 0x00FF_FFFF_FFFF_FFFF);
buffer[..8].copy_from_slice(&word.to_be_bytes());
Ok(())
}
#[inline]
pub fn encode_float_single_with_format(
value: f32,
buffer: &mut [u8],
format: FloatFormat,
) -> Result<()> {
match format {
FloatFormat::IeeeBigEndian => encode_float_single_ieee_be(value, buffer),
FloatFormat::IbmHex => encode_float_single_ibm_hex(value, buffer),
}
}
#[inline]
pub fn encode_float_double_with_format(
value: f64,
buffer: &mut [u8],
format: FloatFormat,
) -> Result<()> {
match format {
FloatFormat::IeeeBigEndian => encode_float_double_ieee_be(value, buffer),
FloatFormat::IbmHex => encode_float_double_ibm_hex(value, buffer),
}
}
#[inline]
pub fn encode_float_single(value: f32, buffer: &mut [u8]) -> Result<()> {
encode_float_single_ieee_be(value, buffer)
}
#[inline]
pub fn encode_float_double(value: f64, buffer: &mut [u8]) -> Result<()> {
encode_float_double_ieee_be(value, 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"
);
}
}