//! `printf`/`sprintf` de awk, y la conversión número→cadena que usan CONVFMT y OFMT. //! //! Se escribe a mano porque el `format!` de Rust no habla el mismo idioma: no tiene `%c`, no tiene //! el `*` que toma la anchura de un argumento, y su `{:e}` no produce lo que produce `%e` de C //! (`1e2` contra `1.000000e+02`). Y porque awk manda cosas que C no: `%c` con un número es el //! carácter de ese código, pero `%c` con una cadena es su PRIMER carácter. //! //! Los formatos admitidos son los de POSIX: `%d %i %o %u %x %X %c %s %e %E %f %F %g %G %a %A %%`, //! con banderas `-+ #0`, anchura, precisión y `*` en las dos. pub struct Campo { pub izquierda: bool, pub signo: bool, pub espacio: bool, pub alterno: bool, pub cero: bool, pub ancho: Option, pub precision: Option, pub conv: char, } /// Aplica un formato de UNA conversión a un número. Lo usa `numero_a_cadena` para CONVFMT/OFMT, /// donde el formato viene de una variable del programa y puede ser cualquier cosa. pub fn una_conversion(fmt: &str, n: f64) -> String { let mut salida = String::new(); let cs: Vec = fmt.chars().collect(); let mut i = 0; while i < cs.len() { if cs[i] != '%' { salida.push(cs[i]); i += 1; continue; } if i + 1 < cs.len() && cs[i + 1] == '%' { salida.push('%'); i += 2; continue; } let (campo, siguiente) = match analizar(&cs, i + 1, &mut Vec::new(), &mut 0) { Some(x) => x, None => { salida.push('%'); i += 1; continue; } }; salida.push_str(&aplicar_numero(&campo, n)); i = siguiente; // Sólo la PRIMERA conversión consume el número; el resto del formato se copia tal cual. while i < cs.len() { salida.push(cs[i]); i += 1; } } salida } /// Lee un especificador tras el `%`. `args` y `pos` sirven para los `*`, que consumen argumentos. fn analizar(cs: &[char], mut i: usize, anchos: &mut Vec, _pos: &mut usize) -> Option<(Campo, usize)> { let mut c = Campo { izquierda: false, signo: false, espacio: false, alterno: false, cero: false, ancho: None, precision: None, conv: 's', }; loop { match cs.get(i) { Some('-') => c.izquierda = true, Some('+') => c.signo = true, Some(' ') => c.espacio = true, Some('#') => c.alterno = true, Some('0') => c.cero = true, _ => break, } i += 1; } if cs.get(i) == Some(&'*') { i += 1; let v = if anchos.is_empty() { 0 } else { anchos.remove(0) }; if v < 0 { c.izquierda = true; c.ancho = Some((-v) as usize); } else { c.ancho = Some(v as usize); } } else { let mut n = String::new(); while matches!(cs.get(i), Some(d) if d.is_ascii_digit()) { n.push(cs[i]); i += 1; } if !n.is_empty() { c.ancho = n.parse().ok(); } } if cs.get(i) == Some(&'.') { i += 1; if cs.get(i) == Some(&'*') { i += 1; let v = if anchos.is_empty() { 0 } else { anchos.remove(0) }; c.precision = Some(v.max(0) as usize); } else { let mut n = String::new(); while matches!(cs.get(i), Some(d) if d.is_ascii_digit()) { n.push(cs[i]); i += 1; } c.precision = Some(n.parse().unwrap_or(0)); } } // Modificadores de longitud de C: se aceptan y se ignoran, que es lo que hacen los awk. while matches!(cs.get(i), Some('h') | Some('l') | Some('L') | Some('q') | Some('j') | Some('z') | Some('t')) { i += 1; } let conv = *cs.get(i)?; c.conv = conv; Some((c, i + 1)) } fn rellenar(s: String, c: &Campo, numerico: bool) -> String { let ancho = match c.ancho { None => return s, Some(a) => a }; let largo = s.chars().count(); if largo >= ancho { return s; } let faltan = ancho - largo; if c.izquierda { return s + &" ".repeat(faltan); } if c.cero && numerico && c.precision.is_none() { // El cero va DESPUÉS del signo: `%05d` de -1 es `-0001`, no `000-1`. let (signo, resto) = match s.chars().next() { Some(x) if x == '-' || x == '+' || x == ' ' => (s[..1].to_string(), s[1..].to_string()), _ => (String::new(), s), }; return signo + &"0".repeat(faltan) + &resto; } " ".repeat(faltan) + &s } fn con_signo(s: String, negativo: bool, c: &Campo) -> String { if negativo { return s; } if c.signo { return format!("+{s}"); } if c.espacio { return format!(" {s}"); } s } fn aplicar_numero(c: &Campo, n: f64) -> String { let cuerpo = match c.conv { 'd' | 'i' => { // awk trunca hacia cero, como C. let v = if n.is_nan() { 0 } else { n.trunc() as i64 }; let mut s = v.abs().to_string(); if let Some(p) = c.precision { if s.len() < p { s = "0".repeat(p - s.len()) + &s; } } let s = if v < 0 { format!("-{s}") } else { s }; con_signo(s, v < 0, c) } 'o' | 'x' | 'X' | 'u' => { // Los negativos se ven como unsigned de 64 bits, igual que C. let v = if n.is_nan() { 0i64 } else { n.trunc() as i64 }; let u = v as u64; let mut s = match c.conv { 'o' => format!("{u:o}"), 'x' => format!("{u:x}"), 'X' => format!("{u:X}"), _ => format!("{u}"), }; if let Some(p) = c.precision { if s.len() < p { s = "0".repeat(p - s.len()) + &s; } } if c.alterno { match c.conv { 'o' if !s.starts_with('0') => s = format!("0{s}"), 'x' if u != 0 => s = format!("0x{s}"), 'X' if u != 0 => s = format!("0X{s}"), _ => {} } } s } 'c' => { let cod = n.trunc() as u32; char::from_u32(cod).map(|x| x.to_string()).unwrap_or_default() } 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'a' | 'A' => { let p = c.precision.unwrap_or(6); let neg = n.is_sign_negative() && n != 0.0; let a = n.abs(); let s = match c.conv { 'f' | 'F' => format!("{a:.p$}", p = p), 'e' | 'E' => exponencial(a, p, c.conv == 'E'), 'a' | 'A' => format!("{a}"), // hexadecimal de coma flotante: rarísimo en awk _ => general(a, p, c.conv == 'G', c.alterno), }; let s = if neg { format!("-{s}") } else { s }; con_signo(s, neg, c) } 's' => String::new(), otro => format!("%{otro}"), }; rellenar(cuerpo, c, c.conv != 's' && c.conv != 'c') } fn exponencial(a: f64, p: usize, mayus: bool) -> String { let s = format!("{:.*e}", p, a); // Rust escribe `1.5e2`; C quiere `1.5e+02`, con al menos dos dígitos y signo siempre. let (mant, exp) = match s.split_once('e') { Some(x) => x, None => return s }; let n: i32 = exp.parse().unwrap_or(0); let e = if mayus { 'E' } else { 'e' }; format!("{mant}{e}{}{:02}", if n < 0 { '-' } else { '+' }, n.abs()) } /// `%g`: el más corto entre `%e` y `%f`, y sin ceros a la derecha salvo con `#`. fn general(a: f64, p: usize, mayus: bool, alterno: bool) -> String { let p = if p == 0 { 1 } else { p }; let exp = if a == 0.0 { 0 } else { a.abs().log10().floor() as i32 }; let usa_e = exp < -4 || exp >= p as i32; let mut s = if usa_e { exponencial(a, p - 1, mayus) } else { let dec = (p as i32 - 1 - exp).max(0) as usize; format!("{a:.dec$}") }; if !alterno && s.contains('.') { // Quitar ceros de cola, y el punto si queda solo. Con exponente hay que respetar la parte // de después de la `e`. if let Some(pos_e) = s.find(|c| c == 'e' || c == 'E') { let (m, e) = s.split_at(pos_e); let m = m.trim_end_matches('0').trim_end_matches('.'); s = format!("{m}{e}"); } else { s = s.trim_end_matches('0').trim_end_matches('.').to_string(); } } s } /// El `printf` completo: recorre el formato consumiendo argumentos. pub fn printf(fmt: &str, args: &[Arg], convfmt: &str) -> String { let cs: Vec = fmt.chars().collect(); let mut salida = String::new(); let mut i = 0; let mut n_arg = 0; // Los `*` consumen argumentos por delante; se resuelven al vuelo. while i < cs.len() { if cs[i] != '%' { salida.push(cs[i]); i += 1; continue; } if cs.get(i + 1) == Some(&'%') { salida.push('%'); i += 2; continue; } // Contar cuántos `*` trae este especificador para sacarlos de los argumentos. let mut anchos: Vec = Vec::new(); { let mut j = i + 1; while matches!(cs.get(j), Some('-') | Some('+') | Some(' ') | Some('#') | Some('0')) { j += 1; } if cs.get(j) == Some(&'*') { anchos.push(args.get(n_arg).map(|a| a.numero() as i64).unwrap_or(0)); n_arg += 1; j += 1; } else { while matches!(cs.get(j), Some(d) if d.is_ascii_digit()) { j += 1; } } if cs.get(j) == Some(&'.') { j += 1; if cs.get(j) == Some(&'*') { anchos.push(args.get(n_arg).map(|a| a.numero() as i64).unwrap_or(0)); n_arg += 1; } } } let (campo, sig) = match analizar(&cs, i + 1, &mut anchos, &mut n_arg) { Some(x) => x, None => { salida.push('%'); i += 1; continue; } }; i = sig; let arg = args.get(n_arg); n_arg += 1; match campo.conv { 's' => { let mut s = arg.map(|a| a.cadena(convfmt)).unwrap_or_default(); if let Some(p) = campo.precision { s = s.chars().take(p).collect(); } salida.push_str(&rellenar(s, &campo, false)); } 'c' => { // ⚠ `%c` mira el TIPO: con un número es el carácter de ese código; con una cadena, // su primer carácter. Un `printf "%c", 65` da `A`, y `printf "%c", "65"` da `6`. let s = match arg { None => String::new(), Some(a) => match a.caracter(convfmt) { Some(c) => c.to_string(), None => String::new() }, }; salida.push_str(&rellenar(s, &campo, false)); } _ => { let n = arg.map(|a| a.numero()).unwrap_or(0.0); salida.push_str(&aplicar_numero(&campo, n)); } } } salida } /// Lo que `printf` necesita saber de un argumento. Se define acá para que `formato` no dependa del /// intérprete entero. pub enum Arg { Numero(f64), Texto(String), /// De la entrada: `%c` lo trata como cadena, `%d` como número. Entrada(String, Option), } impl Arg { pub fn numero(&self) -> f64 { match self { Arg::Numero(n) => *n, Arg::Texto(s) => crate::valor::prefijo_numerico(s), Arg::Entrada(s, n) => n.unwrap_or_else(|| crate::valor::prefijo_numerico(s)), } } pub fn cadena(&self, convfmt: &str) -> String { match self { Arg::Numero(n) => crate::valor::numero_a_cadena(*n, convfmt), Arg::Texto(s) => s.clone(), Arg::Entrada(s, _) => s.clone(), } } fn caracter(&self, convfmt: &str) -> Option { match self { Arg::Numero(n) => char::from_u32(n.trunc() as u32), Arg::Texto(s) => s.chars().next(), // Un strnum con pinta de número se comporta como número. Arg::Entrada(s, Some(n)) => { let _ = convfmt; char::from_u32(n.trunc() as u32).or_else(|| s.chars().next()) } Arg::Entrada(s, None) => s.chars().next(), } } } #[cfg(test)] mod pruebas { use super::*; fn p(fmt: &str, args: Vec) -> String { printf(fmt, &args, "%.6g") } #[test] fn enteros_con_ancho_y_signo() { assert_eq!(p("%d", vec![Arg::Numero(42.0)]), "42"); assert_eq!(p("%5d|", vec![Arg::Numero(42.0)]), " 42|"); assert_eq!(p("%-5d|", vec![Arg::Numero(42.0)]), "42 |"); assert_eq!(p("%05d", vec![Arg::Numero(-1.0)]), "-0001"); // el cero va tras el signo assert_eq!(p("%+d", vec![Arg::Numero(7.0)]), "+7"); assert_eq!(p("%d", vec![Arg::Numero(-3.9)]), "-3"); // trunca hacia cero } #[test] fn coma_flotante() { assert_eq!(p("%.2f", vec![Arg::Numero(3.14159)]), "3.14"); assert_eq!(p("%e", vec![Arg::Numero(150.0)]), "1.500000e+02"); assert_eq!(p("%g", vec![Arg::Numero(150.0)]), "150"); assert_eq!(p("%g", vec![Arg::Numero(0.0000123)]), "1.23e-05"); assert_eq!(p("%.3g", vec![Arg::Numero(3.14159)]), "3.14"); } #[test] fn porciento_c_mira_el_tipo() { assert_eq!(p("%c", vec![Arg::Numero(65.0)]), "A"); assert_eq!(p("%c", vec![Arg::Texto("65".into())]), "6"); assert_eq!(p("%c", vec![Arg::Texto("héroe".into())]), "h"); } #[test] fn el_asterisco_toma_la_anchura_del_argumento() { assert_eq!(p("%*d|", vec![Arg::Numero(5.0), Arg::Numero(42.0)]), " 42|"); assert_eq!(p("%.*f", vec![Arg::Numero(2.0), Arg::Numero(3.14159)]), "3.14"); } #[test] fn cadenas_con_precision() { assert_eq!(p("%.3s", vec![Arg::Texto("abcdef".into())]), "abc"); assert_eq!(p("%10.3s|", vec![Arg::Texto("abcdef".into())]), " abc|"); } #[test] fn faltan_argumentos_no_es_error() { assert_eq!(p("%s-%d", vec![]), "-0"); } #[test] fn hexadecimal_y_octal() { assert_eq!(p("%x", vec![Arg::Numero(255.0)]), "ff"); assert_eq!(p("%#X", vec![Arg::Numero(255.0)]), "0XFF"); assert_eq!(p("%o", vec![Arg::Numero(8.0)]), "10"); } }