⎇ 4d9ce11b1e444430e6e2bcc0a72baa1d40a4527c
crates/modules/charka/charka-shadow/src/field.rs
Historia
Quién tocó qué
crudo
feat(charka): charka-shadow — validador en sombra + corpus COBOL
1//! El estado de los datos durante la ejecución sombra: el árbol de
2//! `DataItem` del IR se aplana a un mapa de campos vivos.
3//!
4//! La clasificación de PICTURE refleja la de `charka-codegen` — un
5//! futuro refactor la unificaría en `charka-runtime`.
6
7use std::collections::HashMap;
8
9use charka_ir::DataItem;
10use charka_runtime::{Num, Picture, Text};
11
12/// Un campo vivo: numérico o alfanumérico.
13pub(crate) enum Cell {
14 Num(Num),
15 Text(Text),
16}
17
18/// Aplana el árbol de datos en un mapa `nombre COBOL → campo`.
19pub(crate) fn build_fields(data: &[DataItem]) -> HashMap<String, Cell> {
20 let mut map = HashMap::new();
21 collect(data, &mut map);
22 map
23}
24
25/// Recorre el árbol: los grupos no son campos (se recurre en sus
26/// hijos); se saltan los niveles 88/66 y los `FILLER`.
27fn collect(items: &[DataItem], map: &mut HashMap<String, Cell>) {
28 for it in items {
29 if it.level == 88 || it.level == 66 {
30 continue;
31 }
32 if !it.children.is_empty() {
33 collect(&it.children, map);
34 continue;
35 }
36 if it.name == "FILLER" {
37 continue;
38 }
39 if let Some(cell) = make_cell(it.picture.as_deref(), it.value.as_deref()) {
40 map.entry(it.name.to_uppercase()).or_insert(cell);
41 }
42 }
43}
44
45/// Construye un campo desde su PICTURE y su cláusula `VALUE`.
46fn make_cell(pic: Option<&str>, value: Option<&str>) -> Option<Cell> {
47 let up = pic?.to_uppercase();
48 if up.contains('X') || up.contains('A') {
49 return Some(Cell::Text(Text::with_value(
50 pic_width(&up).max(1),
51 &text_value(value),
52 )));
53 }
54 if let Ok(p) = Picture::parse(&up) {
55 return Some(Cell::Num(Num::with_value(p, &numeric_value(value))));
56 }
57 // PICTURE de edición → campo de texto de presentación.
58 Some(Cell::Text(Text::with_value(
59 pic_width(&up).max(1),
60 &text_value(value),
61 )))
62}
63
64/// Cuenta las posiciones de presentación de una PICTURE, expandiendo
65/// la repetición `C(n)`. `S` y `V` no ocupan posición.
66fn pic_width(up: &str) -> usize {
67 let chars: Vec<char> = up.chars().collect();
68 let mut i = 0;
69 let mut total = 0usize;
70 while i < chars.len() {
71 let c = chars[i];
72 i += 1;
73 if c == 'S' || c == 'V' {
74 continue;
75 }
76 let mut count = 1usize;
77 if chars.get(i) == Some(&'(') {
78 i += 1;
79 let start = i;
80 while i < chars.len() && chars[i].is_ascii_digit() {
81 i += 1;
82 }
83 if let Ok(n) = chars[start..i].iter().collect::<String>().parse::<usize>() {
84 count = n;
85 }
86 if chars.get(i) == Some(&')') {
87 i += 1;
88 }
89 }
90 total += count;
91 }
92 total
93}
94
95/// Normaliza el `VALUE` de un campo numérico a un literal parseable.
96fn numeric_value(v: Option<&str>) -> String {
97 let Some(raw) = v else {
98 return "0".to_string();
99 };
100 if matches!(raw.to_uppercase().as_str(), "ZERO" | "ZEROS" | "ZEROES") {
101 return "0".to_string();
102 }
103 if charka_runtime::Decimal::parse(raw).is_ok() {
104 raw.to_string()
105 } else {
106 "0".to_string()
107 }
108}
109
110/// Normaliza el `VALUE` de un campo de texto. El parser envuelve los
111/// literales de texto en comillas simples; aquí se desenvuelven.
112fn text_value(v: Option<&str>) -> String {
113 let Some(raw) = v else {
114 return String::new();
115 };
116 let up = raw.to_uppercase();
117 if matches!(up.as_str(), "SPACE" | "SPACES") {
118 return String::new();
119 }
120 if matches!(up.as_str(), "ZERO" | "ZEROS" | "ZEROES") {
121 return "0".to_string();
122 }
123 if raw.len() >= 2 && raw.starts_with('\'') && raw.ends_with('\'') {
124 raw[1..raw.len() - 1].to_string()
125 } else {
126 raw.to_string()
127 }
128}