feat(charka): charka-bcd — aritmética decimal con semántica COBOL
Cimiento numérico del transpilador. Picture parsea la cláusula PICTURE (9, V, S, 9(n)); Decimal es punto fijo exacto (mantissa i128 + scale) con suma/resta/producto exactos, división con escala de resultado fija, redondeo Truncate/HalfUp y coerce a un Picture con detección de desbordamiento (ON SIZE ERROR). 22 tests. Determinista, sin deps de plataforma — base de Fase D. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
//! La cláusula `PICTURE` — la forma declarada de un campo numérico COBOL.
|
||||
//!
|
||||
//! Sólo el subconjunto numérico: `9` (dígito), `V` (punto decimal
|
||||
//! implícito), `S` (signo), y la repetición `9(n)`. Lo de edición
|
||||
//! (`Z`, `*`, `,`, `.`, `$`, `B`…) es presentación y se trata aparte.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::BcdError;
|
||||
|
||||
/// La forma de un campo numérico: cuántos dígitos enteros, cuántos
|
||||
/// fraccionarios y si admite signo.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Picture {
|
||||
pub integer_digits: u8,
|
||||
pub fraction_digits: u8,
|
||||
pub signed: bool,
|
||||
}
|
||||
|
||||
impl Picture {
|
||||
/// Construye una `Picture` directa.
|
||||
pub fn new(integer_digits: u8, fraction_digits: u8, signed: bool) -> Self {
|
||||
Self { integer_digits, fraction_digits, signed }
|
||||
}
|
||||
|
||||
/// Dígitos totales del campo (enteros + fraccionarios).
|
||||
pub fn total_digits(&self) -> u8 {
|
||||
self.integer_digits + self.fraction_digits
|
||||
}
|
||||
|
||||
/// Parsea una cláusula PICTURE — `"9(5)V99"`, `"S9(3)"`, `"9999"`.
|
||||
/// Acepta el prefijo `PIC ` / `PICTURE ` opcional.
|
||||
pub fn parse(src: &str) -> Result<Picture, BcdError> {
|
||||
let up = src.trim().to_ascii_uppercase();
|
||||
let body = up
|
||||
.strip_prefix("PICTURE ")
|
||||
.or_else(|| up.strip_prefix("PIC "))
|
||||
.unwrap_or(&up)
|
||||
.trim();
|
||||
|
||||
let chars: Vec<char> = body.chars().collect();
|
||||
let mut i = 0;
|
||||
let mut signed = false;
|
||||
let mut integer_digits: u32 = 0;
|
||||
let mut fraction_digits: u32 = 0;
|
||||
let mut seen_v = false;
|
||||
|
||||
// El signo, si lo hay, va primero.
|
||||
if chars.first() == Some(&'S') {
|
||||
signed = true;
|
||||
i = 1;
|
||||
}
|
||||
|
||||
let bad = || BcdError::BadPicture(src.to_string());
|
||||
|
||||
while i < chars.len() {
|
||||
match chars[i] {
|
||||
'V' => {
|
||||
if seen_v {
|
||||
return Err(bad()); // dos puntos decimales
|
||||
}
|
||||
seen_v = true;
|
||||
i += 1;
|
||||
}
|
||||
'9' => {
|
||||
// Cuenta este 9 y un posible '(n)' que lo siga.
|
||||
let mut count: u32 = 1;
|
||||
i += 1;
|
||||
if chars.get(i) == Some(&'(') {
|
||||
i += 1;
|
||||
let start = i;
|
||||
while i < chars.len() && chars[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
}
|
||||
if start == i || chars.get(i) != Some(&')') {
|
||||
return Err(bad());
|
||||
}
|
||||
let n: u32 = chars[start..i]
|
||||
.iter()
|
||||
.collect::<String>()
|
||||
.parse()
|
||||
.map_err(|_| bad())?;
|
||||
count = n;
|
||||
i += 1; // consume ')'
|
||||
}
|
||||
if seen_v {
|
||||
fraction_digits += count;
|
||||
} else {
|
||||
integer_digits += count;
|
||||
}
|
||||
}
|
||||
_ => return Err(bad()),
|
||||
}
|
||||
}
|
||||
|
||||
let total = integer_digits + fraction_digits;
|
||||
if total == 0 || total > 38 {
|
||||
// i128 soporta 38 dígitos decimales.
|
||||
return Err(bad());
|
||||
}
|
||||
Ok(Picture {
|
||||
integer_digits: integer_digits as u8,
|
||||
fraction_digits: fraction_digits as u8,
|
||||
signed,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_integer_and_fraction() {
|
||||
let p = Picture::parse("9(5)V99").unwrap();
|
||||
assert_eq!(p, Picture::new(5, 2, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_signed() {
|
||||
let p = Picture::parse("S9(3)").unwrap();
|
||||
assert_eq!(p, Picture::new(3, 0, true));
|
||||
assert!(p.signed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_repeated_nines() {
|
||||
assert_eq!(Picture::parse("9999V9").unwrap(), Picture::new(4, 1, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_pic_prefix() {
|
||||
assert_eq!(Picture::parse("PIC 9(2)").unwrap(), Picture::new(2, 0, false));
|
||||
assert_eq!(Picture::parse("PICTURE S9V9").unwrap(), Picture::new(1, 1, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_garbage_and_double_v() {
|
||||
assert!(Picture::parse("X(3)").is_err());
|
||||
assert!(Picture::parse("9V9V9").is_err());
|
||||
assert!(Picture::parse("").is_err());
|
||||
assert!(Picture::parse("9(").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_digits_sums_both_parts() {
|
||||
assert_eq!(Picture::parse("9(7)V999").unwrap().total_digits(), 10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user