feat(charka): PICTURE de edición — Z, coma de millares y punto decimal

El formateo de informes de COBOL: supresión de ceros a la izquierda,
coma de millares e inserción del punto decimal. Rebanada vertical.

- charka-lexer: el punto separador exige un espacio detrás; un punto
  pegado a un carácter (ZZ9.99) ya no es terminador, sino símbolo —
  el parser lo reensambla dentro de la cláusula PICTURE.
- charka-runtime: format_edited(valor, pic) — 9, Z, coma, punto, B.
- charka-ir: Field::edit guarda la PICTURE; el campo es texto.
- charka-codegen / charka-shadow: MOVE a un campo de edición pasa por
  format_edited antes de almacenar.
- Corpus: 19-reporte. Sombra y crate compilado dan la misma salida.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 23:00:15 +00:00
parent b3278bdb0c
commit 634a43006a
15 changed files with 264 additions and 23 deletions
+30 -12
View File
@@ -31,6 +31,9 @@ pub struct Field {
/// Si es una tabla (`OCCURS n`), su número de elementos; `None`
/// para un dato escalar.
pub occurs: Option<u32>,
/// Si es un campo de edición (`ZZ,ZZ9.99`), su PICTURE — para
/// formatear el valor al moverlo. El campo se almacena como texto.
pub edit: Option<String>,
}
/// Un nombre de condición — un dato de nivel 88. `IF <name>` equivale
@@ -126,7 +129,7 @@ fn walk(items: &[DataItem], model: &mut DataModel) -> Vec<String> {
}
produced.extend(members);
} else if it.name != "FILLER" {
if let Some(kind) = classify(it.picture.as_deref()) {
if let Some((kind, edit)) = classify(it.picture.as_deref()) {
let init = match kind {
FieldKind::Num { .. } => numeric_value(it.value.as_deref()),
FieldKind::Text { .. } => text_value(it.value.as_deref()),
@@ -137,6 +140,7 @@ fn walk(items: &[DataItem], model: &mut DataModel) -> Vec<String> {
kind,
init,
occurs: it.occurs,
edit,
});
}
}
@@ -147,23 +151,37 @@ fn walk(items: &[DataItem], model: &mut DataModel) -> Vec<String> {
/// Clasifica una cláusula PICTURE: alfanumérica si tiene `X`/`A`,
/// numérica si `charka-bcd` la parsea; una PICTURE de edición se trata
/// como texto de presentación.
fn classify(pic: Option<&str>) -> Option<FieldKind> {
fn classify(pic: Option<&str>) -> Option<(FieldKind, Option<String>)> {
let up = pic?.to_uppercase();
if up.contains('X') || up.contains('A') {
return Some(FieldKind::Text {
len: pic_width(&up).max(1),
});
return Some((
FieldKind::Text {
len: pic_width(&up).max(1),
},
None,
));
}
if let Ok(p) = Picture::parse(&up) {
return Some(FieldKind::Num {
int: p.integer_digits,
frac: p.fraction_digits,
signed: p.signed,
});
return Some((
FieldKind::Num {
int: p.integer_digits,
frac: p.fraction_digits,
signed: p.signed,
},
None,
));
}
Some(FieldKind::Text {
// ¿PICTURE de edición? — tiene un símbolo de edición y un dígito.
let kind = FieldKind::Text {
len: pic_width(&up).max(1),
})
};
let has_edit = up.contains(['Z', ',', '.', '*']);
let has_digit = up.contains(['9', 'Z']);
if has_edit && has_digit {
Some((kind, Some(up)))
} else {
Some((kind, None))
}
}
/// Cuenta las posiciones de presentación de una PICTURE, expandiendo