use crate::options::ZonedEncodingFormat;
use copybook_core::{Error, ErrorCode, Result};
use std::fmt::Write;
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) 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]
pub(super) fn scale_abs_to_u32(scale: i16) -> u32 {
u32::from(scale.unsigned_abs())
}