1pub use rt::picture::Sym;
2use syntax::ast::{CurrencySign, Environment};
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum Category {
6 Alphanumeric,
7 Numeric,
8 National,
9 Dbcs,
11 NumericEdited,
12 AlphanumericEdited,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct Picture {
17 pub category: Category,
18 pub size: u32,
21 pub digits: u32,
22 pub scale: u32,
23 pub signed: bool,
24 pub edit: Option<Vec<Sym>>,
25 pub scaling: u32,
28 pub currency: Option<String>,
30}
31
32#[derive(Clone, Copy, Debug, Default)]
35pub struct Notation<'a> {
36 pub decimal_comma: bool,
37 pub currency: &'a [CurrencySign],
38}
39
40impl<'a> Notation<'a> {
41 pub fn of(environment: &'a Environment) -> Self {
42 Self { decimal_comma: environment.decimal_point_comma, currency: &environment.currency }
43 }
44
45 pub fn currency_value(&self, symbol: char) -> Option<&'a str> {
48 match self.currency {
49 [] => (symbol == '$').then_some("$"),
50 signs => signs.iter().find(|c| c.symbol == symbol).map(|c| c.value.as_str()),
51 }
52 }
53}
54
55const MAX_EDITED: u64 = 4096;
57
58pub const MAX_POSITIONS: u64 = 134_217_727;
60
61pub fn analyse(text: &str) -> Result<Picture, String> {
62 analyse_with(text, Notation::default())
63}
64
65pub fn analyse_with(text: &str, notation: Notation) -> Result<Picture, String> {
69 let (runs, currency) = runs(text, notation)?;
70 let has = |symbol: char| runs.iter().any(|&(c, _)| c == symbol);
71 if has('G') || has('N') && has('B') {
72 return dbcs(text, &runs);
73 }
74 if runs.iter().any(|&(c, _)| matches!(c, 'Z' | '*' | '+' | '-' | '.' | ',' | 'B' | '0' | '/' | '$' | 'C' | 'R' | 'D')) {
75 return edited(text, &runs, notation.decimal_comma, currency);
76 }
77 let (mut digits, mut scale, mut signed, mut after_point) = (0u64, 0u64, false, false);
78 let (mut alnum, mut national) = (0u64, 0u64);
79 let (mut left, mut right) = (0u64, 0u64);
80 let misplaced = || Err(format!("PICTURE {text}: P must be one string of scaling positions at the left or right end of the digits"));
81 for (i, &(c, n)) in runs.iter().enumerate() {
82 match c {
83 '9' if right > 0 => return misplaced(),
84 '9' => {
85 digits += n;
86 if after_point {
87 scale += n;
88 }
89 }
90 'S' if i == 0 && n == 1 => signed = true,
91 'V' if left > 0 => return misplaced(),
92 'V' if !after_point && n == 1 => after_point = true,
93 'X' | 'A' => alnum += n,
94 'N' => national += n,
95 'P' if digits == 0 && runs.get(i + 1).is_some_and(|&(c, _)| matches!(c, 'P' | '9')) => left += n,
96 'P' if digits > 0 && left == 0 && !after_point => right += n,
97 'P' => return misplaced(),
98 _ => return Err(format!("PICTURE {text}: {c:?} is not a PICTURE symbol")),
99 }
100 }
101 if alnum + digits > MAX_POSITIONS || national > MAX_POSITIONS {
102 return Err(format!("PICTURE {text}: more than {MAX_POSITIONS} character positions"));
103 }
104 let positions = digits + left + right;
105 if left > 0 {
106 scale = left + digits;
107 }
108 let (digits, scale, alnum, national) = (digits as u32, scale as u32, alnum as u32, national as u32);
109 let scaled = left + right > 0;
110 match (digits > 0, alnum > 0, national > 0) {
111 (true, false, false) if positions <= 31 => Ok(Picture { category: Category::Numeric, size: digits, digits, scale, signed, edit: None, scaling: right as u32, currency: None }),
112 (true, false, false) => Err(format!("PICTURE {text}: more than 31 digits")),
113 (_, true, false) if !signed && !after_point && !scaled => Ok(Picture { category: Category::Alphanumeric, size: alnum + digits, digits: 0, scale: 0, signed, edit: None, scaling: 0, currency: None }),
114 (false, false, true) if !signed && !after_point && !scaled => Ok(Picture { category: Category::National, size: national, digits: 0, scale: 0, signed, edit: None, scaling: 0, currency: None }),
115 _ => Err(format!("PICTURE {text}: mixes symbols of different categories")),
116 }
117}
118
119fn dbcs(text: &str, runs: &[(char, u64)]) -> Result<Picture, String> {
123 let symbol = if runs.iter().any(|&(c, _)| c == 'G') { 'G' } else { 'N' };
124 if let Some(&(c, _)) = runs.iter().find(|&&(c, _)| c != symbol && c != 'B') {
125 return Err(format!("PICTURE {text}: {c:?} cannot be in a PICTURE of {symbol}, which takes {symbol} and B only"));
126 }
127 let size: u64 = runs.iter().map(|&(_, n)| n).sum();
128 let inserts = runs.iter().any(|&(c, _)| c == 'B');
129 if size > MAX_POSITIONS || inserts && size > MAX_EDITED {
130 return Err(format!("PICTURE {text}: more character positions than a DBCS item holds"));
131 }
132 let edit = inserts.then(|| runs.iter().flat_map(|&(c, n)| std::iter::repeat_n(if c == 'B' { Sym::Insert(' ') } else { Sym::Char }, n as usize)).collect());
133 let category = if symbol == 'G' { Category::Dbcs } else { Category::National };
134 Ok(Picture { category, size: size as u32, digits: 0, scale: 0, signed: false, edit, scaling: 0, currency: None })
135}
136
137pub fn blank_when_zero(p: &Picture) -> Result<Picture, String> {
140 if p.signed {
141 return Err("BLANK WHEN ZERO cannot be given for a PICTURE with S".into());
142 }
143 let int = p.digits.saturating_sub(p.scale) as usize;
144 let mut syms = vec![Sym::Nine; int];
145 if p.scale > 0 {
146 syms.push(Sym::Implied);
147 }
148 syms.extend(std::iter::repeat_n(Sym::Nine, p.digits as usize - int));
149 Ok(Picture { category: Category::NumericEdited, edit: Some(syms), ..p.clone() })
150}
151
152fn edited(text: &str, runs: &[(char, u64)], decimal_comma: bool, currency: Option<&str>) -> Result<Picture, String> {
153 let total: u64 = runs.iter().map(|&(_, n)| n).sum();
154 if total > MAX_EDITED {
155 return Err(format!("PICTURE {text}: an edited PICTURE longer than {MAX_EDITED} positions"));
156 }
157 let chars: Vec<char> = runs.iter().flat_map(|&(c, n)| std::iter::repeat_n(c, n as usize)).collect();
158 let bad = |why: &str| Err(format!("PICTURE {text}: {why}"));
159 if chars.iter().any(|c| matches!(c, 'S' | 'N')) {
160 return bad("S and N are not allowed in an edited PICTURE");
161 }
162 if chars.iter().any(|c| matches!(c, 'X' | 'A')) {
163 let mut syms = Vec::new();
164 for &c in &chars {
165 syms.push(match c {
166 'X' | 'A' | '9' => Sym::Char,
167 'B' => Sym::Insert(' '),
168 '0' | '/' => Sym::Insert(c),
169 _ => return bad("an alphanumeric-edited PICTURE takes only X, A, 9, B, 0 and /"),
170 });
171 }
172 let size = syms.len() as u32;
173 return Ok(Picture { category: Category::AlphanumericEdited, size, digits: 0, scale: 0, signed: false, edit: Some(syms), scaling: 0, currency: None });
174 }
175 let (point, comma) = if decimal_comma { (',', '.') } else { ('.', ',') };
176 let floating: Vec<char> = ['+', '-', '$'].into_iter().filter(|f| chars.iter().filter(|c| *c == f).count() >= 2).collect();
177 if floating.len() > 1 {
178 return bad("two floating insertion strings");
179 }
180 let float = floating.first().copied();
181 let (mut syms, mut led, mut i) = (Vec::new(), false, 0);
182 let misplaced = "P must be one string of scaling positions at the left or right end of the digits";
183 let mut scaling: Option<(usize, u32, usize)> = None;
184 while i < chars.len() {
185 let c = chars[i];
186 let next = chars.get(i + 1).copied();
187 if c == 'P' {
188 match &mut scaling {
189 Some((at, n, _)) if *at + *n as usize == i => *n += 1,
190 Some(_) => return bad(misplaced),
191 None => scaling = Some((i, 1, syms.len())),
192 }
193 i += 1;
194 continue;
195 }
196 let sym = match c {
197 'C' if next == Some('R') => {
198 i += 1;
199 Sym::Cr
200 }
201 'D' if next == Some('B') => {
202 i += 1;
203 Sym::Db
204 }
205 c if Some(c) == float && !led => {
206 led = true;
207 Sym::FloatLead(c)
208 }
209 c if Some(c) == float => Sym::Float(c),
210 '+' | '-' => Sym::Sign(c),
211 '$' => Sym::Currency,
212 '9' => Sym::Nine,
213 'Z' => Sym::Z,
214 '*' => Sym::Star,
215 c if c == point => Sym::Point,
216 'V' => Sym::Implied,
217 'B' => Sym::Insert(' '),
218 '0' | '/' => Sym::Insert(c),
219 c if c == comma => Sym::Insert(c),
220 _ => return bad(&format!("{c:?} is not a numeric-edited symbol")),
221 };
222 syms.push(sym);
223 i += 1;
224 }
225 let points = syms.iter().filter(|s| matches!(s, Sym::Point | Sym::Implied)).count();
226 if points > 1 {
227 return bad("more than one decimal point");
228 }
229 let digits = syms.iter().filter(|s| s.is_digit()).count() as u32;
230 let mut scale = syms.iter().skip_while(|s| !matches!(s, Sym::Point | Sym::Implied)).filter(|s| s.is_digit()).count() as u32;
231 let mut right = 0;
232 if let Some((_, n, at)) = scaling {
233 let before = syms[..at].iter().filter(|s| s.is_digit()).count() as u32;
234 match (points, before) {
235 (0, 0) => scale = n + digits,
236 (0, b) if b == digits => right = n,
237 _ => return bad(misplaced),
238 }
239 }
240 if digits == 0 || digits + scaling.map_or(0, |(_, n, _)| n) > 31 {
241 return bad("a numeric-edited PICTURE needs 1 to 31 digit positions");
242 }
243 let widths: u32 = syms.iter().map(|s| s.width() as u32).sum();
244 let size = widths + currency.map_or(0, |v| v.chars().count() as u32 - 1);
245 Ok(Picture { category: Category::NumericEdited, size, digits, scale, signed: false, edit: Some(syms), scaling: right, currency: currency.map(str::to_owned) })
246}
247
248type Runs<'a> = (Vec<(char, u64)>, Option<&'a str>);
250
251fn runs<'a>(text: &str, notation: Notation<'a>) -> Result<Runs<'a>, String> {
254 let mut out: Vec<(char, u64)> = Vec::new();
255 let (mut chars, mut currency) = (text.chars(), None);
256 while let Some(c) = chars.next() {
257 if c == '(' {
258 let count: String = chars.by_ref().take_while(|&d| d != ')').collect();
259 let n: u64 = count.parse().ok().filter(|&n| (1..=MAX_POSITIONS).contains(&n)).ok_or_else(|| format!("PICTURE {text}: bad repetition ({count})"))?;
260 let last = out.last_mut().ok_or_else(|| format!("PICTURE {text}: a repetition with nothing to repeat"))?;
261 last.1 += n - 1;
262 } else if let Some(value) = notation.currency_value(c) {
263 match currency {
264 Some((symbol, _)) if symbol != c => return Err(format!("PICTURE {text}: two different currency symbols")),
265 _ => currency = Some((c, value)),
266 }
267 out.push(('$', 1));
268 } else if c == '$' {
269 return Err(format!("PICTURE {text}: '$' is not a currency symbol of this program, whose CURRENCY SIGN clauses or CURRENCY option name others"));
270 } else {
271 out.push((c.to_ascii_uppercase(), 1));
272 }
273 }
274 Ok((out, currency.map(|(_, value)| value)))
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 const COMMA: Notation = Notation { decimal_comma: true, currency: &[] };
282
283 #[test]
284 fn numeric_pictures() {
285 assert_eq!(analyse("S9(3)V99").unwrap(), Picture { category: Category::Numeric, size: 5, digits: 5, scale: 2, signed: true, edit: None, scaling: 0, currency: None });
286 assert_eq!(analyse("9(18)").unwrap().digits, 18);
287 assert_eq!(analyse("SV9").unwrap().scale, 1);
288 }
289
290 #[test]
291 fn edited_pictures() {
292 let p = analyse("$$,$$9.99CR").unwrap();
293 assert_eq!((p.category, p.size, p.digits, p.scale), (Category::NumericEdited, 11, 6, 2));
294 assert_eq!(p.edit.as_ref().unwrap()[0], Sym::FloatLead('$'));
295 let z = analyse("-ZZ,ZZ9").unwrap();
296 assert_eq!(z.edit.unwrap()[0], Sym::Sign('-'));
297 assert_eq!(analyse("XXBXX/99").unwrap().category, Category::AlphanumericEdited);
298 }
299
300 #[test]
301 fn alphanumeric_and_national() {
302 assert_eq!(analyse("X(512)").unwrap().size, 512);
303 assert_eq!(analyse("N(256)").unwrap().category, Category::National);
304 }
305
306 #[test]
307 fn unsupported_and_invalid_pictures_say_why() {
308 assert!(analyse("ZZ9.99.9").unwrap_err().contains("decimal point"));
309 assert!(analyse("9(32)").unwrap_err().contains("31 digits"));
310 assert!(analyse("XN").unwrap_err().contains("categories"));
311 assert!(analyse("X(999999999)").unwrap_err().contains("repetition"));
312 assert!(analyse("X(134217727)X").unwrap_err().contains("character positions"));
313 assert_eq!(analyse("XX99").unwrap(), Picture { category: Category::Alphanumeric, size: 4, digits: 0, scale: 0, signed: false, edit: None, scaling: 0, currency: None });
314 }
315
316 #[test]
317 fn scaling_positions_left_of_the_digits_raise_the_scale_and_right_of_them_scale_the_value() {
318 let shape = |pic: &str| analyse(pic).map(|p| (p.category, p.size, p.digits, p.scale, p.scaling));
319 assert_eq!(shape("SP(8)9"), Ok((Category::Numeric, 1, 1, 9, 0)));
320 assert_eq!(shape("VPP99"), Ok((Category::Numeric, 2, 2, 4, 0)));
321 assert_eq!(shape("S999PP"), Ok((Category::Numeric, 3, 3, 0, 2)));
322 assert_eq!(shape("99P(6)V"), Ok((Category::Numeric, 2, 2, 0, 6)));
323 assert_eq!(shape("ZZZPP"), Ok((Category::NumericEdited, 3, 3, 0, 2)));
324 for bad in ["P9P", "9P9", "PPV9", "9V9P", "99VPP", "Z.ZPP", "P(31)9"] {
325 assert!(analyse(bad).is_err(), "{bad}");
326 }
327 assert!(analyse("P(30)9").is_ok());
328 }
329
330 #[test]
331 fn under_decimal_point_is_comma_the_comma_is_the_point_and_the_period_an_insertion() {
332 let p = analyse_with("Z.ZZ9,99", COMMA).unwrap();
333 assert_eq!((p.category, p.size, p.digits, p.scale), (Category::NumericEdited, 8, 6, 2));
334 assert_eq!(p.edit.as_ref().unwrap()[1], Sym::Insert('.'));
335 assert_eq!(p.edit.as_ref().unwrap()[5], Sym::Point);
336 assert_eq!(analyse_with("ZZ9.99", Notation::default()).unwrap().scale, 2);
337 assert_eq!(analyse_with("ZZ9.99", COMMA).unwrap().scale, 0);
338 assert!(analyse_with("9,99,9", COMMA).unwrap_err().contains("decimal point"));
339 }
340
341 #[test]
342 fn a_picture_can_end_in_an_insertion_comma_or_the_decimal_point() {
343 let edit = |pic: &str, notation: Notation, value: u128| {
344 let p = analyse_with(pic, notation).unwrap();
345 let point = if notation.decimal_comma { ',' } else { '.' };
346 ((p.category, p.size, p.digits, p.scale), rt::edit::numeric(p.edit.as_ref().unwrap(), p.digits, false, value, false, point, ""))
347 };
348 assert_eq!(edit("9,9,9,", Notation::default(), 123), ((Category::NumericEdited, 6, 3, 0), "1,2,3,".into()));
349 assert_eq!(edit("999.", Notation::default(), 7), ((Category::NumericEdited, 4, 3, 0), "007.".into()));
350 assert_eq!(edit("9.9.9,", COMMA, 123), ((Category::NumericEdited, 6, 3, 0), "1.2.3,".into()));
351 assert_eq!(edit("999.", COMMA, 7), ((Category::NumericEdited, 4, 3, 0), "007.".into()));
352 assert_eq!(analyse_with("999.", COMMA).unwrap().edit.unwrap()[3], Sym::Insert('.'));
353 }
354
355 #[test]
356 fn a_currency_sign_clause_names_the_symbol_and_the_value_it_stands_for() {
357 let signs = [CurrencySign { value: "W".into(), symbol: 'W', hex: None }, CurrencySign { value: "EUR ".into(), symbol: 'e', hex: None }];
358 let notation = Notation { decimal_comma: true, currency: &signs };
359 let w = analyse_with("W9.999,99", notation).unwrap();
360 assert_eq!((w.category, w.size, w.digits, w.scale, w.currency.as_deref()), (Category::NumericEdited, 9, 6, 2, Some("W")));
361 assert_eq!(w.edit.as_ref().unwrap()[0], Sym::Currency);
362 let floating = analyse_with("eeee9,99", notation).unwrap();
363 assert_eq!((floating.size, floating.digits, floating.currency.as_deref()), (11, 6, Some("EUR ")));
364 assert_eq!(floating.edit.as_ref().unwrap()[0], Sym::FloatLead('$'));
365 assert!(analyse_with("$$9", notation).unwrap_err().contains("'$'"));
366 assert!(analyse_with("We9", notation).unwrap_err().contains("two different currency symbols"));
367 assert!(analyse_with("E9", notation).unwrap_err().contains("not a PICTURE symbol"));
368 assert_eq!(analyse("$9").unwrap().currency.as_deref(), Some("$"));
369 }
370
371 #[test]
372 fn blank_when_zero_makes_a_numeric_picture_numeric_edited() {
373 let p = blank_when_zero(&analyse("99V9").unwrap()).unwrap();
374 assert_eq!((p.category, p.size, p.digits, p.scale), (Category::NumericEdited, 3, 3, 1));
375 assert_eq!(p.edit.unwrap(), [Sym::Nine, Sym::Nine, Sym::Implied, Sym::Nine]);
376 assert_eq!(blank_when_zero(&analyse("VPP99").unwrap()).unwrap().edit.unwrap(), [Sym::Implied, Sym::Nine, Sym::Nine]);
377 assert!(blank_when_zero(&analyse("S99").unwrap()).unwrap_err().contains("with S"));
378 }
379}