principiante⏱35 minutosLección 10 de 10
Proyecto Principiante — Conversor de Temperatura CLI
Construye un conversor de temperatura completo de línea de comandos con validación de entrada, manejo de errores y conversiones de unidad
Proyecto Principiante: Conversor de Temperatura CLI
Construye un conversor de temperatura de línea de comandos que maneja conversiones Fahrenheit ↔ Celsius con validación robusta de entrada.
Configuración del Proyecto
bash
cargo new conversor_temp
cd conversor_tempPaso 1: Entendiendo las Fórmulas
| Conversión | Fórmula |
|---|---|
| °F → °C | (f - 32) * 5/9 |
| °C → °F | (c * 9/5) + 32 |
rust
fn fahrenheit_a_celsius(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
fn celsius_a_fahrenheit(c: f64) -> f64 {
(c * 9.0 / 5.0) + 32.0
}
#[test]
fn test_conversiones() {
assert!((fahrenheit_a_celsius(32.0) - 0.0).abs() < f64::EPSILON);
assert!((celsius_a_fahrenheit(0.0) - 32.0).abs() < f64::EPSILON);
assert!((fahrenheit_a_celsius(212.0) - 100.0).abs() < f64::EPSILON);
assert!((celsius_a_fahrenheit(100.0) - 212.0).abs() < f64::EPSILON);
}Note
Usamos (a - b).abs() < f64::EPSILON para comparación de floats porque == en floats no es confiable debido a la precisión.
Paso 2: Analizando Entrada del Usuario
rust
use std::io::{self, Write};
fn leer_linea() -> String {
let mut entrada = String::new();
io::stdin().read_line(&mut entrada).expect("Fallo al leer línea");
entrada.trim().to_string()
}
fn analizar_temperatura(entrada: &str) -> Result<f64, String> {
entrada.trim().parse::<f64>()
.map_err(|_| format!("'{entrada}' no es un número válido"))
}
fn analizar_unidad(entrada: &str) -> Result<&str, String> {
match entrada.trim().to_lowercase().as_str() {
"c" | "celsius" => Ok("C"),
"f" | "fahrenheit" => Ok("F"),
_ => Err(format!("'{entrada}' no es una unidad válida. Usa 'C' o 'F'")),
}
}Paso 3: Construyendo la Aplicación
rust
fn ejecutar_conversor() -> Result<(), String> {
println!("=== Conversor de Temperatura ===");
print!("Ingresa temperatura: ");
io::stdout().flush().map_err(|e| e.to_string())?;
let entrada_temp = leer_linea();
let temperatura = analizar_temperatura(&entrada_temp)?;
print!("Convertir a (C/F): ");
io::stdout().flush().map_err(|e| e.to_string())?;
let entrada_unid = leer_linea();
let unidad_destino = analizar_unidad(&entrada_unid)?;
match unidad_destino {
"C" => {
let resultado = fahrenheit_a_celsius(temperatura);
println!("{temperatura}°F = {resultado:.1}°C");
}
"F" => {
let resultado = celsius_a_fahrenheit(temperatura);
println!("{temperatura}°C = {resultado:.1}°F");
}
_ => unreachable!(), // analizar_unidad ya validó
}
Ok(())
}
fn main() {
if let Err(e) = ejecutar_conversor() {
eprintln!("Error: {e}");
std::process::exit(1);
}
}Success
Usar Result como tipo de retorno nos permite usar el operador ? para propagación limpia de errores. La función main maneja el caso Err externo.
Paso 4: Agregando Validación de Entrada
rust
fn validar_temperatura(valor: f64, unidad: &str) -> Result<(), String> {
match unidad {
"C" if valor < -273.15 => {
Err(format!("{valor}°C está bajo cero absoluto (-273.15°C)"))
}
"F" if valor < -459.67 => {
Err(format!("{valor}°F está bajo cero absoluto (-459.67°F)"))
}
_ => Ok(()),
}
}
fn ejecutar_conversor_validado() -> Result<(), String> {
println!("=== Conversor de Temperatura ===");
print!("Ingresa temperatura: ");
io::stdout().flush().map_err(|e| e.to_string())?;
let temperatura = analizar_temperatura(&leer_linea())?;
print!("Ingresa unidad (C/F): ");
io::stdout().flush().map_err(|e| e.to_string())?;
let unidad = analizar_unidad(&leer_linea())?;
validar_temperatura(temperatura, unidad)?;
match unidad {
"C" => {
let resultado = celsius_a_fahrenheit(temperatura);
println!("{temperatura:.1}°C = {resultado:.1}°F");
}
"F" => {
let resultado = fahrenheit_a_celsius(temperatura);
println!("{temperatura:.1}°F = {resultado:.1}°C");
}
_ => unreachable!(),
}
Ok(())
}Paso 5: Aplicación Completa
Aquí está el programa completo:
rust
use std::io::{self, Write};
fn fahrenheit_a_celsius(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
fn celsius_a_fahrenheit(c: f64) -> f64 {
(c * 9.0 / 5.0) + 32.0
}
fn leer_linea() -> String {
let mut entrada = String::new();
io::stdin().read_line(&mut entrada).expect("Fallo al leer línea");
entrada.trim().to_string()
}
fn analizar_temperatura(entrada: &str) -> Result<f64, String> {
entrada.parse::<f64>()
.map_err(|_| format!("'{entrada}' no es un número válido"))
}
fn analizar_unidad(entrada: &str) -> Result<&str, String> {
match entrada.trim().to_lowercase().as_str() {
"c" | "celsius" => Ok("C"),
"f" | "fahrenheit" => Ok("F"),
_ => Err(format!("'{entrada}' no es una unidad válida. Usa 'C' o 'F'")),
}
}
fn validar_temperatura(valor: f64, unidad: &str) -> Result<(), String> {
match unidad {
"C" if valor < -273.15 => {
Err(format!("{valor}°C está bajo cero absoluto"))
}
"F" if valor < -459.67 => {
Err(format!("{valor}°F está bajo cero absoluto"))
}
_ => Ok(()),
}
}
fn ejecutar_conversor() -> Result<(), String> {
println!("=== Conversor de Temperatura ===");
print!("Ingresa temperatura: ");
io::stdout().flush().map_err(|e| e.to_string())?;
let temperatura = analizar_temperatura(&leer_linea())?;
print!("Ingresa unidad (C/F): ");
io::stdout().flush().map_err(|e| e.to_string())?;
let unidad = analizar_unidad(&leer_linea())?;
validar_temperatura(temperatura, unidad)?;
match (unidad, temperatura) {
("C", c) => {
let f = celsius_a_fahrenheit(c);
println!("{c:.1}°C = {f:.1}°F");
}
("F", f) => {
let c = fahrenheit_a_celsius(f);
println!("{f:.1}°F = {c:.1}°C");
}
_ => unreachable!(),
}
Ok(())
}
fn main() {
if let Err(e) = ejecutar_conversor() {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fahrenheit_a_celsius() {
assert!((fahrenheit_a_celsius(32.0) - 0.0).abs() < f64::EPSILON);
assert!((fahrenheit_a_celsius(212.0) - 100.0).abs() < f64::EPSILON);
assert!((fahrenheit_a_celsius(-40.0) - (-40.0)).abs() < f64::EPSILON);
}
#[test]
fn test_celsius_a_fahrenheit() {
assert!((celsius_a_fahrenheit(0.0) - 32.0).abs() < f64::EPSILON);
assert!((celsius_a_fahrenheit(100.0) - 212.0).abs() < f64::EPSILON);
assert!((celsius_a_fahrenheit(-40.0) - (-40.0)).abs() < f64::EPSILON);
}
#[test]
fn test_analizar_temperatura() {
assert_eq!(analizar_temperatura("25.5"), Ok(25.5));
assert!(analizar_temperatura("abc").is_err());
assert!(analizar_temperatura("").is_err());
}
#[test]
fn test_analizar_unidad() {
assert_eq!(analizar_unidad("C"), Ok("C"));
assert_eq!(analizar_unidad("fahrenheit"), Ok("F"));
assert!(analizar_unidad("K").is_err());
}
#[test]
fn test_validar_temperatura() {
assert!(validar_temperatura(25.0, "C").is_ok());
assert!(validar_temperatura(-300.0, "C").is_err());
assert!(validar_temperatura(-500.0, "F").is_err());
assert!(validar_temperatura(100.0, "F").is_ok());
}
}Paso 6: Ejecutando y Probando
bash
# Ejecutar en modo release
cargo run --release
# Ejecutar pruebas
cargo test
# Ejemplo de salida:
# === Conversor de Temperatura ===
# Ingresa temperatura: 100
# Ingresa unidad (C/F): F
# 100.0°C = 212.0°F
# Caso de error:
# === Conversor de Temperatura ===
# Ingresa temperatura: abc
# Ingresa unidad (C/F): F
# Error: 'abc' no es un número válidoWarning
Siempre verifica f64::NAN y f64::INFINITY en aplicaciones reales. Los usuarios pueden ingresar estos valores y causar comportamiento inesperado.
Extensiones
Prueba estas mejoras:
- Bucle hasta salir — Sigue convirtiendo hasta que el usuario escriba "q"
- Soporte Kelvin — Agrega unidad Kelvin
- Salida más bonita — Usa salida coloreada con crate
colored - Conversión por lotes — Lee múltiples valores de un archivo (
--file entrada.txt) - Indicador de precisión —
--precision 4controla decimales - Argumentos CLI — Usa crate
clap:conversor_temp 100 C to F
rust
// Extensión: bucle hasta salir
fn main() {
loop {
println!("\n=== Conversor de Temperatura (q para salir) ===");
print!("Ingresa temperatura: ");
io::stdout().flush().unwrap();
let entrada = leer_linea();
if entrada.to_lowercase() == "q" {
break;
}
match analizar_temperatura(&entrada) {
Ok(temp) => {
print!("Ingresa unidad (C/F): ");
io::stdout().flush().unwrap();
let entrada_unid = leer_linea();
match analizar_unidad(&entrada_unid) {
Ok(unidad) => {
if let Err(e) = validar_temperatura(temp, unidad) {
eprintln!("Error: {e}");
continue;
}
match unidad {
"C" => println!("{temp:.1}°C = {:.1}°F", celsius_a_fahrenheit(temp)),
"F" => println!("{temp:.1}°F = {:.1}°C", fahrenheit_a_celsius(temp)),
_ => unreachable!(),
}
}
Err(e) => eprintln!("Error: {e}"),
}
}
Err(e) => eprintln!("Error: {e}"),
}
}
}Preguntas de Práctica
- ¿Qué hace
io::stdout().flush()y por qué es necesario? - ¿Por qué usamos
f64en lugar def32para temperatura? - ¿Qué hace el operador
?en el código del conversor? - ¿Cómo maneja
parse::<f64>()la entrada inválida? - ¿Por qué no podemos usar
==para comparar resultados float? - ¿Cuál es el propósito de la función
validar_temperatura? - ¿Cómo agregarías conversión Kelvin al programa?
- ¿Por qué la función
ejecutar_conversorestá separada demain? - ¿Qué hace
std::process::exit(1)? - ¿Cómo escribirías una prueba para el análisis de unidad sensible a mayúsculas?
Progreso100%