Skip to main content

ironwork_exec/
edit.rs

1//! Editing: a numeric value laid into a numeric-edited PICTURE, alphanumeric data into an
2//! alphanumeric-edited one, and de-editing back to a number.
3
4use crate::picture::Sym;
5
6fn is_digit(s: &Sym) -> bool {
7    matches!(s, Sym::Nine | Sym::Z | Sym::Star | Sym::Float(_))
8}
9
10/// The characters a numeric-edited item holds for a value. `magnitude` is already aligned to the
11/// PICTURE's decimal places and within its digit positions.
12pub fn numeric(syms: &[Sym], digits: u32, negative: bool, magnitude: u128, blank_when_zero: bool) -> String {
13    let size: usize = syms.iter().map(|s| match s {
14        Sym::Implied => 0,
15        Sym::Cr | Sym::Db => 2,
16        _ => 1,
17    }).sum();
18    let digit_syms: Vec<&Sym> = syms.iter().filter(|s| is_digit(s)).collect();
19    if magnitude == 0 && (blank_when_zero || digit_syms.iter().all(|s| matches!(s, Sym::Z | Sym::Float(_)))) {
20        return " ".repeat(size);
21    }
22    if magnitude == 0 && digit_syms.iter().all(|s| matches!(s, Sym::Star)) {
23        return syms.iter().filter(|s| !matches!(s, Sym::Implied)).map(|s| if *s == Sym::Point { '.' } else { '*' }).collect();
24    }
25    let text = format!("{magnitude:0width$}", width = digits as usize);
26    let mut digit_chars = text.chars().skip(text.len() - digits as usize);
27    let mut out: Vec<char> = Vec::with_capacity(size);
28    let (mut significant, mut fill) = (false, ' ');
29    let (mut float_char, mut float_slot) = (None, None);
30    for s in syms {
31        match *s {
32            Sym::Nine => {
33                significant = true;
34                out.push(digit_chars.next().unwrap_or('0'));
35            }
36            Sym::Z | Sym::Star => {
37                fill = if *s == Sym::Star { '*' } else { ' ' };
38                let d = digit_chars.next().unwrap_or('0');
39                significant |= d != '0';
40                out.push(if significant { d } else { fill });
41            }
42            Sym::FloatLead(c) => {
43                float_char = Some(c);
44                float_slot = Some(out.len());
45                out.push(' ');
46            }
47            Sym::Float(_) => {
48                let d = digit_chars.next().unwrap_or('0');
49                significant |= d != '0';
50                if significant {
51                    out.push(d);
52                } else {
53                    float_slot = Some(out.len());
54                    out.push(' ');
55                }
56            }
57            Sym::Point | Sym::Implied => {
58                significant = true;
59                if *s == Sym::Point {
60                    out.push('.');
61                }
62            }
63            Sym::Insert(c) => {
64                if significant {
65                    out.push(c);
66                } else if float_char.is_some() {
67                    float_slot = Some(out.len());
68                    out.push(' ');
69                } else {
70                    out.push(fill);
71                }
72            }
73            Sym::Sign(c) => out.push(match (c, negative) {
74                ('+', false) => '+',
75                (_, true) => '-',
76                _ => ' ',
77            }),
78            Sym::Currency => out.push('$'),
79            Sym::Cr => out.extend(if negative { ['C', 'R'] } else { [' ', ' '] }),
80            Sym::Db => out.extend(if negative { ['D', 'B'] } else { [' ', ' '] }),
81            Sym::Char => out.push(' '),
82        }
83    }
84    if let (Some(c), Some(slot)) = (float_char, float_slot) {
85        out[slot] = match (c, negative) {
86            ('$', _) => '$',
87            ('+', false) => '+',
88            (_, true) => '-',
89            _ => ' ',
90        };
91    }
92    out.into_iter().collect()
93}
94
95/// Alphanumeric data laid into an alphanumeric-edited PICTURE: characters fill the X, A and 9
96/// positions in order; insertion characters stand where they are written.
97pub fn alphanumeric(syms: &[Sym], data: &[u8], space: u8, encode: impl Fn(char) -> u8) -> Vec<u8> {
98    let mut source = data.iter();
99    syms.iter()
100        .map(|s| match s {
101            Sym::Insert(c) => encode(*c),
102            _ => source.next().copied().unwrap_or(space),
103        })
104        .collect()
105}
106
107/// A numeric-edited item's value: the digits in its digit positions, negative when it shows a
108/// minus sign, CR or DB.
109pub fn de_edit(syms: &[Sym], text: &str) -> (bool, u128) {
110    let chars: Vec<char> = text.chars().collect();
111    let (mut at, mut magnitude, mut negative) = (0usize, 0u128, false);
112    for s in syms {
113        let c = chars.get(at).copied().unwrap_or(' ');
114        match s {
115            Sym::Implied => continue,
116            Sym::Cr | Sym::Db => {
117                negative |= c != ' ';
118                at += 2;
119                continue;
120            }
121            _ if is_digit(s) => magnitude = magnitude * 10 + c.to_digit(10).unwrap_or(0) as u128,
122            Sym::Sign(_) | Sym::FloatLead(_) | Sym::Float(_) => negative |= c == '-',
123            _ => {}
124        }
125        if matches!(s, Sym::Float(_)) {
126            negative |= c == '-';
127        }
128        at += 1;
129    }
130    (negative, magnitude)
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::picture::analyse;
137
138    fn edit(pic: &str, value: i128) -> String {
139        let p = analyse(pic).unwrap();
140        numeric(p.edit.as_ref().unwrap(), p.digits, value < 0, value.unsigned_abs(), false)
141    }
142
143    #[test]
144    fn zero_suppression_and_insertion() {
145        assert_eq!(edit("ZZ,ZZ9", 1234), " 1,234");
146        assert_eq!(edit("ZZ,ZZ9", 5), "     5");
147        assert_eq!(edit("ZZZ", 0), "   ");
148        assert_eq!(edit("ZZ9.99", 505), "  5.05");
149        assert_eq!(edit("ZZ.ZZ", 0), "     ");
150        assert_eq!(edit("99/99/99", 270926), "27/09/26");
151        assert_eq!(edit("999B999", 123456), "123 456");
152    }
153
154    #[test]
155    fn check_protection() {
156        assert_eq!(edit("**,**9.99", 1234), "****12.34");
157        assert_eq!(edit("***", 0), "***");
158        assert_eq!(edit("**.**", 0), "**.**");
159    }
160
161    #[test]
162    fn floating_currency_and_signs() {
163        assert_eq!(edit("$$,$$9.99", 123450), "$1,234.50");
164        assert_eq!(edit("$$,$$9.99", 550), "    $5.50");
165        assert_eq!(edit("----9", -42), "  -42");
166        assert_eq!(edit("----9", 42), "   42");
167        assert_eq!(edit("++++9", 42), "  +42");
168    }
169
170    #[test]
171    fn fixed_signs_cr_and_db() {
172        assert_eq!(edit("-ZZ9", -7), "-  7");
173        assert_eq!(edit("+ZZ9", 7), "+  7");
174        assert_eq!(edit("ZZ9-", -7), "  7-");
175        assert_eq!(edit("ZZ9CR", -7), "  7CR");
176        assert_eq!(edit("ZZ9DB", 7), "  7  ");
177        assert_eq!(edit("$ZZ9.99", 1999), "$ 19.99");
178    }
179
180    #[test]
181    fn blank_when_zero() {
182        let p = analyse("999.99").unwrap();
183        assert_eq!(numeric(p.edit.as_ref().unwrap(), p.digits, false, 0, true), "      ");
184    }
185
186    #[test]
187    fn de_editing_recovers_the_value() {
188        for (pic, v) in [("$$,$$9.99CR", -123450i128), ("ZZ9-", -7), ("ZZ,ZZ9", 1234)] {
189            let p = analyse(pic).unwrap();
190            let text = numeric(p.edit.as_ref().unwrap(), p.digits, v < 0, v.unsigned_abs(), false);
191            assert_eq!(de_edit(p.edit.as_ref().unwrap(), &text), (v < 0, v.unsigned_abs()), "{pic} {text:?}");
192        }
193    }
194
195    #[test]
196    fn alphanumeric_editing() {
197        let p = analyse("XXBXX/X").unwrap();
198        let out = alphanumeric(p.edit.as_ref().unwrap(), b"ABCDE", b' ', |c| c as u8);
199        assert_eq!(out, b"AB CD/E");
200    }
201}