Skip to main content

ironwork_exec/
picture.rs

1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2pub enum Category {
3    Alphanumeric,
4    Numeric,
5    National,
6    NumericEdited,
7    AlphanumericEdited,
8}
9
10/// One position of an edited PICTURE.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Sym {
13    Nine,
14    Z,
15    Star,
16    /// The first symbol of a floating insertion string: a sign or currency position, not a digit.
17    FloatLead(char),
18    /// A later symbol of a floating insertion string: a digit position.
19    Float(char),
20    /// A fixed + or -.
21    Sign(char),
22    Currency,
23    Cr,
24    Db,
25    Point,
26    /// V: the decimal point, occupying no position.
27    Implied,
28    /// B (as a space), 0, / or a comma.
29    Insert(char),
30    /// X, A or 9 in an alphanumeric-edited PICTURE.
31    Char,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct Picture {
36    pub category: Category,
37    /// Character positions: bytes for alphanumeric, digits for numeric, characters for national.
38    pub size: u32,
39    pub digits: u32,
40    pub scale: u32,
41    pub signed: bool,
42    pub edit: Option<Vec<Sym>>,
43}
44
45/// An edited PICTURE is written out position by position, so its length is bounded tighter.
46const MAX_EDITED: u64 = 4096;
47
48/// Enterprise COBOL's limit on an elementary item's character positions.
49pub const MAX_POSITIONS: u64 = 134_217_727;
50
51pub fn analyse(text: &str) -> Result<Picture, String> {
52    let runs = runs(text)?;
53    let (mut digits, mut scale, mut signed, mut after_point) = (0u64, 0u64, false, false);
54    let (mut alnum, mut national) = (0u64, 0u64);
55    for (i, &(c, n)) in runs.iter().enumerate() {
56        match c {
57            '9' => {
58                digits += n;
59                if after_point {
60                    scale += n;
61                }
62            }
63            'S' if i == 0 && n == 1 => signed = true,
64            'V' if !after_point && n == 1 => after_point = true,
65            'X' | 'A' => alnum += n,
66            'N' => national += n,
67            'P' => return Err(format!("PICTURE {text}: scaling position P is not supported yet")),
68            'Z' | '*' | '+' | '-' | '.' | ',' | 'B' | '0' | '/' | '$' | 'C' | 'R' | 'D' => return edited(text, &runs),
69            _ => return Err(format!("PICTURE {text}: {c:?} is not a PICTURE symbol")),
70        }
71    }
72    if alnum + digits > MAX_POSITIONS || national > MAX_POSITIONS {
73        return Err(format!("PICTURE {text}: more than {MAX_POSITIONS} character positions"));
74    }
75    let (digits, scale, alnum, national) = (digits as u32, scale as u32, alnum as u32, national as u32);
76    match (digits > 0, alnum > 0, national > 0) {
77        (true, false, false) if digits <= 31 => Ok(Picture { category: Category::Numeric, size: digits, digits, scale, signed, edit: None }),
78        (true, false, false) => Err(format!("PICTURE {text}: more than 31 digits")),
79        (_, true, false) if !signed && !after_point => Ok(Picture { category: Category::Alphanumeric, size: alnum + digits, digits: 0, scale: 0, signed, edit: None }),
80        (false, false, true) if !signed && !after_point => Ok(Picture { category: Category::National, size: national, digits: 0, scale: 0, signed, edit: None }),
81        _ => Err(format!("PICTURE {text}: mixes symbols of different categories")),
82    }
83}
84
85fn edited(text: &str, runs: &[(char, u64)]) -> Result<Picture, String> {
86    let total: u64 = runs.iter().map(|&(_, n)| n).sum();
87    if total > MAX_EDITED {
88        return Err(format!("PICTURE {text}: an edited PICTURE longer than {MAX_EDITED} positions"));
89    }
90    let chars: Vec<char> = runs.iter().flat_map(|&(c, n)| std::iter::repeat_n(c, n as usize)).collect();
91    let bad = |why: &str| Err(format!("PICTURE {text}: {why}"));
92    if chars.iter().any(|c| matches!(c, 'S' | 'N' | 'P')) {
93        return bad("S, N and P are not allowed in an edited PICTURE");
94    }
95    if chars.iter().any(|c| matches!(c, 'X' | 'A')) {
96        let mut syms = Vec::new();
97        for &c in &chars {
98            syms.push(match c {
99                'X' | 'A' | '9' => Sym::Char,
100                'B' => Sym::Insert(' '),
101                '0' | '/' => Sym::Insert(c),
102                _ => return bad("an alphanumeric-edited PICTURE takes only X, A, 9, B, 0 and /"),
103            });
104        }
105        let size = syms.len() as u32;
106        return Ok(Picture { category: Category::AlphanumericEdited, size, digits: 0, scale: 0, signed: false, edit: Some(syms) });
107    }
108    let floating: Vec<char> = ['+', '-', '$'].into_iter().filter(|f| chars.iter().filter(|c| *c == f).count() >= 2).collect();
109    if floating.len() > 1 {
110        return bad("two floating insertion strings");
111    }
112    let float = floating.first().copied();
113    let (mut syms, mut led, mut i) = (Vec::new(), false, 0);
114    while i < chars.len() {
115        let c = chars[i];
116        let next = chars.get(i + 1).copied();
117        let sym = match c {
118            'C' if next == Some('R') => {
119                i += 1;
120                Sym::Cr
121            }
122            'D' if next == Some('B') => {
123                i += 1;
124                Sym::Db
125            }
126            c if Some(c) == float && !led => {
127                led = true;
128                Sym::FloatLead(c)
129            }
130            c if Some(c) == float => Sym::Float(c),
131            '+' | '-' => Sym::Sign(c),
132            '$' => Sym::Currency,
133            '9' => Sym::Nine,
134            'Z' => Sym::Z,
135            '*' => Sym::Star,
136            '.' => Sym::Point,
137            'V' => Sym::Implied,
138            'B' => Sym::Insert(' '),
139            '0' | '/' | ',' => Sym::Insert(c),
140            _ => return bad(&format!("{c:?} is not a numeric-edited symbol")),
141        };
142        syms.push(sym);
143        i += 1;
144    }
145    let points = syms.iter().filter(|s| matches!(s, Sym::Point | Sym::Implied)).count();
146    if points > 1 {
147        return bad("more than one decimal point");
148    }
149    let is_digit = |s: &Sym| matches!(s, Sym::Nine | Sym::Z | Sym::Star | Sym::Float(_));
150    let digits = syms.iter().filter(|s| is_digit(s)).count() as u32;
151    let scale = syms.iter().skip_while(|s| !matches!(s, Sym::Point | Sym::Implied)).filter(|s| is_digit(s)).count() as u32;
152    if digits == 0 || digits > 31 {
153        return bad("a numeric-edited PICTURE needs 1 to 31 digit positions");
154    }
155    let size = syms.iter().map(|s| match s {
156        Sym::Implied => 0,
157        Sym::Cr | Sym::Db => 2,
158        _ => 1,
159    }).sum();
160    Ok(Picture { category: Category::NumericEdited, size, digits, scale, signed: false, edit: Some(syms) })
161}
162
163/// The PICTURE as runs of one symbol and a count, `9(4)` read as four nines without writing them out.
164fn runs(text: &str) -> Result<Vec<(char, u64)>, String> {
165    let mut out: Vec<(char, u64)> = Vec::new();
166    let mut chars = text.chars();
167    while let Some(c) = chars.next() {
168        if c == '(' {
169            let count: String = chars.by_ref().take_while(|&d| d != ')').collect();
170            let n: u64 = count.parse().ok().filter(|&n| (1..=MAX_POSITIONS).contains(&n)).ok_or_else(|| format!("PICTURE {text}: bad repetition ({count})"))?;
171            let last = out.last_mut().ok_or_else(|| format!("PICTURE {text}: a repetition with nothing to repeat"))?;
172            last.1 += n - 1;
173        } else {
174            out.push((c.to_ascii_uppercase(), 1));
175        }
176    }
177    Ok(out)
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn numeric_pictures() {
186        assert_eq!(analyse("S9(3)V99").unwrap(), Picture { category: Category::Numeric, size: 5, digits: 5, scale: 2, signed: true, edit: None });
187        assert_eq!(analyse("9(18)").unwrap().digits, 18);
188        assert_eq!(analyse("SV9").unwrap().scale, 1);
189    }
190
191    #[test]
192    fn edited_pictures() {
193        let p = analyse("$$,$$9.99CR").unwrap();
194        assert_eq!((p.category, p.size, p.digits, p.scale), (Category::NumericEdited, 11, 6, 2));
195        assert_eq!(p.edit.as_ref().unwrap()[0], Sym::FloatLead('$'));
196        let z = analyse("-ZZ,ZZ9").unwrap();
197        assert_eq!(z.edit.unwrap()[0], Sym::Sign('-'));
198        assert_eq!(analyse("XXBXX/99").unwrap().category, Category::AlphanumericEdited);
199    }
200
201    #[test]
202    fn alphanumeric_and_national() {
203        assert_eq!(analyse("X(512)").unwrap().size, 512);
204        assert_eq!(analyse("N(256)").unwrap().category, Category::National);
205    }
206
207    #[test]
208    fn unsupported_and_invalid_pictures_say_why() {
209        assert!(analyse("ZZ9.99.9").unwrap_err().contains("decimal point"));
210        assert!(analyse("9(32)").unwrap_err().contains("31 digits"));
211        assert!(analyse("XN").unwrap_err().contains("categories"));
212        assert!(analyse("X(999999999)").unwrap_err().contains("repetition"));
213        assert!(analyse("X(134217727)X").unwrap_err().contains("character positions"));
214        assert_eq!(analyse("XX99").unwrap(), Picture { category: Category::Alphanumeric, size: 4, digits: 0, scale: 0, signed: false, edit: None });
215    }
216}