1use crate::picture::Sym;
5
6fn is_digit(s: &Sym) -> bool {
7 matches!(s, Sym::Nine | Sym::Z | Sym::Star | Sym::Float(_))
8}
9
10pub 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
95pub 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
107pub 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}