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…
el awk se llama akllay: `qillqa` estaba TOMADO en tawasuyu, y no lo comprobé
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}