use copybook_core::{Error, ErrorCode, Result};
use tracing::warn;
#[derive(Debug, Clone, PartialEq)]
pub enum PicToken {
Digit,
ZeroSuppress,
ZeroInsert,
AsteriskFill,
Space,
Comma,
Slash,
DecimalPoint,
Currency,
LeadingPlus,
LeadingMinus,
TrailingPlus,
TrailingMinus,
Credit,
Debit,
}
impl std::fmt::Display for PicToken {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Digit => write!(f, "9"),
Self::ZeroSuppress => write!(f, "Z"),
Self::ZeroInsert => write!(f, "0"),
Self::AsteriskFill => write!(f, "*"),
Self::Space => write!(f, "B"),
Self::Comma => write!(f, ","),
Self::Slash => write!(f, "/"),
Self::DecimalPoint => write!(f, "."),
Self::Currency => write!(f, "$"),
Self::LeadingPlus | Self::TrailingPlus => write!(f, "+"),
Self::LeadingMinus | Self::TrailingMinus => write!(f, "-"),
Self::Credit => write!(f, "CR"),
Self::Debit => write!(f, "DB"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Sign {
Positive,
Negative,
}
#[inline]
#[allow(clippy::too_many_lines)]
pub fn tokenize_edited_pic(pic_str: &str) -> Result<Vec<PicToken>> {
let mut tokens = Vec::new();
let mut chars = pic_str.chars().peekable();
let mut found_decimal = false;
if chars.peek() == Some(&'S') || chars.peek() == Some(&'s') {
chars.next();
}
while let Some(ch) = chars.next() {
match ch.to_ascii_uppercase() {
'9' => {
let count = parse_repetition(&mut chars)?;
for _ in 0..count {
tokens.push(PicToken::Digit);
}
}
'Z' => {
let count = parse_repetition(&mut chars)?;
for _ in 0..count {
tokens.push(PicToken::ZeroSuppress);
}
}
'0' => {
let count = parse_repetition(&mut chars)?;
for _ in 0..count {
tokens.push(PicToken::ZeroInsert);
}
}
'*' => {
let count = parse_repetition(&mut chars)?;
for _ in 0..count {
tokens.push(PicToken::AsteriskFill);
}
}
'B' => {
let count = parse_repetition(&mut chars)?;
for _ in 0..count {
tokens.push(PicToken::Space);
}
}
',' => tokens.push(PicToken::Comma),
'/' => tokens.push(PicToken::Slash),
'.' => {
if found_decimal {
return Err(Error::new(
ErrorCode::CBKP001_SYNTAX,
format!("Multiple decimal points in edited PIC: {pic_str}"),
));
}
found_decimal = true;
tokens.push(PicToken::DecimalPoint);
}
'$' => tokens.push(PicToken::Currency),
'+' => {
if tokens.is_empty() {
tokens.push(PicToken::LeadingPlus);
} else {
tokens.push(PicToken::TrailingPlus);
}
}
'-' => {
if tokens.is_empty() {
tokens.push(PicToken::LeadingMinus);
} else {
tokens.push(PicToken::TrailingMinus);
}
}
'C' => {
if let Some(&next_ch) = chars.peek()
&& (next_ch == 'R' || next_ch == 'r')
{
chars.next(); tokens.push(PicToken::Credit);
} else {
return Err(Error::new(
ErrorCode::CBKP001_SYNTAX,
format!("Invalid character 'C' in edited PIC: {pic_str}"),
));
}
}
'D' => {
if let Some(&next_ch) = chars.peek()
&& (next_ch == 'B' || next_ch == 'b')
{
chars.next(); tokens.push(PicToken::Debit);
} else {
return Err(Error::new(
ErrorCode::CBKP001_SYNTAX,
format!("Invalid character 'D' in edited PIC: {pic_str}"),
));
}
}
_ => {
}
}
}
if tokens.is_empty() {
return Err(Error::new(
ErrorCode::CBKP001_SYNTAX,
format!("Empty or invalid edited PIC pattern: {pic_str}"),
));
}
Ok(tokens)
}
fn parse_repetition<I>(chars: &mut std::iter::Peekable<I>) -> Result<usize>
where
I: Iterator<Item = char>,
{
if chars.peek() == Some(&'(') {
chars.next(); let mut count_str = String::new();
while let Some(&ch) = chars.peek() {
if ch == ')' {
chars.next(); break;
} else if ch.is_ascii_digit() {
count_str.push(ch);
chars.next();
} else {
return Err(Error::new(
ErrorCode::CBKP001_SYNTAX,
format!("Invalid repetition count: {count_str}"),
));
}
}
count_str.parse::<usize>().map_err(|_| {
Error::new(
ErrorCode::CBKP001_SYNTAX,
format!("Invalid repetition count: {count_str}"),
)
})
} else {
Ok(1)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct NumericValue {
pub sign: Sign,
pub digits: String,
pub scale: u16,
}
impl NumericValue {
#[must_use]
#[inline]
pub fn to_decimal_string(&self) -> String {
if self.digits.is_empty() || self.digits.chars().all(|c| c == '0') {
return "0".to_string();
}
let sign_prefix = match self.sign {
Sign::Positive => "",
Sign::Negative => "-",
};
if self.scale == 0 {
format!("{sign_prefix}{}", self.digits)
} else {
let scale = self.scale as usize;
let digits_len = self.digits.len();
if scale >= digits_len {
let zeros = "0".repeat(scale - digits_len);
format!("{sign_prefix}0.{zeros}{}", self.digits)
} else {
let (int_part, frac_part) = self.digits.split_at(digits_len - scale);
if int_part.is_empty() {
format!("{sign_prefix}0.{frac_part}")
} else {
format!("{sign_prefix}{int_part}.{frac_part}")
}
}
}
}
}
#[inline]
#[allow(clippy::too_many_lines)]
pub fn decode_edited_numeric(
input: &str,
pattern: &[PicToken],
scale: u16,
blank_when_zero: bool,
) -> Result<NumericValue> {
if blank_when_zero && input.chars().all(|c| c == ' ') {
warn!("CBKD423_EDITED_PIC_BLANK_WHEN_ZERO: Edited PIC field is blank, decoding as zero");
crate::lib_api::increment_warning_counter();
return Ok(NumericValue {
sign: Sign::Positive,
digits: "0".to_string(),
scale,
});
}
let input_chars: Vec<char> = input.chars().collect();
let mut pattern_idx = 0;
let mut input_idx = 0;
let mut digits = String::new();
let mut sign = Sign::Positive;
let mut found_non_zero = false;
while pattern_idx < pattern.len() {
let token = &pattern[pattern_idx];
if input_idx >= input_chars.len() {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!(
"Input too short for edited PIC pattern (expected {} characters, got {})",
pattern.len(),
input.len()
),
));
}
let input_char = input_chars[input_idx];
match token {
PicToken::Digit
| PicToken::ZeroSuppress
| PicToken::ZeroInsert
| PicToken::AsteriskFill => {
if input_char.is_ascii_digit() {
let digit_val = input_char;
if digit_val != '0' {
found_non_zero = true;
}
if found_non_zero || matches!(token, PicToken::Digit | PicToken::ZeroInsert) {
digits.push(digit_val);
} else {
digits.push('0');
}
} else if input_char == ' ' {
if matches!(token, PicToken::Digit) {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!("Expected digit but found space at position {input_idx}"),
));
}
digits.push('0');
} else if input_char == '*' {
if !matches!(token, PicToken::AsteriskFill) {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!("Unexpected asterisk at position {input_idx}"),
));
}
digits.push('0');
} else {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!(
"Expected digit, space, or asterisk but found '{input_char}' at position {input_idx}"
),
));
}
input_idx += 1;
}
PicToken::Space => {
if input_char != ' ' && input_char != 'B' {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!("Expected space but found '{input_char}' at position {input_idx}"),
));
}
input_idx += 1;
}
PicToken::Comma => {
if input_char != ',' {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!("Expected comma but found '{input_char}' at position {input_idx}"),
));
}
input_idx += 1;
}
PicToken::Slash => {
if input_char != '/' {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!("Expected slash but found '{input_char}' at position {input_idx}"),
));
}
input_idx += 1;
}
PicToken::DecimalPoint => {
if input_char != '.' {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!(
"Expected decimal point but found '{input_char}' at position {input_idx}"
),
));
}
input_idx += 1;
}
PicToken::Currency => {
if input_char != '$' && input_char != ' ' {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!(
"Expected currency symbol but found '{input_char}' at position {input_idx}"
),
));
}
input_idx += 1;
}
PicToken::LeadingPlus => {
if input_char == '+' || input_char == ' ' {
sign = Sign::Positive;
} else if input_char == '-' {
sign = Sign::Negative;
} else {
return Err(Error::new(
ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
format!("Expected '+' or '-' for leading plus but found '{input_char}'"),
));
}
input_idx += 1;
}
PicToken::LeadingMinus => {
if input_char == '-' {
sign = Sign::Negative;
} else if input_char == ' ' {
sign = Sign::Positive;
} else {
return Err(Error::new(
ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
format!("Expected '-' for leading minus but found '{input_char}'"),
));
}
input_idx += 1;
}
PicToken::TrailingPlus => {
if input_char == '+' || input_char == ' ' {
sign = Sign::Positive;
} else if input_char == '-' {
sign = Sign::Negative;
} else {
return Err(Error::new(
ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
format!("Expected '+' or '-' for trailing plus but found '{input_char}'"),
));
}
input_idx += 1;
}
PicToken::TrailingMinus => {
if input_char == '-' {
sign = Sign::Negative;
} else if input_char == ' ' {
sign = Sign::Positive;
} else {
return Err(Error::new(
ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
format!("Expected '-' for trailing minus but found '{input_char}'"),
));
}
input_idx += 1;
}
PicToken::Credit => {
if input_idx + 1 >= input_chars.len() {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
"Input too short for CR symbol".to_string(),
));
}
let cr_str: String = input_chars[input_idx..input_idx + 2].iter().collect();
if cr_str == "CR" {
sign = Sign::Negative;
} else if cr_str == " " {
sign = Sign::Positive;
} else {
return Err(Error::new(
ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
format!("Expected 'CR' or spaces but found '{cr_str}'"),
));
}
input_idx += 2;
}
PicToken::Debit => {
if input_idx + 1 >= input_chars.len() {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
"Input too short for DB symbol".to_string(),
));
}
let db_str: String = input_chars[input_idx..input_idx + 2].iter().collect();
if db_str == "DB" {
sign = Sign::Negative;
} else if db_str == " " {
sign = Sign::Positive;
} else {
return Err(Error::new(
ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
format!("Expected 'DB' or spaces but found '{db_str}'"),
));
}
input_idx += 2;
}
}
pattern_idx += 1;
}
if input_idx != input_chars.len() {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!(
"Input longer than expected (pattern consumed {} chars, input has {} chars)",
input_idx,
input_chars.len()
),
));
}
let digits = digits.trim_start_matches('0');
let digits = if digits.is_empty() {
"0".to_string()
} else {
digits.to_string()
};
if digits == "0" {
sign = Sign::Positive;
}
Ok(NumericValue {
sign,
digits,
scale,
})
}
#[derive(Debug, Clone)]
struct ParsedNumeric {
sign: Sign,
digits: Vec<u8>,
decimal_places: usize,
}
fn parse_numeric_value(value: &str) -> Result<ParsedNumeric> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
"Empty numeric value",
));
}
let mut chars = trimmed.chars().peekable();
let sign = if chars.peek() == Some(&'-') {
chars.next();
Sign::Negative
} else if chars.peek() == Some(&'+') {
chars.next();
Sign::Positive
} else {
Sign::Positive
};
let mut digits = Vec::new();
let mut found_decimal = false;
let mut decimal_places = 0;
let mut found_digit = false;
for ch in chars {
if ch.is_ascii_digit() {
digits.push(ch as u8 - b'0');
if found_decimal {
decimal_places += 1;
}
found_digit = true;
} else if ch == '.' {
if found_decimal {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!("Multiple decimal points in value: {value}"),
));
}
found_decimal = true;
} else {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!("Invalid character '{ch}' in numeric value: {value}"),
));
}
}
if !found_digit {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!("No digits found in value: {value}"),
));
}
Ok(ParsedNumeric {
sign,
digits,
decimal_places,
})
}
#[inline]
#[allow(clippy::too_many_lines)]
pub fn encode_edited_numeric(
value: &str,
pattern: &[PicToken],
scale: u16,
_blank_when_zero: bool,
) -> Result<String> {
let parsed = parse_numeric_value(value)?;
let is_zero = parsed.digits.iter().all(|&d| d == 0);
let effective_sign = if is_zero { Sign::Positive } else { parsed.sign };
let mut has_decimal = false;
for token in pattern {
if *token == PicToken::DecimalPoint {
has_decimal = true;
}
}
let _pattern_decimal_places = if has_decimal {
let mut after_decimal = 0;
let mut found = false;
for token in pattern {
if *token == PicToken::DecimalPoint {
found = true;
} else if found
&& matches!(
token,
PicToken::Digit | PicToken::ZeroSuppress | PicToken::ZeroInsert
)
{
after_decimal += 1;
}
}
after_decimal
} else {
0
};
let scale = scale as usize;
let mut adjusted_digits = parsed.digits.clone();
if scale > parsed.decimal_places {
let to_add = scale - parsed.decimal_places;
adjusted_digits.extend(std::iter::repeat_n(0, to_add));
} else if scale < parsed.decimal_places {
let to_remove = parsed.decimal_places - scale;
for _ in 0..to_remove {
adjusted_digits.pop();
}
}
let decimal_places = scale;
let total_digits = adjusted_digits.len();
let int_digits = total_digits.saturating_sub(decimal_places);
let mut int_positions = 0;
let mut frac_positions = 0;
let mut after_decimal = false;
for token in pattern {
match token {
PicToken::Digit
| PicToken::ZeroSuppress
| PicToken::ZeroInsert
| PicToken::AsteriskFill => {
if after_decimal {
frac_positions += 1;
} else {
int_positions += 1;
}
}
PicToken::DecimalPoint => {
after_decimal = true;
}
_ => {}
}
}
if int_digits > int_positions || decimal_places > frac_positions {
return Err(Error::new(
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
format!(
"Value too long for pattern (pattern has {int_positions} integer positions, value has {int_digits} digits)"
),
));
}
let output_len: usize = pattern
.iter()
.map(|token| match token {
PicToken::Credit | PicToken::Debit => 2,
_ => 1,
})
.sum();
let mut result: Vec<char> = vec![' '; output_len];
let mut int_digit_idx = int_digits; let mut frac_digit_idx = decimal_places;
let mut char_pos = output_len;
for (token_idx, token) in pattern.iter().enumerate().rev() {
let token_width = match token {
PicToken::Credit | PicToken::Debit => 2,
_ => 1,
};
char_pos -= token_width;
match token {
PicToken::Digit => {
let digit = {
let is_after_decimal = pattern[..token_idx].contains(&PicToken::DecimalPoint);
if is_after_decimal && frac_digit_idx > 0 {
frac_digit_idx -= 1;
char::from_digit(
u32::from(adjusted_digits[int_digits + frac_digit_idx]),
10,
)
.unwrap_or('0')
} else if !is_after_decimal && int_digit_idx > 0 {
int_digit_idx -= 1;
char::from_digit(u32::from(adjusted_digits[int_digit_idx]), 10)
.unwrap_or('0')
} else {
'0'
}
};
result[char_pos] = digit;
}
PicToken::ZeroSuppress => {
let is_after_decimal = pattern[..token_idx].contains(&PicToken::DecimalPoint);
if is_after_decimal && frac_digit_idx > 0 {
frac_digit_idx -= 1;
let d = adjusted_digits[int_digits + frac_digit_idx];
result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
} else if !is_after_decimal && int_digit_idx > 0 {
int_digit_idx -= 1;
let d = adjusted_digits[int_digit_idx];
result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
} else {
result[char_pos] = ' ';
}
}
PicToken::ZeroInsert => {
let is_after_decimal = pattern[..token_idx].contains(&PicToken::DecimalPoint);
if is_after_decimal && frac_digit_idx > 0 {
frac_digit_idx -= 1;
let d = adjusted_digits[int_digits + frac_digit_idx];
result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
} else if !is_after_decimal && int_digit_idx > 0 {
int_digit_idx -= 1;
let d = adjusted_digits[int_digit_idx];
result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
} else {
result[char_pos] = '0';
}
}
PicToken::AsteriskFill => {
let is_after_decimal = pattern[..token_idx].contains(&PicToken::DecimalPoint);
if is_after_decimal && frac_digit_idx > 0 {
frac_digit_idx -= 1;
let d = adjusted_digits[int_digits + frac_digit_idx];
result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
} else if !is_after_decimal && int_digit_idx > 0 {
int_digit_idx -= 1;
let d = adjusted_digits[int_digit_idx];
result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
} else {
result[char_pos] = '*';
}
}
PicToken::DecimalPoint => {
result[char_pos] = '.';
}
PicToken::Comma => {
let has_significant_right = result[char_pos + 1..]
.iter()
.any(|&ch| ch != ' ' && ch != '0' && ch != ',' && ch != '.');
result[char_pos] = if !is_zero || has_significant_right {
','
} else {
' '
};
}
PicToken::Slash => {
result[char_pos] = '/';
}
PicToken::Currency => {
result[char_pos] = '$';
}
PicToken::LeadingPlus | PicToken::TrailingPlus => {
result[char_pos] = match effective_sign {
Sign::Positive => '+',
Sign::Negative => '-',
};
}
PicToken::LeadingMinus | PicToken::TrailingMinus => {
result[char_pos] = match effective_sign {
Sign::Positive => ' ',
Sign::Negative => '-',
};
}
PicToken::Credit => {
match effective_sign {
Sign::Positive => {
result[char_pos] = ' ';
result[char_pos + 1] = ' ';
}
Sign::Negative => {
result[char_pos] = 'C';
result[char_pos + 1] = 'R';
}
}
}
PicToken::Debit => {
match effective_sign {
Sign::Positive => {
result[char_pos] = ' ';
result[char_pos + 1] = ' ';
}
Sign::Negative => {
result[char_pos] = 'D';
result[char_pos + 1] = 'B';
}
}
}
PicToken::Space => {
result[char_pos] = ' ';
}
}
}
Ok(result.into_iter().collect())
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_tokenize_simple_z() {
let tokens = tokenize_edited_pic("ZZZ9").unwrap();
assert_eq!(
tokens,
vec![
PicToken::ZeroSuppress,
PicToken::ZeroSuppress,
PicToken::ZeroSuppress,
PicToken::Digit
]
);
}
#[test]
fn test_tokenize_with_decimal() {
let tokens = tokenize_edited_pic("ZZZ9.99").unwrap();
assert_eq!(
tokens,
vec![
PicToken::ZeroSuppress,
PicToken::ZeroSuppress,
PicToken::ZeroSuppress,
PicToken::Digit,
PicToken::DecimalPoint,
PicToken::Digit,
PicToken::Digit
]
);
}
#[test]
fn test_tokenize_currency() {
let tokens = tokenize_edited_pic("$ZZ,ZZZ.99").unwrap();
assert_eq!(tokens[0], PicToken::Currency);
assert!(tokens.contains(&PicToken::Comma));
assert!(tokens.contains(&PicToken::DecimalPoint));
}
#[test]
fn test_decode_simple() {
let pattern = tokenize_edited_pic("ZZZ9").unwrap();
let result = decode_edited_numeric(" 12", &pattern, 0, false).unwrap();
assert_eq!(result.sign, Sign::Positive);
assert_eq!(result.digits, "12");
assert_eq!(result.to_decimal_string(), "12");
}
#[test]
fn test_decode_with_decimal() {
let pattern = tokenize_edited_pic("ZZZ9.99").unwrap();
let result = decode_edited_numeric(" 12.34", &pattern, 2, false).unwrap();
assert_eq!(result.sign, Sign::Positive);
assert_eq!(result.digits, "1234");
assert_eq!(result.scale, 2);
assert_eq!(result.to_decimal_string(), "12.34");
}
#[test]
fn test_decode_blank_when_zero() {
let pattern = tokenize_edited_pic("ZZZ9").unwrap();
let result = decode_edited_numeric(" ", &pattern, 0, true).unwrap();
assert_eq!(result.to_decimal_string(), "0");
}
#[test]
fn test_decode_with_currency() {
let pattern = tokenize_edited_pic("$ZZZ.99").unwrap();
let result = decode_edited_numeric("$ 12.34", &pattern, 2, false).unwrap();
assert_eq!(result.to_decimal_string(), "12.34");
}
#[test]
fn test_decode_trailing_cr() {
let pattern = tokenize_edited_pic("ZZZ9CR").unwrap();
let result = decode_edited_numeric(" 12CR", &pattern, 0, false).unwrap();
assert_eq!(result.sign, Sign::Negative);
assert_eq!(result.to_decimal_string(), "-12");
}
#[test]
fn test_decode_trailing_db() {
let pattern = tokenize_edited_pic("ZZZ9DB").unwrap();
let result = decode_edited_numeric(" 12DB", &pattern, 0, false).unwrap();
assert_eq!(result.sign, Sign::Negative);
assert_eq!(result.to_decimal_string(), "-12");
}
#[test]
fn test_encode_basic_digits() {
let pattern = tokenize_edited_pic("9999").unwrap();
let result = encode_edited_numeric("1234", &pattern, 0, false).unwrap();
assert_eq!(result, "1234");
}
#[test]
fn test_encode_zero_with_zero_insert() {
let pattern = tokenize_edited_pic("9999").unwrap();
let result = encode_edited_numeric("0", &pattern, 0, false).unwrap();
assert_eq!(result, "0000");
}
#[test]
fn test_encode_zero_suppression() {
let pattern = tokenize_edited_pic("ZZZ9").unwrap();
let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
assert_eq!(result, " 123");
}
#[test]
fn test_encode_zero_suppression_zero() {
let pattern = tokenize_edited_pic("ZZZ9").unwrap();
let result = encode_edited_numeric("0", &pattern, 0, false).unwrap();
assert_eq!(result, " 0");
}
#[test]
fn test_encode_zero_suppression_single_digit() {
let pattern = tokenize_edited_pic("ZZZ9").unwrap();
let result = encode_edited_numeric("1", &pattern, 0, false).unwrap();
assert_eq!(result, " 1");
}
#[test]
fn test_encode_zero_insert() {
let pattern = tokenize_edited_pic("0009").unwrap();
let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
assert_eq!(result, "0123");
}
#[test]
fn test_encode_zero_insert_all_zeros() {
let pattern = tokenize_edited_pic("0009").unwrap();
let result = encode_edited_numeric("0", &pattern, 0, false).unwrap();
assert_eq!(result, "0000");
}
#[test]
fn test_encode_decimal_point() {
let pattern = tokenize_edited_pic("99.99").unwrap();
let result = encode_edited_numeric("12.34", &pattern, 2, false).unwrap();
assert_eq!(result, "12.34");
}
#[test]
fn test_encode_zero_decimal() {
let pattern = tokenize_edited_pic("99.99").unwrap();
let result = encode_edited_numeric("0.00", &pattern, 2, false).unwrap();
assert_eq!(result, "00.00");
}
#[test]
fn test_encode_leading_plus_positive() {
let pattern = tokenize_edited_pic("+999").unwrap();
let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
assert_eq!(result, "+123");
}
#[test]
fn test_encode_leading_plus_negative() {
let pattern = tokenize_edited_pic("+999").unwrap();
let result = encode_edited_numeric("-123", &pattern, 0, false).unwrap();
assert_eq!(result, "-123");
}
#[test]
fn test_encode_leading_minus_positive() {
let pattern = tokenize_edited_pic("-999").unwrap();
let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
assert_eq!(result, " 123");
}
#[test]
fn test_encode_leading_minus_negative() {
let pattern = tokenize_edited_pic("-999").unwrap();
let result = encode_edited_numeric("-123", &pattern, 0, false).unwrap();
assert_eq!(result, "-123");
}
#[test]
fn test_encode_leading_plus_with_decimal() {
let pattern = tokenize_edited_pic("+99.99").unwrap();
let result = encode_edited_numeric("12.34", &pattern, 2, false).unwrap();
assert_eq!(result, "+12.34");
}
#[test]
fn test_encode_leading_minus_with_decimal() {
let pattern = tokenize_edited_pic("-99.99").unwrap();
let result = encode_edited_numeric("-12.34", &pattern, 2, false).unwrap();
assert_eq!(result, "-12.34");
}
#[test]
fn test_encode_negative_zero_forces_positive() {
let pattern = tokenize_edited_pic("-999").unwrap();
let result = encode_edited_numeric("-0", &pattern, 0, false).unwrap();
assert_eq!(result, " 000");
}
#[test]
fn test_encode_value_too_long() {
let pattern = tokenize_edited_pic("999").unwrap();
let result = encode_edited_numeric("1234", &pattern, 0, false);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().code,
ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT
));
}
#[test]
fn test_encode_space_insertion_simple() {
let pattern = tokenize_edited_pic("999B999").unwrap();
let result = encode_edited_numeric("123456", &pattern, 0, false).unwrap();
assert_eq!(result, "123 456");
}
#[test]
fn test_encode_space_insertion_multiple() {
let pattern = tokenize_edited_pic("9B9B9").unwrap();
let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
assert_eq!(result, "1 2 3");
}
#[test]
fn test_encode_space_with_zero_suppress() {
let pattern = tokenize_edited_pic("ZZZB999").unwrap();
let result = encode_edited_numeric("123456", &pattern, 0, false).unwrap();
assert_eq!(result, "123 456");
}
#[test]
fn test_encode_space_with_decimal() {
let pattern = tokenize_edited_pic("999B999.99").unwrap();
let result = encode_edited_numeric("123456.78", &pattern, 2, false).unwrap();
assert_eq!(result, "123 456.78");
}
#[test]
fn test_encode_space_multiple_repetition() {
let pattern = tokenize_edited_pic("99B(3)99").unwrap();
let result = encode_edited_numeric("1234", &pattern, 0, false).unwrap();
assert_eq!(result, "12 34");
}
#[test]
fn test_encode_space_with_currency() {
let pattern = tokenize_edited_pic("$999B999.99").unwrap();
let result = encode_edited_numeric("123456.78", &pattern, 2, false).unwrap();
assert_eq!(result, "$123 456.78");
}
#[test]
fn test_encode_space_with_sign() {
let pattern = tokenize_edited_pic("+999B999").unwrap();
let result = encode_edited_numeric("123456", &pattern, 0, false).unwrap();
assert_eq!(result, "+123 456");
}
#[test]
fn test_encode_empty_value() {
let pattern = tokenize_edited_pic("999").unwrap();
let result = encode_edited_numeric("", &pattern, 0, false);
assert!(result.is_err());
}
#[test]
fn test_encode_invalid_character() {
let pattern = tokenize_edited_pic("999").unwrap();
let result = encode_edited_numeric("12a", &pattern, 0, false);
assert!(result.is_err());
}
}