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 188Rust 55Shell 30Markdown 281211 Definitions

⎇ a5ad955459a91c70830dc6ee466bff6a78f9004e crates/qillqa/src/formato.rs Rust · 349 lines · 13.7 KB History Who touched what raw

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

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}