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…
⎇ 83720b303431ffec520a4171b21b2da651a80d31
crates/akllay/src/formato.rs
Historia
Quién tocó qué
crudo
el awk se llama akllay: `qillqa` estaba TOMADO en tawasuyu, y no lo comprobé
1//! `printf`/`sprintf` de awk, y la conversión número→cadena que usan CONVFMT y OFMT.
2//!
3//! Se escribe a mano porque el `format!` de Rust no habla el mismo idioma: no tiene `%c`, no tiene
4//! el `*` que toma la anchura de un argumento, y su `{:e}` no produce lo que produce `%e` de C
5//! (`1e2` contra `1.000000e+02`). Y porque awk manda cosas que C no: `%c` con un número es el
6//! carácter de ese código, pero `%c` con una cadena es su PRIMER carácter.
7//!
8//! Los formatos admitidos son los de POSIX: `%d %i %o %u %x %X %c %s %e %E %f %F %g %G %a %A %%`,
9//! con banderas `-+ #0`, anchura, precisión y `*` en las dos.
10
11pub struct Campo {
12 pub izquierda: bool,
13 pub signo: bool,
14 pub espacio: bool,
15 pub alterno: bool,
16 pub cero: bool,
17 pub ancho: Option<usize>,
18 pub precision: Option<usize>,
19 pub conv: char,
20}
21
22/// Aplica un formato de UNA conversión a un número. Lo usa `numero_a_cadena` para CONVFMT/OFMT,
23/// donde el formato viene de una variable del programa y puede ser cualquier cosa.
24pub fn una_conversion(fmt: &str, n: f64) -> String {
25 let mut salida = String::new();
26 let cs: Vec<char> = fmt.chars().collect();
27 let mut i = 0;
28 while i < cs.len() {
29 if cs[i] != '%' { salida.push(cs[i]); i += 1; continue; }
30 if i + 1 < cs.len() && cs[i + 1] == '%' { salida.push('%'); i += 2; continue; }
31 let (campo, siguiente) = match analizar(&cs, i + 1, &mut Vec::new(), &mut 0) {
32 Some(x) => x,
33 None => { salida.push('%'); i += 1; continue; }
34 };
35 salida.push_str(&aplicar_numero(&campo, n));
36 i = siguiente;
37 // Sólo la PRIMERA conversión consume el número; el resto del formato se copia tal cual.
38 while i < cs.len() { salida.push(cs[i]); i += 1; }
39 }
40 salida
41}
42
43/// Lee un especificador tras el `%`. `args` y `pos` sirven para los `*`, que consumen argumentos.
44fn analizar(cs: &[char], mut i: usize, anchos: &mut Vec<i64>, _pos: &mut usize) -> Option<(Campo, usize)> {
45 let mut c = Campo {
46 izquierda: false, signo: false, espacio: false, alterno: false, cero: false,
47 ancho: None, precision: None, conv: 's',
48 };
49 loop {
50 match cs.get(i) {
51 Some('-') => c.izquierda = true,
52 Some('+') => c.signo = true,
53 Some(' ') => c.espacio = true,
54 Some('#') => c.alterno = true,
55 Some('0') => c.cero = true,
56 _ => break,
57 }
58 i += 1;
59 }
60 if cs.get(i) == Some(&'*') {
61 i += 1;
62 let v = if anchos.is_empty() { 0 } else { anchos.remove(0) };
63 if v < 0 { c.izquierda = true; c.ancho = Some((-v) as usize); } else { c.ancho = Some(v as usize); }
64 } else {
65 let mut n = String::new();
66 while matches!(cs.get(i), Some(d) if d.is_ascii_digit()) { n.push(cs[i]); i += 1; }
67 if !n.is_empty() { c.ancho = n.parse().ok(); }
68 }
69 if cs.get(i) == Some(&'.') {
70 i += 1;
71 if cs.get(i) == Some(&'*') {
72 i += 1;
73 let v = if anchos.is_empty() { 0 } else { anchos.remove(0) };
74 c.precision = Some(v.max(0) as usize);
75 } else {
76 let mut n = String::new();
77 while matches!(cs.get(i), Some(d) if d.is_ascii_digit()) { n.push(cs[i]); i += 1; }
78 c.precision = Some(n.parse().unwrap_or(0));
79 }
80 }
81 // Modificadores de longitud de C: se aceptan y se ignoran, que es lo que hacen los awk.
82 while matches!(cs.get(i), Some('h') | Some('l') | Some('L') | Some('q') | Some('j') | Some('z') | Some('t')) { i += 1; }
83 let conv = *cs.get(i)?;
84 c.conv = conv;
85 Some((c, i + 1))
86}
87
88fn rellenar(s: String, c: &Campo, numerico: bool) -> String {
89 let ancho = match c.ancho { None => return s, Some(a) => a };
90 let largo = s.chars().count();
91 if largo >= ancho { return s; }
92 let faltan = ancho - largo;
93 if c.izquierda { return s + &" ".repeat(faltan); }
94 if c.cero && numerico && c.precision.is_none() {
95 // El cero va DESPUÉS del signo: `%05d` de -1 es `-0001`, no `000-1`.
96 let (signo, resto) = match s.chars().next() {
97 Some(x) if x == '-' || x == '+' || x == ' ' => (s[..1].to_string(), s[1..].to_string()),
98 _ => (String::new(), s),
99 };
100 return signo + &"0".repeat(faltan) + &resto;
101 }
102 " ".repeat(faltan) + &s
103}
104
105fn con_signo(s: String, negativo: bool, c: &Campo) -> String {
106 if negativo { return s; }
107 if c.signo { return format!("+{s}"); }
108 if c.espacio { return format!(" {s}"); }
109 s
110}
111
112fn aplicar_numero(c: &Campo, n: f64) -> String {
113 let cuerpo = match c.conv {
114 'd' | 'i' => {
115 // awk trunca hacia cero, como C.
116 let v = if n.is_nan() { 0 } else { n.trunc() as i64 };
117 let mut s = v.abs().to_string();
118 if let Some(p) = c.precision { if s.len() < p { s = "0".repeat(p - s.len()) + &s; } }
119 let s = if v < 0 { format!("-{s}") } else { s };
120 con_signo(s, v < 0, c)
121 }
122 'o' | 'x' | 'X' | 'u' => {
123 // Los negativos se ven como unsigned de 64 bits, igual que C.
124 let v = if n.is_nan() { 0i64 } else { n.trunc() as i64 };
125 let u = v as u64;
126 let mut s = match c.conv {
127 'o' => format!("{u:o}"),
128 'x' => format!("{u:x}"),
129 'X' => format!("{u:X}"),
130 _ => format!("{u}"),
131 };
132 if let Some(p) = c.precision { if s.len() < p { s = "0".repeat(p - s.len()) + &s; } }
133 if c.alterno {
134 match c.conv {
135 'o' if !s.starts_with('0') => s = format!("0{s}"),
136 'x' if u != 0 => s = format!("0x{s}"),
137 'X' if u != 0 => s = format!("0X{s}"),
138 _ => {}
139 }
140 }
141 s
142 }
143 'c' => {
144 let cod = n.trunc() as u32;
145 char::from_u32(cod).map(|x| x.to_string()).unwrap_or_default()
146 }
147 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'a' | 'A' => {
148 let p = c.precision.unwrap_or(6);
149 let neg = n.is_sign_negative() && n != 0.0;
150 let a = n.abs();
151 let s = match c.conv {
152 'f' | 'F' => format!("{a:.p$}", p = p),
153 'e' | 'E' => exponencial(a, p, c.conv == 'E'),
154 'a' | 'A' => format!("{a}"), // hexadecimal de coma flotante: rarísimo en awk
155 _ => general(a, p, c.conv == 'G', c.alterno),
156 };
157 let s = if neg { format!("-{s}") } else { s };
158 con_signo(s, neg, c)
159 }
160 's' => String::new(),
161 otro => format!("%{otro}"),
162 };
163 rellenar(cuerpo, c, c.conv != 's' && c.conv != 'c')
164}
165
166fn exponencial(a: f64, p: usize, mayus: bool) -> String {
167 let s = format!("{:.*e}", p, a);
168 // Rust escribe `1.5e2`; C quiere `1.5e+02`, con al menos dos dígitos y signo siempre.
169 let (mant, exp) = match s.split_once('e') { Some(x) => x, None => return s };
170 let n: i32 = exp.parse().unwrap_or(0);
171 let e = if mayus { 'E' } else { 'e' };
172 format!("{mant}{e}{}{:02}", if n < 0 { '-' } else { '+' }, n.abs())
173}
174
175/// `%g`: el más corto entre `%e` y `%f`, y sin ceros a la derecha salvo con `#`.
176fn general(a: f64, p: usize, mayus: bool, alterno: bool) -> String {
177 let p = if p == 0 { 1 } else { p };
178 let exp = if a == 0.0 { 0 } else { a.abs().log10().floor() as i32 };
179 let usa_e = exp < -4 || exp >= p as i32;
180 let mut s = if usa_e {
181 exponencial(a, p - 1, mayus)
182 } else {
183 let dec = (p as i32 - 1 - exp).max(0) as usize;
184 format!("{a:.dec$}")
185 };
186 if !alterno && s.contains('.') {
187 // Quitar ceros de cola, y el punto si queda solo. Con exponente hay que respetar la parte
188 // de después de la `e`.
189 if let Some(pos_e) = s.find(|c| c == 'e' || c == 'E') {
190 let (m, e) = s.split_at(pos_e);
191 let m = m.trim_end_matches('0').trim_end_matches('.');
192 s = format!("{m}{e}");
193 } else {
194 s = s.trim_end_matches('0').trim_end_matches('.').to_string();
195 }
196 }
197 s
198}
199
200/// El `printf` completo: recorre el formato consumiendo argumentos.
201pub fn printf(fmt: &str, args: &[Arg], convfmt: &str) -> String {
202 let cs: Vec<char> = fmt.chars().collect();
203 let mut salida = String::new();
204 let mut i = 0;
205 let mut n_arg = 0;
206 // Los `*` consumen argumentos por delante; se resuelven al vuelo.
207 while i < cs.len() {
208 if cs[i] != '%' { salida.push(cs[i]); i += 1; continue; }
209 if cs.get(i + 1) == Some(&'%') { salida.push('%'); i += 2; continue; }
210 // Contar cuántos `*` trae este especificador para sacarlos de los argumentos.
211 let mut anchos: Vec<i64> = Vec::new();
212 {
213 let mut j = i + 1;
214 while matches!(cs.get(j), Some('-') | Some('+') | Some(' ') | Some('#') | Some('0')) { j += 1; }
215 if cs.get(j) == Some(&'*') {
216 anchos.push(args.get(n_arg).map(|a| a.numero() as i64).unwrap_or(0));
217 n_arg += 1;
218 j += 1;
219 } else { while matches!(cs.get(j), Some(d) if d.is_ascii_digit()) { j += 1; } }
220 if cs.get(j) == Some(&'.') {
221 j += 1;
222 if cs.get(j) == Some(&'*') {
223 anchos.push(args.get(n_arg).map(|a| a.numero() as i64).unwrap_or(0));
224 n_arg += 1;
225 }
226 }
227 }
228 let (campo, sig) = match analizar(&cs, i + 1, &mut anchos, &mut n_arg) {
229 Some(x) => x,
230 None => { salida.push('%'); i += 1; continue; }
231 };
232 i = sig;
233 let arg = args.get(n_arg);
234 n_arg += 1;
235 match campo.conv {
236 's' => {
237 let mut s = arg.map(|a| a.cadena(convfmt)).unwrap_or_default();
238 if let Some(p) = campo.precision { s = s.chars().take(p).collect(); }
239 salida.push_str(&rellenar(s, &campo, false));
240 }
241 'c' => {
242 // ⚠ `%c` mira el TIPO: con un número es el carácter de ese código; con una cadena,
243 // su primer carácter. Un `printf "%c", 65` da `A`, y `printf "%c", "65"` da `6`.
244 let s = match arg {
245 None => String::new(),
246 Some(a) => match a.caracter(convfmt) { Some(c) => c.to_string(), None => String::new() },
247 };
248 salida.push_str(&rellenar(s, &campo, false));
249 }
250 _ => {
251 let n = arg.map(|a| a.numero()).unwrap_or(0.0);
252 salida.push_str(&aplicar_numero(&campo, n));
253 }
254 }
255 }
256 salida
257}
258
259/// Lo que `printf` necesita saber de un argumento. Se define acá para que `formato` no dependa del
260/// intérprete entero.
261pub enum Arg {
262 Numero(f64),
263 Texto(String),
264 /// De la entrada: `%c` lo trata como cadena, `%d` como número.
265 Entrada(String, Option<f64>),
266}
267
268impl Arg {
269 pub fn numero(&self) -> f64 {
270 match self {
271 Arg::Numero(n) => *n,
272 Arg::Texto(s) => crate::valor::prefijo_numerico(s),
273 Arg::Entrada(s, n) => n.unwrap_or_else(|| crate::valor::prefijo_numerico(s)),
274 }
275 }
276 pub fn cadena(&self, convfmt: &str) -> String {
277 match self {
278 Arg::Numero(n) => crate::valor::numero_a_cadena(*n, convfmt),
279 Arg::Texto(s) => s.clone(),
280 Arg::Entrada(s, _) => s.clone(),
281 }
282 }
283 fn caracter(&self, convfmt: &str) -> Option<char> {
284 match self {
285 Arg::Numero(n) => char::from_u32(n.trunc() as u32),
286 Arg::Texto(s) => s.chars().next(),
287 // Un strnum con pinta de número se comporta como número.
288 Arg::Entrada(s, Some(n)) => { let _ = convfmt; char::from_u32(n.trunc() as u32).or_else(|| s.chars().next()) }
289 Arg::Entrada(s, None) => s.chars().next(),
290 }
291 }
292}
293
294#[cfg(test)]
295mod pruebas {
296 use super::*;
297
298 fn p(fmt: &str, args: Vec<Arg>) -> String { printf(fmt, &args, "%.6g") }
299
300 #[test]
301 fn enteros_con_ancho_y_signo() {
302 assert_eq!(p("%d", vec![Arg::Numero(42.0)]), "42");
303 assert_eq!(p("%5d|", vec![Arg::Numero(42.0)]), " 42|");
304 assert_eq!(p("%-5d|", vec![Arg::Numero(42.0)]), "42 |");
305 assert_eq!(p("%05d", vec![Arg::Numero(-1.0)]), "-0001"); // el cero va tras el signo
306 assert_eq!(p("%+d", vec![Arg::Numero(7.0)]), "+7");
307 assert_eq!(p("%d", vec![Arg::Numero(-3.9)]), "-3"); // trunca hacia cero
308 }
309
310 #[test]
311 fn coma_flotante() {
312 assert_eq!(p("%.2f", vec![Arg::Numero(3.14159)]), "3.14");
313 assert_eq!(p("%e", vec![Arg::Numero(150.0)]), "1.500000e+02");
314 assert_eq!(p("%g", vec![Arg::Numero(150.0)]), "150");
315 assert_eq!(p("%g", vec![Arg::Numero(0.0000123)]), "1.23e-05");
316 assert_eq!(p("%.3g", vec![Arg::Numero(3.14159)]), "3.14");
317 }
318
319 #[test]
320 fn porciento_c_mira_el_tipo() {
321 assert_eq!(p("%c", vec![Arg::Numero(65.0)]), "A");
322 assert_eq!(p("%c", vec![Arg::Texto("65".into())]), "6");
323 assert_eq!(p("%c", vec![Arg::Texto("héroe".into())]), "h");
324 }
325
326 #[test]
327 fn el_asterisco_toma_la_anchura_del_argumento() {
328 assert_eq!(p("%*d|", vec![Arg::Numero(5.0), Arg::Numero(42.0)]), " 42|");
329 assert_eq!(p("%.*f", vec![Arg::Numero(2.0), Arg::Numero(3.14159)]), "3.14");
330 }
331
332 #[test]
333 fn cadenas_con_precision() {
334 assert_eq!(p("%.3s", vec![Arg::Texto("abcdef".into())]), "abc");
335 assert_eq!(p("%10.3s|", vec![Arg::Texto("abcdef".into())]), " abc|");
336 }
337
338 #[test]
339 fn faltan_argumentos_no_es_error() {
340 assert_eq!(p("%s-%d", vec![]), "-0");
341 }
342
343 #[test]
344 fn hexadecimal_y_octal() {
345 assert_eq!(p("%x", vec![Arg::Numero(255.0)]), "ff");
346 assert_eq!(p("%#X", vec![Arg::Numero(255.0)]), "0XFF");
347 assert_eq!(p("%o", vec![Arg::Numero(8.0)]), "10");
348 }
349}