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