gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 815Rust 56Shell 37Markdown 291229 Definiciones

⎇ a5ad955459a91c70830dc6ee466bff6a78f9004e crates/qillqa/src/valor.rs Rust · 208 líneas · 8.0 KB Historia Quién tocó qué crudo

qillqa: el awk propio — 3531 líneas, sólo libc, y CERO divergencias contra dos oráculos ae6f0b45d · Sergio ·

1//! El modelo de valores de awk. Es la parte que más silenciosamente se equivoca.
2//!
3//! awk tiene TRES clases de valor, no dos (POSIX.1-2024 §2.6, «Expressions in awk»):
4//!
5//!   · **número**   — `1`, `x+0`, el resultado de cualquier aritmética.
6//!   · **cadena**   — un literal entre comillas, o el resultado de `substr`, `sprintf`…
7//!   · **strnum**   — lo que VIENE DE FUERA y *parece* un número: campos (`$1`), `getline`, los
8//!                    trozos de `split`, `ARGV`, `ENVIRON`, `FILENAME`. Se compara como número.
9//!
10//! La tercera es la que decide, y la diferencia es observable:
11//!
12//! ```text
13//! echo '10 9' | awk '{ print ($1 > $2) }'        ⇒ 1   (strnum: comparación NUMÉRICA)
14//! awk 'BEGIN { print ("10" > "9") }'             ⇒ 0   (cadenas: "1" < "9" alfabéticamente)
15//! ```
16//!
17//! Un awk que trate los campos como cadenas ordena mal cualquier tabla de números; uno que los trate
18//! siempre como números rompe `$1 == "0012"`. Por eso `Valor` lleva la procedencia y no sólo el dato.
19
20use std::fmt::Write as _;
21
22#[derive(Debug, Clone)]
23pub enum Valor {
24    Num(f64),
25    Cad(String),
26    /// Vino de la entrada. Guarda el texto TAL CUAL —`print $1` tiene que imprimir `0012`, no `12`—
27    /// y, si parece un número, también su valor.
28    StrNum(String, Option<f64>),
29    /// Variable nunca asignada: vale `""` y `0` a la vez, y compara igual con los dos.
30    Vacio,
31}
32
33/// ¿Este texto es un número en el sentido de awk? Admite espacios alrededor, signo, punto decimal y
34/// exponente; NO admite basura detrás (`"12x"` es cadena) ni hexadecimal (POSIX lo deja fuera, y
35/// gawk sólo lo acepta en modo no-POSIX).
36pub fn como_numero(s: &str) -> Option<f64> {
37    let t = s.trim_matches(|c: char| c == ' ' || c == '\t' || c == '\n');
38    if t.is_empty() { return None; }
39    let b: Vec<char> = t.chars().collect();
40    let mut i = 0;
41    if b[i] == '+' || b[i] == '-' { i += 1; }
42    let mut digitos = false;
43    while i < b.len() && b[i].is_ascii_digit() { i += 1; digitos = true; }
44    if i < b.len() && b[i] == '.' {
45        i += 1;
46        while i < b.len() && b[i].is_ascii_digit() { i += 1; digitos = true; }
47    }
48    if !digitos { return None; }
49    if i < b.len() && (b[i] == 'e' || b[i] == 'E') {
50        let guardado = i;
51        i += 1;
52        if i < b.len() && (b[i] == '+' || b[i] == '-') { i += 1; }
53        if i < b.len() && b[i].is_ascii_digit() {
54            while i < b.len() && b[i].is_ascii_digit() { i += 1; }
55        } else { i = guardado; }
56    }
57    if i != b.len() { return None; }
58    t.parse::<f64>().ok()
59}
60
61/// El prefijo numérico del texto, que es lo que usa la CONVERSIÓN explícita (`"12x"+0` es 12).
62/// Distinto de `como_numero`, que exige que no sobre nada: ahí `"12x"` no es un número.
63pub fn prefijo_numerico(s: &str) -> f64 {
64    let t = s.trim_start_matches(|c: char| c == ' ' || c == '\t' || c == '\n');
65    let b: Vec<char> = t.chars().collect();
66    let mut i = 0;
67    if i < b.len() && (b[i] == '+' || b[i] == '-') { i += 1; }
68    let mut fin = i;
69    while fin < b.len() && b[fin].is_ascii_digit() { fin += 1; }
70    if fin < b.len() && b[fin] == '.' {
71        fin += 1;
72        while fin < b.len() && b[fin].is_ascii_digit() { fin += 1; }
73    }
74    if fin < b.len() && (b[fin] == 'e' || b[fin] == 'E') {
75        let guardado = fin;
76        let mut j = fin + 1;
77        if j < b.len() && (b[j] == '+' || b[j] == '-') { j += 1; }
78        if j < b.len() && b[j].is_ascii_digit() {
79            while j < b.len() && b[j].is_ascii_digit() { j += 1; }
80            fin = j;
81        } else { fin = guardado; }
82    }
83    let t2: String = b[..fin].iter().collect();
84    t2.parse::<f64>().unwrap_or(0.0)
85}
86
87/// Formatea un número como lo hace awk al convertirlo a cadena.
88///
89/// La regla de POSIX: si el valor es **entero exacto**, se escribe como entero —`print 1/1` da `1`,
90/// no `1.000000`—; si no, con `CONVFMT` (por defecto `%.6g`) o con `OFMT` cuando lo imprime `print`.
91/// Ese «entero exacto» es por VALOR, no por tipo: `2.0` es entero y `1e17` también.
92pub fn numero_a_cadena(n: f64, fmt: &str) -> String {
93    if n.is_nan() { return "nan".into(); }
94    if n.is_infinite() { return if n > 0.0 { "inf".into() } else { "-inf".into() }; }
95    if n == n.trunc() && n.abs() < 1e17 {
96        let mut s = String::new();
97        let _ = write!(s, "{}", n as i64);
98        return s;
99    }
100    crate::formato::una_conversion(fmt, n)
101}
102
103impl Valor {
104    pub fn de_entrada(s: &str) -> Valor {
105        let n = como_numero(s);
106        Valor::StrNum(s.to_string(), n)
107    }
108
109    pub fn cadena(&self, convfmt: &str) -> String {
110        match self {
111            Valor::Num(n) => numero_a_cadena(*n, convfmt),
112            Valor::Cad(s) => s.clone(),
113            Valor::StrNum(s, _) => s.clone(),
114            Valor::Vacio => String::new(),
115        }
116    }
117
118    pub fn numero(&self) -> f64 {
119        match self {
120            Valor::Num(n) => *n,
121            Valor::Cad(s) => prefijo_numerico(s),
122            Valor::StrNum(_, Some(n)) => *n,
123            Valor::StrNum(s, None) => prefijo_numerico(s),
124            Valor::Vacio => 0.0,
125        }
126    }
127
128    /// Verdad de awk: un número es verdadero si no es 0; una CADENA, si no está vacía. Y un strnum
129    /// se juzga como número — `$1` valiendo `"0"` es FALSO, aunque la cadena no esté vacía.
130    pub fn verdad(&self) -> bool {
131        match self {
132            Valor::Num(n) => *n != 0.0,
133            Valor::Cad(s) => !s.is_empty(),
134            Valor::StrNum(s, n) => match n { Some(v) => *v != 0.0, None => !s.is_empty() },
135            Valor::Vacio => false,
136        }
137    }
138
139    /// ¿Se compara como número frente a otro? Sí si es número, si es un strnum que parece número, o
140    /// si nunca se asignó.
141    fn numerico(&self) -> bool {
142        matches!(self, Valor::Num(_) | Valor::Vacio | Valor::StrNum(_, Some(_)))
143    }
144
145    /// Comparación de awk: numérica si AMBOS lados lo admiten, de cadena en cuanto uno sea cadena.
146    pub fn comparar(&self, otro: &Valor, convfmt: &str) -> std::cmp::Ordering {
147        if self.numerico() && otro.numerico() {
148            let (a, b) = (self.numero(), otro.numero());
149            return a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal);
150        }
151        self.cadena(convfmt).cmp(&otro.cadena(convfmt))
152    }
153}
154
155#[cfg(test)]
156mod pruebas {
157    use super::*;
158
159    #[test]
160    fn strnum_compara_como_numero_y_la_cadena_no() {
161        let campo10 = Valor::de_entrada("10");
162        let campo9 = Valor::de_entrada("9");
163        assert_eq!(campo10.comparar(&campo9, "%.6g"), std::cmp::Ordering::Greater);
164        let cad10 = Valor::Cad("10".into());
165        let cad9 = Valor::Cad("9".into());
166        assert_eq!(cad10.comparar(&cad9, "%.6g"), std::cmp::Ordering::Less);
167    }
168
169    #[test]
170    fn un_campo_que_no_parece_numero_se_compara_como_cadena() {
171        let a = Valor::de_entrada("12x");
172        let b = Valor::de_entrada("9");
173        assert_eq!(a.comparar(&b, "%.6g"), std::cmp::Ordering::Less); // "12x" < "9"
174        assert_eq!(a.numero(), 12.0); // pero convertido a número es su prefijo
175    }
176
177    #[test]
178    fn el_texto_original_se_conserva() {
179        let v = Valor::de_entrada("0012");
180        assert_eq!(v.cadena("%.6g"), "0012");
181        assert_eq!(v.numero(), 12.0);
182    }
183
184    #[test]
185    fn enteros_exactos_no_llevan_decimales() {
186        assert_eq!(numero_a_cadena(1.0, "%.6g"), "1");
187        assert_eq!(numero_a_cadena(-0.0, "%.6g"), "0");
188        assert_eq!(numero_a_cadena(0.5, "%.6g"), "0.5");
189        assert_eq!(numero_a_cadena(1.0 / 3.0, "%.6g"), "0.333333");
190    }
191
192    #[test]
193    fn cero_en_un_campo_es_falso_aunque_la_cadena_no_este_vacia() {
194        assert!(!Valor::de_entrada("0").verdad());
195        assert!(Valor::Cad("0".into()).verdad());
196        assert!(!Valor::de_entrada("").verdad());
197    }
198
199    #[test]
200    fn que_es_numero_y_que_no() {
201        assert_eq!(como_numero(" 3.5e2 "), Some(350.0));
202        assert_eq!(como_numero("+.5"), Some(0.5));
203        assert_eq!(como_numero("12x"), None);
204        assert_eq!(como_numero("0x10"), None); // POSIX no admite hexadecimal
205        assert_eq!(como_numero(""), None);
206        assert_eq!(como_numero("."), None);
207    }
208}