1use copybook_core::{Error, ErrorCode, Result};
12use tracing::warn;
13
14#[derive(Debug, Clone, PartialEq)]
19pub enum PicToken {
20 Digit,
22 ZeroSuppress,
24 ZeroInsert,
26 AsteriskFill,
28 Space,
30 Comma,
32 Slash,
34 DecimalPoint,
36 Currency,
38 LeadingPlus,
40 LeadingMinus,
42 TrailingPlus,
44 TrailingMinus,
46 Credit,
48 Debit,
50}
51
52impl std::fmt::Display for PicToken {
53 #[inline]
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::Digit => write!(f, "9"),
57 Self::ZeroSuppress => write!(f, "Z"),
58 Self::ZeroInsert => write!(f, "0"),
59 Self::AsteriskFill => write!(f, "*"),
60 Self::Space => write!(f, "B"),
61 Self::Comma => write!(f, ","),
62 Self::Slash => write!(f, "/"),
63 Self::DecimalPoint => write!(f, "."),
64 Self::Currency => write!(f, "$"),
65 Self::LeadingPlus | Self::TrailingPlus => write!(f, "+"),
66 Self::LeadingMinus | Self::TrailingMinus => write!(f, "-"),
67 Self::Credit => write!(f, "CR"),
68 Self::Debit => write!(f, "DB"),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq)]
75pub enum Sign {
76 Positive,
78 Negative,
80}
81
82#[inline]
87#[allow(clippy::too_many_lines)]
88#[must_use = "Handle the Result or propagate the error"]
89pub fn tokenize_edited_pic(pic_str: &str) -> Result<Vec<PicToken>> {
90 let mut tokens = Vec::new();
91 let mut chars = pic_str.chars().peekable();
92 let mut found_decimal = false;
93
94 if chars.peek() == Some(&'S') || chars.peek() == Some(&'s') {
96 chars.next();
97 }
98
99 while let Some(ch) = chars.next() {
100 match ch.to_ascii_uppercase() {
101 '9' => {
102 let count = parse_repetition(&mut chars)?;
103 for _ in 0..count {
104 tokens.push(PicToken::Digit);
105 }
106 }
107 'Z' => {
108 let count = parse_repetition(&mut chars)?;
109 for _ in 0..count {
110 tokens.push(PicToken::ZeroSuppress);
111 }
112 }
113 '0' => {
114 let count = parse_repetition(&mut chars)?;
115 for _ in 0..count {
116 tokens.push(PicToken::ZeroInsert);
117 }
118 }
119 '*' => {
120 let count = parse_repetition(&mut chars)?;
121 for _ in 0..count {
122 tokens.push(PicToken::AsteriskFill);
123 }
124 }
125 'B' => {
126 let count = parse_repetition(&mut chars)?;
127 for _ in 0..count {
128 tokens.push(PicToken::Space);
129 }
130 }
131 ',' => tokens.push(PicToken::Comma),
132 '/' => tokens.push(PicToken::Slash),
133 '.' => {
134 if found_decimal {
135 return Err(Error::new(
136 ErrorCode::CBKP001_SYNTAX,
137 format!("Multiple decimal points in edited PIC: {pic_str}"),
138 ));
139 }
140 found_decimal = true;
141 tokens.push(PicToken::DecimalPoint);
142 }
143 '$' => tokens.push(PicToken::Currency),
144 '+' => {
145 if tokens.is_empty() {
147 tokens.push(PicToken::LeadingPlus);
148 } else {
149 tokens.push(PicToken::TrailingPlus);
150 }
151 }
152 '-' => {
153 if tokens.is_empty() {
155 tokens.push(PicToken::LeadingMinus);
156 } else {
157 tokens.push(PicToken::TrailingMinus);
158 }
159 }
160 'C' => {
161 if let Some(&next_ch) = chars.peek()
163 && (next_ch == 'R' || next_ch == 'r')
164 {
165 chars.next(); tokens.push(PicToken::Credit);
167 } else {
168 return Err(Error::new(
169 ErrorCode::CBKP001_SYNTAX,
170 format!("Invalid character 'C' in edited PIC: {pic_str}"),
171 ));
172 }
173 }
174 'D' => {
175 if let Some(&next_ch) = chars.peek()
177 && (next_ch == 'B' || next_ch == 'b')
178 {
179 chars.next(); tokens.push(PicToken::Debit);
181 } else {
182 return Err(Error::new(
183 ErrorCode::CBKP001_SYNTAX,
184 format!("Invalid character 'D' in edited PIC: {pic_str}"),
185 ));
186 }
187 }
188 _ => {
189 }
191 }
192 }
193
194 if tokens.is_empty() {
195 return Err(Error::new(
196 ErrorCode::CBKP001_SYNTAX,
197 format!("Empty or invalid edited PIC pattern: {pic_str}"),
198 ));
199 }
200
201 Ok(tokens)
202}
203
204fn parse_repetition<I>(chars: &mut std::iter::Peekable<I>) -> Result<usize>
206where
207 I: Iterator<Item = char>,
208{
209 if chars.peek() == Some(&'(') {
210 chars.next(); let mut count_str = String::new();
212 while let Some(&ch) = chars.peek() {
213 if ch == ')' {
214 chars.next(); break;
216 } else if ch.is_ascii_digit() {
217 count_str.push(ch);
218 chars.next();
219 } else {
220 return Err(Error::new(
221 ErrorCode::CBKP001_SYNTAX,
222 format!("Invalid repetition count: {count_str}"),
223 ));
224 }
225 }
226 count_str.parse::<usize>().map_err(|_| {
227 Error::new(
228 ErrorCode::CBKP001_SYNTAX,
229 format!("Invalid repetition count: {count_str}"),
230 )
231 })
232 } else {
233 Ok(1)
234 }
235}
236
237#[derive(Debug, Clone, PartialEq)]
242pub struct NumericValue {
243 pub sign: Sign,
245 pub digits: String,
247 pub scale: u16,
249}
250
251impl NumericValue {
252 #[must_use]
254 #[inline]
255 pub fn to_decimal_string(&self) -> String {
256 if self.digits.is_empty() || self.digits.chars().all(|c| c == '0') {
257 return "0".to_string();
258 }
259
260 let sign_prefix = match self.sign {
261 Sign::Positive => "",
262 Sign::Negative => "-",
263 };
264
265 if self.scale == 0 {
266 format!("{sign_prefix}{}", self.digits)
268 } else {
269 let scale = self.scale as usize;
271 let digits_len = self.digits.len();
272
273 if scale >= digits_len {
274 let zeros = "0".repeat(scale - digits_len);
276 format!("{sign_prefix}0.{zeros}{}", self.digits)
277 } else {
278 let (int_part, frac_part) = self.digits.split_at(digits_len - scale);
280 if int_part.is_empty() {
281 format!("{sign_prefix}0.{frac_part}")
282 } else {
283 format!("{sign_prefix}{int_part}.{frac_part}")
284 }
285 }
286 }
287 }
288}
289
290#[inline]
295#[allow(clippy::too_many_lines)]
296#[must_use = "Handle the Result or propagate the error"]
297pub fn decode_edited_numeric(
298 input: &str,
299 pattern: &[PicToken],
300 scale: u16,
301 blank_when_zero: bool,
302) -> Result<NumericValue> {
303 if blank_when_zero && input.chars().all(|c| c == ' ') {
305 warn!("CBKD423_EDITED_PIC_BLANK_WHEN_ZERO: Edited PIC field is blank, decoding as zero");
306 crate::lib_api::increment_warning_counter();
307 return Ok(NumericValue {
308 sign: Sign::Positive,
309 digits: "0".to_string(),
310 scale,
311 });
312 }
313
314 let input_chars: Vec<char> = input.chars().collect();
315 let mut pattern_idx = 0;
316 let mut input_idx = 0;
317 let mut digits = String::new();
318 let mut sign = Sign::Positive;
319 let mut found_non_zero = false;
320
321 while pattern_idx < pattern.len() {
323 let token = &pattern[pattern_idx];
324
325 if input_idx >= input_chars.len() {
326 return Err(Error::new(
327 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
328 format!(
329 "Input too short for edited PIC pattern (expected {} characters, got {})",
330 pattern.len(),
331 input.len()
332 ),
333 ));
334 }
335
336 let input_char = input_chars[input_idx];
337
338 match token {
339 PicToken::Digit
340 | PicToken::ZeroSuppress
341 | PicToken::ZeroInsert
342 | PicToken::AsteriskFill => {
343 if input_char.is_ascii_digit() {
345 let digit_val = input_char;
346 if digit_val != '0' {
347 found_non_zero = true;
348 }
349 if found_non_zero || matches!(token, PicToken::Digit | PicToken::ZeroInsert) {
350 digits.push(digit_val);
351 } else {
352 digits.push('0');
354 }
355 } else if input_char == ' ' {
356 if matches!(token, PicToken::Digit) {
358 return Err(Error::new(
360 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
361 format!("Expected digit but found space at position {input_idx}"),
362 ));
363 }
364 digits.push('0');
365 } else if input_char == '*' {
366 if !matches!(token, PicToken::AsteriskFill) {
368 return Err(Error::new(
369 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
370 format!("Unexpected asterisk at position {input_idx}"),
371 ));
372 }
373 digits.push('0');
374 } else {
375 return Err(Error::new(
376 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
377 format!(
378 "Expected digit, space, or asterisk but found '{input_char}' at position {input_idx}"
379 ),
380 ));
381 }
382 input_idx += 1;
383 }
384 PicToken::Space => {
385 if input_char != ' ' && input_char != 'B' {
386 return Err(Error::new(
387 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
388 format!("Expected space but found '{input_char}' at position {input_idx}"),
389 ));
390 }
391 input_idx += 1;
392 }
393 PicToken::Comma => {
394 if input_char != ',' {
395 return Err(Error::new(
396 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
397 format!("Expected comma but found '{input_char}' at position {input_idx}"),
398 ));
399 }
400 input_idx += 1;
401 }
402 PicToken::Slash => {
403 if input_char != '/' {
404 return Err(Error::new(
405 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
406 format!("Expected slash but found '{input_char}' at position {input_idx}"),
407 ));
408 }
409 input_idx += 1;
410 }
411 PicToken::DecimalPoint => {
412 if input_char != '.' {
413 return Err(Error::new(
414 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
415 format!(
416 "Expected decimal point but found '{input_char}' at position {input_idx}"
417 ),
418 ));
419 }
420 input_idx += 1;
421 }
422 PicToken::Currency => {
423 if input_char != '$' && input_char != ' ' {
424 return Err(Error::new(
425 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
426 format!(
427 "Expected currency symbol but found '{input_char}' at position {input_idx}"
428 ),
429 ));
430 }
431 input_idx += 1;
432 }
433 PicToken::LeadingPlus => {
434 if input_char == '+' || input_char == ' ' {
435 sign = Sign::Positive;
436 } else if input_char == '-' {
437 sign = Sign::Negative;
438 } else {
439 return Err(Error::new(
440 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
441 format!("Expected '+' or '-' for leading plus but found '{input_char}'"),
442 ));
443 }
444 input_idx += 1;
445 }
446 PicToken::LeadingMinus => {
447 if input_char == '-' {
448 sign = Sign::Negative;
449 } else if input_char == ' ' {
450 sign = Sign::Positive;
451 } else {
452 return Err(Error::new(
453 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
454 format!("Expected '-' for leading minus but found '{input_char}'"),
455 ));
456 }
457 input_idx += 1;
458 }
459 PicToken::TrailingPlus => {
460 if input_char == '+' || input_char == ' ' {
461 sign = Sign::Positive;
462 } else if input_char == '-' {
463 sign = Sign::Negative;
464 } else {
465 return Err(Error::new(
466 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
467 format!("Expected '+' or '-' for trailing plus but found '{input_char}'"),
468 ));
469 }
470 input_idx += 1;
471 }
472 PicToken::TrailingMinus => {
473 if input_char == '-' {
474 sign = Sign::Negative;
475 } else if input_char == ' ' {
476 sign = Sign::Positive;
477 } else {
478 return Err(Error::new(
479 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
480 format!("Expected '-' for trailing minus but found '{input_char}'"),
481 ));
482 }
483 input_idx += 1;
484 }
485 PicToken::Credit => {
486 if input_idx + 1 >= input_chars.len() {
488 return Err(Error::new(
489 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
490 "Input too short for CR symbol".to_string(),
491 ));
492 }
493 let cr_str: String = input_chars[input_idx..input_idx + 2].iter().collect();
494 if cr_str == "CR" {
495 sign = Sign::Negative;
496 } else if cr_str == " " {
497 sign = Sign::Positive;
498 } else {
499 return Err(Error::new(
500 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
501 format!("Expected 'CR' or spaces but found '{cr_str}'"),
502 ));
503 }
504 input_idx += 2;
505 }
506 PicToken::Debit => {
507 if input_idx + 1 >= input_chars.len() {
509 return Err(Error::new(
510 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
511 "Input too short for DB symbol".to_string(),
512 ));
513 }
514 let db_str: String = input_chars[input_idx..input_idx + 2].iter().collect();
515 if db_str == "DB" {
516 sign = Sign::Negative;
517 } else if db_str == " " {
518 sign = Sign::Positive;
519 } else {
520 return Err(Error::new(
521 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
522 format!("Expected 'DB' or spaces but found '{db_str}'"),
523 ));
524 }
525 input_idx += 2;
526 }
527 }
528
529 pattern_idx += 1;
530 }
531
532 if input_idx != input_chars.len() {
534 return Err(Error::new(
535 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
536 format!(
537 "Input longer than expected (pattern consumed {} chars, input has {} chars)",
538 input_idx,
539 input_chars.len()
540 ),
541 ));
542 }
543
544 let digits = digits.trim_start_matches('0');
546 let digits = if digits.is_empty() {
547 "0".to_string()
548 } else {
549 digits.to_string()
550 };
551
552 if digits == "0" {
554 sign = Sign::Positive;
555 }
556
557 Ok(NumericValue {
558 sign,
559 digits,
560 scale,
561 })
562}
563
564#[derive(Debug, Clone)]
566struct ParsedNumeric {
567 sign: Sign,
569 digits: Vec<u8>,
571 decimal_places: usize,
573}
574
575fn parse_numeric_value(value: &str) -> Result<ParsedNumeric> {
577 let trimmed = value.trim();
578 if trimmed.is_empty() {
579 return Err(Error::new(
580 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
581 "Empty numeric value",
582 ));
583 }
584
585 let mut chars = trimmed.chars().peekable();
586 let sign = if chars.peek() == Some(&'-') {
587 chars.next();
588 Sign::Negative
589 } else if chars.peek() == Some(&'+') {
590 chars.next();
591 Sign::Positive
592 } else {
593 Sign::Positive
594 };
595
596 let mut digits = Vec::new();
597 let mut found_decimal = false;
598 let mut decimal_places = 0;
599 let mut found_digit = false;
600
601 for ch in chars {
602 if ch.is_ascii_digit() {
603 digits.push(ch as u8 - b'0');
604 if found_decimal {
605 decimal_places += 1;
606 }
607 found_digit = true;
608 } else if ch == '.' {
609 if found_decimal {
610 return Err(Error::new(
611 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
612 format!("Multiple decimal points in value: {value}"),
613 ));
614 }
615 found_decimal = true;
616 } else {
617 return Err(Error::new(
618 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
619 format!("Invalid character '{ch}' in numeric value: {value}"),
620 ));
621 }
622 }
623
624 if !found_digit {
625 return Err(Error::new(
626 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
627 format!("No digits found in value: {value}"),
628 ));
629 }
630
631 Ok(ParsedNumeric {
632 sign,
633 digits,
634 decimal_places,
635 })
636}
637
638#[inline]
643#[allow(clippy::too_many_lines)]
644#[must_use = "Handle the Result or propagate the error"]
645pub fn encode_edited_numeric(
646 value: &str,
647 pattern: &[PicToken],
648 scale: u16,
649 _blank_when_zero: bool,
650) -> Result<String> {
651 let parsed = parse_numeric_value(value)?;
653
654 let is_zero = parsed.digits.iter().all(|&d| d == 0);
659 let effective_sign = if is_zero { Sign::Positive } else { parsed.sign };
660
661 let mut has_decimal = false;
663 for token in pattern {
664 if *token == PicToken::DecimalPoint {
665 has_decimal = true;
666 }
667 }
668
669 let _pattern_decimal_places = if has_decimal {
671 let mut after_decimal = 0;
673 let mut found = false;
674 for token in pattern {
675 if *token == PicToken::DecimalPoint {
676 found = true;
677 } else if found
678 && matches!(
679 token,
680 PicToken::Digit | PicToken::ZeroSuppress | PicToken::ZeroInsert
681 )
682 {
683 after_decimal += 1;
684 }
685 }
686 after_decimal
687 } else {
688 0
689 };
690
691 let scale = scale as usize;
693 let mut adjusted_digits = parsed.digits.clone();
694
695 if scale > parsed.decimal_places {
697 let to_add = scale - parsed.decimal_places;
699 adjusted_digits.extend(std::iter::repeat_n(0, to_add));
700 } else if scale < parsed.decimal_places {
701 let to_remove = parsed.decimal_places - scale;
703 for _ in 0..to_remove {
704 adjusted_digits.pop();
705 }
706 }
707
708 let decimal_places = scale;
710 let total_digits = adjusted_digits.len();
711 let int_digits = total_digits.saturating_sub(decimal_places);
712
713 let mut int_positions = 0;
715 let mut frac_positions = 0;
716 let mut after_decimal = false;
717 for token in pattern {
718 match token {
719 PicToken::Digit
720 | PicToken::ZeroSuppress
721 | PicToken::ZeroInsert
722 | PicToken::AsteriskFill => {
723 if after_decimal {
724 frac_positions += 1;
725 } else {
726 int_positions += 1;
727 }
728 }
729 PicToken::DecimalPoint => {
730 after_decimal = true;
731 }
732 _ => {}
733 }
734 }
735
736 if int_digits > int_positions || decimal_places > frac_positions {
738 return Err(Error::new(
739 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
740 format!(
741 "Value too long for pattern (pattern has {int_positions} integer positions, value has {int_digits} digits)"
742 ),
743 ));
744 }
745
746 let output_len: usize = pattern
748 .iter()
749 .map(|token| match token {
750 PicToken::Credit | PicToken::Debit => 2,
751 _ => 1,
752 })
753 .sum();
754
755 let mut result: Vec<char> = vec![' '; output_len];
757 let mut int_digit_idx = int_digits; let mut frac_digit_idx = decimal_places; let mut char_pos = output_len;
763 for (token_idx, token) in pattern.iter().enumerate().rev() {
764 let token_width = match token {
766 PicToken::Credit | PicToken::Debit => 2,
767 _ => 1,
768 };
769 char_pos -= token_width;
770
771 match token {
772 PicToken::Digit => {
773 let digit = {
774 let is_after_decimal = pattern[..token_idx].contains(&PicToken::DecimalPoint);
776 if is_after_decimal && frac_digit_idx > 0 {
777 frac_digit_idx -= 1;
778 char::from_digit(
779 u32::from(adjusted_digits[int_digits + frac_digit_idx]),
780 10,
781 )
782 .unwrap_or('0')
783 } else if !is_after_decimal && int_digit_idx > 0 {
784 int_digit_idx -= 1;
785 char::from_digit(u32::from(adjusted_digits[int_digit_idx]), 10)
786 .unwrap_or('0')
787 } else {
788 '0'
789 }
790 };
791 result[char_pos] = digit;
792 }
793 PicToken::ZeroSuppress => {
794 let is_after_decimal = pattern[..token_idx].contains(&PicToken::DecimalPoint);
795 if is_after_decimal && frac_digit_idx > 0 {
796 frac_digit_idx -= 1;
797 let d = adjusted_digits[int_digits + frac_digit_idx];
798 result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
799 } else if !is_after_decimal && int_digit_idx > 0 {
800 int_digit_idx -= 1;
801 let d = adjusted_digits[int_digit_idx];
802 result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
803 } else {
804 result[char_pos] = ' ';
805 }
806 }
807 PicToken::ZeroInsert => {
808 let is_after_decimal = pattern[..token_idx].contains(&PicToken::DecimalPoint);
809 if is_after_decimal && frac_digit_idx > 0 {
810 frac_digit_idx -= 1;
811 let d = adjusted_digits[int_digits + frac_digit_idx];
812 result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
813 } else if !is_after_decimal && int_digit_idx > 0 {
814 int_digit_idx -= 1;
815 let d = adjusted_digits[int_digit_idx];
816 result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
817 } else {
818 result[char_pos] = '0';
819 }
820 }
821 PicToken::AsteriskFill => {
822 let is_after_decimal = pattern[..token_idx].contains(&PicToken::DecimalPoint);
823 if is_after_decimal && frac_digit_idx > 0 {
824 frac_digit_idx -= 1;
825 let d = adjusted_digits[int_digits + frac_digit_idx];
826 result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
827 } else if !is_after_decimal && int_digit_idx > 0 {
828 int_digit_idx -= 1;
829 let d = adjusted_digits[int_digit_idx];
830 result[char_pos] = char::from_digit(u32::from(d), 10).unwrap_or('0');
831 } else {
832 result[char_pos] = '*';
833 }
834 }
835 PicToken::DecimalPoint => {
836 result[char_pos] = '.';
837 }
838 PicToken::Comma => {
839 let has_significant_right = result[char_pos + 1..]
843 .iter()
844 .any(|&ch| ch != ' ' && ch != '0' && ch != ',' && ch != '.');
845 result[char_pos] = if !is_zero || has_significant_right {
846 ','
847 } else {
848 ' '
849 };
850 }
851 PicToken::Slash => {
852 result[char_pos] = '/';
854 }
855 PicToken::Currency => {
856 result[char_pos] = '$';
858 }
859 PicToken::LeadingPlus | PicToken::TrailingPlus => {
860 result[char_pos] = match effective_sign {
861 Sign::Positive => '+',
862 Sign::Negative => '-',
863 };
864 }
865 PicToken::LeadingMinus | PicToken::TrailingMinus => {
866 result[char_pos] = match effective_sign {
867 Sign::Positive => ' ',
868 Sign::Negative => '-',
869 };
870 }
871 PicToken::Credit => {
872 match effective_sign {
874 Sign::Positive => {
875 result[char_pos] = ' ';
876 result[char_pos + 1] = ' ';
877 }
878 Sign::Negative => {
879 result[char_pos] = 'C';
880 result[char_pos + 1] = 'R';
881 }
882 }
883 }
884 PicToken::Debit => {
885 match effective_sign {
887 Sign::Positive => {
888 result[char_pos] = ' ';
889 result[char_pos + 1] = ' ';
890 }
891 Sign::Negative => {
892 result[char_pos] = 'D';
893 result[char_pos + 1] = 'B';
894 }
895 }
896 }
897 PicToken::Space => {
898 result[char_pos] = ' ';
900 }
901 }
902 }
903
904 Ok(result.into_iter().collect())
905}
906
907#[cfg(test)]
908#[allow(clippy::expect_used, clippy::unwrap_used)]
909mod tests {
910 use super::*;
911
912 #[test]
913 fn test_tokenize_simple_z() {
914 let tokens = tokenize_edited_pic("ZZZ9").unwrap();
915 assert_eq!(
916 tokens,
917 vec![
918 PicToken::ZeroSuppress,
919 PicToken::ZeroSuppress,
920 PicToken::ZeroSuppress,
921 PicToken::Digit
922 ]
923 );
924 }
925
926 #[test]
927 fn test_tokenize_with_decimal() {
928 let tokens = tokenize_edited_pic("ZZZ9.99").unwrap();
929 assert_eq!(
930 tokens,
931 vec![
932 PicToken::ZeroSuppress,
933 PicToken::ZeroSuppress,
934 PicToken::ZeroSuppress,
935 PicToken::Digit,
936 PicToken::DecimalPoint,
937 PicToken::Digit,
938 PicToken::Digit
939 ]
940 );
941 }
942
943 #[test]
944 fn test_tokenize_currency() {
945 let tokens = tokenize_edited_pic("$ZZ,ZZZ.99").unwrap();
946 assert_eq!(tokens[0], PicToken::Currency);
947 assert!(tokens.contains(&PicToken::Comma));
948 assert!(tokens.contains(&PicToken::DecimalPoint));
949 }
950
951 #[test]
952 fn test_decode_simple() {
953 let pattern = tokenize_edited_pic("ZZZ9").unwrap();
954 let result = decode_edited_numeric(" 12", &pattern, 0, false).unwrap();
955 assert_eq!(result.sign, Sign::Positive);
956 assert_eq!(result.digits, "12");
957 assert_eq!(result.to_decimal_string(), "12");
958 }
959
960 #[test]
961 fn test_decode_with_decimal() {
962 let pattern = tokenize_edited_pic("ZZZ9.99").unwrap();
963 let result = decode_edited_numeric(" 12.34", &pattern, 2, false).unwrap();
964 assert_eq!(result.sign, Sign::Positive);
965 assert_eq!(result.digits, "1234");
966 assert_eq!(result.scale, 2);
967 assert_eq!(result.to_decimal_string(), "12.34");
968 }
969
970 #[test]
971 fn test_decode_blank_when_zero() {
972 let pattern = tokenize_edited_pic("ZZZ9").unwrap();
973 let result = decode_edited_numeric(" ", &pattern, 0, true).unwrap();
974 assert_eq!(result.to_decimal_string(), "0");
975 }
976
977 #[test]
978 fn test_decode_with_currency() {
979 let pattern = tokenize_edited_pic("$ZZZ.99").unwrap();
980 let result = decode_edited_numeric("$ 12.34", &pattern, 2, false).unwrap();
981 assert_eq!(result.to_decimal_string(), "12.34");
982 }
983
984 #[test]
985 fn test_decode_trailing_cr() {
986 let pattern = tokenize_edited_pic("ZZZ9CR").unwrap();
987 let result = decode_edited_numeric(" 12CR", &pattern, 0, false).unwrap();
988 assert_eq!(result.sign, Sign::Negative);
989 assert_eq!(result.to_decimal_string(), "-12");
990 }
991
992 #[test]
993 fn test_decode_trailing_db() {
994 let pattern = tokenize_edited_pic("ZZZ9DB").unwrap();
995 let result = decode_edited_numeric(" 12DB", &pattern, 0, false).unwrap();
996 assert_eq!(result.sign, Sign::Negative);
997 assert_eq!(result.to_decimal_string(), "-12");
998 }
999
1000 #[test]
1003 fn test_encode_basic_digits() {
1004 let pattern = tokenize_edited_pic("9999").unwrap();
1005 let result = encode_edited_numeric("1234", &pattern, 0, false).unwrap();
1006 assert_eq!(result, "1234");
1007 }
1008
1009 #[test]
1010 fn test_encode_zero_with_zero_insert() {
1011 let pattern = tokenize_edited_pic("9999").unwrap();
1012 let result = encode_edited_numeric("0", &pattern, 0, false).unwrap();
1013 assert_eq!(result, "0000");
1014 }
1015
1016 #[test]
1017 fn test_encode_zero_suppression() {
1018 let pattern = tokenize_edited_pic("ZZZ9").unwrap();
1019 let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
1020 assert_eq!(result, " 123");
1021 }
1022
1023 #[test]
1024 fn test_encode_zero_suppression_zero() {
1025 let pattern = tokenize_edited_pic("ZZZ9").unwrap();
1026 let result = encode_edited_numeric("0", &pattern, 0, false).unwrap();
1027 assert_eq!(result, " 0");
1028 }
1029
1030 #[test]
1031 fn test_encode_zero_suppression_single_digit() {
1032 let pattern = tokenize_edited_pic("ZZZ9").unwrap();
1033 let result = encode_edited_numeric("1", &pattern, 0, false).unwrap();
1034 assert_eq!(result, " 1");
1035 }
1036
1037 #[test]
1038 fn test_encode_zero_insert() {
1039 let pattern = tokenize_edited_pic("0009").unwrap();
1040 let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
1041 assert_eq!(result, "0123");
1042 }
1043
1044 #[test]
1045 fn test_encode_zero_insert_all_zeros() {
1046 let pattern = tokenize_edited_pic("0009").unwrap();
1047 let result = encode_edited_numeric("0", &pattern, 0, false).unwrap();
1048 assert_eq!(result, "0000");
1049 }
1050
1051 #[test]
1052 fn test_encode_decimal_point() {
1053 let pattern = tokenize_edited_pic("99.99").unwrap();
1054 let result = encode_edited_numeric("12.34", &pattern, 2, false).unwrap();
1055 assert_eq!(result, "12.34");
1056 }
1057
1058 #[test]
1059 fn test_encode_zero_decimal() {
1060 let pattern = tokenize_edited_pic("99.99").unwrap();
1061 let result = encode_edited_numeric("0.00", &pattern, 2, false).unwrap();
1062 assert_eq!(result, "00.00");
1063 }
1064
1065 #[test]
1066 fn test_encode_leading_plus_positive() {
1067 let pattern = tokenize_edited_pic("+999").unwrap();
1068 let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
1069 assert_eq!(result, "+123");
1070 }
1071
1072 #[test]
1073 fn test_encode_leading_plus_negative() {
1074 let pattern = tokenize_edited_pic("+999").unwrap();
1075 let result = encode_edited_numeric("-123", &pattern, 0, false).unwrap();
1076 assert_eq!(result, "-123");
1077 }
1078
1079 #[test]
1080 fn test_encode_leading_minus_positive() {
1081 let pattern = tokenize_edited_pic("-999").unwrap();
1082 let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
1083 assert_eq!(result, " 123");
1084 }
1085
1086 #[test]
1087 fn test_encode_leading_minus_negative() {
1088 let pattern = tokenize_edited_pic("-999").unwrap();
1089 let result = encode_edited_numeric("-123", &pattern, 0, false).unwrap();
1090 assert_eq!(result, "-123");
1091 }
1092
1093 #[test]
1094 fn test_encode_leading_plus_with_decimal() {
1095 let pattern = tokenize_edited_pic("+99.99").unwrap();
1096 let result = encode_edited_numeric("12.34", &pattern, 2, false).unwrap();
1097 assert_eq!(result, "+12.34");
1098 }
1099
1100 #[test]
1101 fn test_encode_leading_minus_with_decimal() {
1102 let pattern = tokenize_edited_pic("-99.99").unwrap();
1103 let result = encode_edited_numeric("-12.34", &pattern, 2, false).unwrap();
1104 assert_eq!(result, "-12.34");
1105 }
1106
1107 #[test]
1108 fn test_encode_negative_zero_forces_positive() {
1109 let pattern = tokenize_edited_pic("-999").unwrap();
1110 let result = encode_edited_numeric("-0", &pattern, 0, false).unwrap();
1111 assert_eq!(result, " 000");
1112 }
1113
1114 #[test]
1115 fn test_encode_value_too_long() {
1116 let pattern = tokenize_edited_pic("999").unwrap();
1117 let result = encode_edited_numeric("1234", &pattern, 0, false);
1118 assert!(result.is_err());
1119 assert!(matches!(
1120 result.unwrap_err().code,
1121 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT
1122 ));
1123 }
1124
1125 #[test]
1128 fn test_encode_space_insertion_simple() {
1129 let pattern = tokenize_edited_pic("999B999").unwrap();
1130 let result = encode_edited_numeric("123456", &pattern, 0, false).unwrap();
1131 assert_eq!(result, "123 456");
1132 }
1133
1134 #[test]
1135 fn test_encode_space_insertion_multiple() {
1136 let pattern = tokenize_edited_pic("9B9B9").unwrap();
1137 let result = encode_edited_numeric("123", &pattern, 0, false).unwrap();
1138 assert_eq!(result, "1 2 3");
1139 }
1140
1141 #[test]
1142 fn test_encode_space_with_zero_suppress() {
1143 let pattern = tokenize_edited_pic("ZZZB999").unwrap();
1144 let result = encode_edited_numeric("123456", &pattern, 0, false).unwrap();
1145 assert_eq!(result, "123 456");
1146 }
1147
1148 #[test]
1149 fn test_encode_space_with_decimal() {
1150 let pattern = tokenize_edited_pic("999B999.99").unwrap();
1151 let result = encode_edited_numeric("123456.78", &pattern, 2, false).unwrap();
1152 assert_eq!(result, "123 456.78");
1153 }
1154
1155 #[test]
1156 fn test_encode_space_multiple_repetition() {
1157 let pattern = tokenize_edited_pic("99B(3)99").unwrap();
1158 let result = encode_edited_numeric("1234", &pattern, 0, false).unwrap();
1159 assert_eq!(result, "12 34");
1160 }
1161
1162 #[test]
1163 fn test_encode_space_with_currency() {
1164 let pattern = tokenize_edited_pic("$999B999.99").unwrap();
1165 let result = encode_edited_numeric("123456.78", &pattern, 2, false).unwrap();
1166 assert_eq!(result, "$123 456.78");
1167 }
1168
1169 #[test]
1170 fn test_encode_space_with_sign() {
1171 let pattern = tokenize_edited_pic("+999B999").unwrap();
1172 let result = encode_edited_numeric("123456", &pattern, 0, false).unwrap();
1173 assert_eq!(result, "+123 456");
1174 }
1175
1176 #[test]
1177 fn test_encode_empty_value() {
1178 let pattern = tokenize_edited_pic("999").unwrap();
1179 let result = encode_edited_numeric("", &pattern, 0, false);
1180 assert!(result.is_err());
1181 }
1182
1183 #[test]
1184 fn test_encode_invalid_character() {
1185 let pattern = tokenize_edited_pic("999").unwrap();
1186 let result = encode_edited_numeric("12a", &pattern, 0, false);
1187 assert!(result.is_err());
1188 }
1189}