Skip to main content

copybook_codec/
edited_pic.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Edited PIC (Phase E2 + E3) decode and encode support
3//!
4//! This module implements decode and encode for edited numeric PICTURE clauses following IBM COBOL specifications.
5//! Edited PICs include formatting symbols like Z (zero suppression), $ (currency), comma, decimal point,
6//! B (blank space insertion), and sign editing (+, -, CR, DB).
7//!
8//! The decode algorithm walks the input string and PIC pattern in lockstep, extracting numeric digits
9//! and validating formatting symbols. The encode algorithm formats numeric values according to the pattern.
10
11use copybook_core::{Error, ErrorCode, Result};
12use tracing::warn;
13
14/// Pattern tokens for edited PIC clauses.
15///
16/// Each variant represents a formatting symbol in an edited PICTURE clause,
17/// used during decode and encode of edited numeric fields.
18#[derive(Debug, Clone, PartialEq)]
19pub enum PicToken {
20    /// Numeric digit (9) - always displays
21    Digit,
22    /// Zero suppression (Z) - displays space if leading zero
23    ZeroSuppress,
24    /// Zero insert (0) - always displays '0'
25    ZeroInsert,
26    /// Asterisk fill (*) - displays '*' for leading zeros
27    AsteriskFill,
28    /// Blank space (B)
29    Space,
30    /// Literal comma
31    Comma,
32    /// Literal slash
33    Slash,
34    /// Decimal point
35    DecimalPoint,
36    /// Currency symbol ($)
37    Currency,
38    /// Leading plus sign
39    LeadingPlus,
40    /// Leading minus sign
41    LeadingMinus,
42    /// Trailing plus sign
43    TrailingPlus,
44    /// Trailing minus sign
45    TrailingMinus,
46    /// Credit (CR) - two characters
47    Credit,
48    /// Debit (DB) - two characters
49    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/// Sign extracted from an edited PIC field during decode.
74#[derive(Debug, Clone, Copy, PartialEq)]
75pub enum Sign {
76    /// Positive or unsigned value.
77    Positive,
78    /// Negative value.
79    Negative,
80}
81
82/// Tokenize an edited PIC pattern into tokens
83///
84/// # Errors
85/// Returns error if the PIC pattern is malformed
86#[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    // Skip leading 'S' if present (sign is handled separately via sign editing symbols)
94    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                // Check if at beginning (leading) or end (trailing)
145                if tokens.is_empty() {
146                    tokens.push(PicToken::LeadingPlus);
147                } else {
148                    tokens.push(PicToken::TrailingPlus);
149                }
150            }
151            '-' => {
152                // Check if at beginning (leading) or end (trailing)
153                if tokens.is_empty() {
154                    tokens.push(PicToken::LeadingMinus);
155                } else {
156                    tokens.push(PicToken::TrailingMinus);
157                }
158            }
159            'C' => {
160                // Check for CR
161                if let Some(&next_ch) = chars.peek()
162                    && (next_ch == 'R' || next_ch == 'r')
163                {
164                    chars.next(); // consume 'R'
165                    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                // Check for DB
175                if let Some(&next_ch) = chars.peek()
176                    && (next_ch == 'B' || next_ch == 'b')
177                {
178                    chars.next(); // consume 'B'
179                    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                // Skip implicit decimal point markers (V), whitespace, or unknown characters
189            }
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
203/// Parse repetition count from chars like (5)
204fn 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(); // consume '('
210        let mut count_str = String::new();
211        while let Some(&ch) = chars.peek() {
212            if ch == ')' {
213                chars.next(); // consume ')'
214                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/// Decoded numeric value extracted from an edited PIC field.
237///
238/// Contains the sign, raw digit string, and scale needed to
239/// produce a JSON numeric representation.
240#[derive(Debug, Clone, PartialEq)]
241pub struct NumericValue {
242    /// Sign of the number
243    pub sign: Sign,
244    /// Digits without decimal point (e.g., "12345" for 123.45 with scale=2)
245    pub digits: String,
246    /// Number of decimal places
247    pub scale: u16,
248}
249
250impl NumericValue {
251    /// Format as decimal string for JSON output
252    #[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            // Integer - just return digits with sign
266            format!("{sign_prefix}{}", self.digits)
267        } else {
268            // Decimal - insert decimal point
269            let scale = self.scale as usize;
270            let digits_len = self.digits.len();
271
272            if scale >= digits_len {
273                // Need leading zeros (e.g., 0.0123)
274                let zeros = "0".repeat(scale - digits_len);
275                format!("{sign_prefix}0.{zeros}{}", self.digits)
276            } else {
277                // Split at decimal point
278                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/// Decode edited numeric string according to PICTURE pattern
290///
291/// # Errors
292/// Returns error if the input doesn't match the pattern
293#[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    // Check for BLANK WHEN ZERO
302    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    // Extract sign from pattern and input
320    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                // Expect digit or space or asterisk
342                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                        // Leading zero suppression - push 0 to maintain position
351                        digits.push('0');
352                    }
353                } else if input_char == ' ' {
354                    // Space for zero suppression
355                    if matches!(token, PicToken::Digit) {
356                        // Required digit position cannot be space
357                        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                    // Asterisk fill for check protection
365                    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                // CR requires two characters
485                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                // DB requires two characters
506                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    // Check if we consumed all input
531    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    // Clean up digits - remove leading zeros but preserve at least one digit
543    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 all zeros, force positive sign
551    if digits == "0" {
552        sign = Sign::Positive;
553    }
554
555    Ok(NumericValue {
556        sign,
557        digits,
558        scale,
559    })
560}
561
562/// Parsed numeric value for encoding
563#[derive(Debug, Clone)]
564struct ParsedNumeric {
565    /// Sign of the number
566    sign: Sign,
567    /// All digits without decimal point (e.g., "12345" for 123.45)
568    digits: Vec<u8>,
569    /// Position of decimal point from right (0 for integers, 2 for 2 decimal places)
570    decimal_places: usize,
571}
572
573/// Parse a numeric string into its components for encoding
574fn 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/// Encode a numeric value to an edited PIC string
637///
638/// # Errors
639/// Returns error if the value cannot be encoded to the pattern
640#[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    // Parse the input value
649    let parsed = parse_numeric_value(value)?;
650
651    // All tokens are now supported in E3.7 (including Space)
652    // No unsupported token check needed
653
654    // Check if value is all zeros (force positive sign)
655    let is_zero = parsed.digits.iter().all(|&d| d == 0);
656    let effective_sign = if is_zero { Sign::Positive } else { parsed.sign };
657
658    // Count numeric positions and decimal point in pattern
659    let mut has_decimal = false;
660    for token in pattern {
661        if *token == PicToken::DecimalPoint {
662            has_decimal = true;
663        }
664    }
665
666    // Calculate expected decimal places from pattern
667    let _pattern_decimal_places = if has_decimal {
668        // Count numeric positions after decimal point
669        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    // Adjust digits to match pattern scale
689    let scale = scale as usize;
690    let mut adjusted_digits = parsed.digits.clone();
691
692    // Pad or truncate to match scale
693    if scale > parsed.decimal_places {
694        // Need to add trailing zeros
695        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        // Need to truncate (round down for now)
699        let to_remove = parsed.decimal_places - scale;
700        for _ in 0..to_remove {
701            adjusted_digits.pop();
702        }
703    }
704
705    // Calculate integer and fractional parts
706    let decimal_places = scale;
707    let total_digits = adjusted_digits.len();
708    let int_digits = total_digits.saturating_sub(decimal_places);
709
710    // Count integer and fractional positions in pattern
711    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    // Check if value fits in pattern
734    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    // Calculate output length (CR and DB are 2 characters each, others are 1)
744    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    // Build output string by filling from right to left
753    let mut result: Vec<char> = vec![' '; output_len];
754    let mut int_digit_idx = int_digits; // Start from the end
755    let mut frac_digit_idx = decimal_places; // Start from the end
756
757    // Fill from right to left
758    // We need to track character position separately from token index
759    let mut char_pos = output_len;
760    for (token_idx, token) in pattern.iter().enumerate().rev() {
761        // Determine how many characters this token occupies
762        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                    // Check if this position is before or after decimal
772                    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                // Commas are always displayed, but become spaces during zero suppression
837                // Check if there are any significant digits to the right (already filled)
838                // or if the value is not zero
839                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                // Slashes are always displayed (date format use case)
850                result[char_pos] = '/';
851            }
852            PicToken::Currency => {
853                // Currency symbol ($) is always displayed at its pattern position
854                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                // CR occupies 2 characters: "CR" for negative, "  " for positive
870                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                // DB occupies 2 characters: "DB" for negative, "  " for positive
883                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                // B token always inserts a literal space character
896                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    // ===== E3.1 Encode Tests =====
998
999    #[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    // ===== E3.7 Space Insertion Tests =====
1123
1124    #[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}