Beginner Projects
Temperature Converter
A temperature converter parses temperature measurements across Celsius (C), Fahrenheit (F), and Kelvin (K), validates physical lower bounds (Absolute Zero: $0\text{ K} = -273.15^\circ\text{C} = -459.67^\circ\text{F}$), and converts accurately between scales using Kelvin as the canonical intermediate representation.
Canonical Intermediate Conversion Pattern (N Scales = 2N Functions Instead of N*(N-1)!):
Celsius (`C`) ---[ C + 273.15 ]---> ---[ K - 273.15 ]---> Celsius (`C`)
Fahrenheit (`F`) ---[ (F+459.67)*5/9 ]-> [ Kelvin ] ---[ K*9/5 - 459.67 ]-> Fahrenheit (`F`)
Kelvin (`K`) ---[ Identity ]-------> (`>= 0K`) ---[ Identity ]-------> Kelvin (`K`)
Scale Formulas and Physical Bounds
| Scale | Symbol | Absolute Zero Minimum | To Kelvin (to_kelvin) |
From Kelvin (from_kelvin) |
|---|---|---|---|---|
| Celsius | °C |
$-273.15^\circ\text{C}$ | $K = C + 273.15$ | $C = K - 273.15$ |
| Fahrenheit | °F |
$-459.67^\circ\text{F}$ | $K = (F - 32) \times \frac{5}{9} + 273.15$ | $F = (K - 273.15) \times \frac{9}{5} + 32$ |
| Kelvin | K |
$0.0\text{ K}$ | $K = K$ | $K = K$ |
Complete Runnable Implementation
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scale {
Celsius,
Fahrenheit,
Kelvin,
}
impl Scale {
pub const fn symbol(self) -> &'static str {
match self {
Scale::Celsius => "°C",
Scale::Fahrenheit => "°F",
Scale::Kelvin => "K",
}
}
}
#[derive(Debug, PartialEq)]
pub enum TempError {
EmptyInput,
InvalidFormat(String),
UnknownScale(String),
BelowAbsoluteZero { value: f64, scale: Scale },
NonFiniteValue,
}
impl fmt::Display for TempError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TempError::EmptyInput => write!(f, "empty temperature input"),
TempError::InvalidFormat(s) => write!(f, "invalid temperature format: '{s}'"),
TempError::UnknownScale(u) => write!(f, "unknown temperature unit: '{u}'"),
TempError::BelowAbsoluteZero { value, scale } => write!(
f,
"{value:.2}{} is below absolute zero (0 K)",
scale.symbol()
),
TempError::NonFiniteValue => write!(f, "temperature must be a finite number"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Temperature {
kelvin: f64, // Canonical internal representation (`kelvin >= 0.0`)
scale: Scale,
}
impl Temperature {
pub fn new(value: f64, scale: Scale) -> Result<Self, TempError> {
if !value.is_finite() {
return Err(TempError::NonFiniteValue);
}
let kelvin = match scale {
Scale::Celsius => value + 273.15,
Scale::Fahrenheit => (value - 32.0) * (5.0 / 9.0) + 273.15,
Scale::Kelvin => value,
};
// Allow tiny IEEE 754 rounding tolerance (-1e-9) at exact absolute zero:
if kelvin < -1e-9 {
return Err(TempError::BelowAbsoluteZero { value, scale });
}
Ok(Self {
kelvin: kelvin.max(0.0),
scale,
})
}
pub const fn convert_to(self, target: Scale) -> Self {
Self {
kelvin: self.kelvin,
scale: target,
}
}
pub fn value(self) -> f64 {
match self.scale {
Scale::Celsius => self.kelvin - 273.15,
Scale::Fahrenheit => (self.kelvin - 273.15) * (9.0 / 5.0) + 32.0,
Scale::Kelvin => self.kelvin,
}
}
pub const fn scale(self) -> Scale {
self.scale
}
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.2}{}", self.value(), self.scale.symbol())
}
}
/// Parses strings such as `"100C"`, `"-40 °F"`, or `"300 K"` into a validated `Temperature`.
impl FromStr for Temperature {
type Err = TempError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(TempError::EmptyInput);
}
// Find the split point where the numeric prefix ends and the unit suffix begins:
let unit_start = trimmed
.find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+')
.ok_or_else(|| TempError::InvalidFormat(trimmed.to_string()))?;
let (num_part, unit_part) = trimmed.split_at(unit_start);
let value = num_part
.trim()
.parse::<f64>()
.map_err(|_| TempError::InvalidFormat(trimmed.to_string()))?;
let unit_clean = unit_part.trim().trim_start_matches('°').to_ascii_uppercase();
let scale = match unit_clean.as_str() {
"C" | "CELSIUS" => Scale::Celsius,
"F" | "FAHRENHEIT" => Scale::Fahrenheit,
"K" | "KELVIN" => Scale::Kelvin,
_ => return Err(TempError::UnknownScale(unit_part.trim().to_string())),
};
Self::new(value, scale)
}
}
fn main() {
let boiling: Temperature = "100 °C".parse().unwrap();
let in_f = boiling.convert_to(Scale::Fahrenheit);
let in_k = boiling.convert_to(Scale::Kelvin);
assert!((in_f.value() - 212.0).abs() < 1e-6);
assert!((in_k.value() - 373.15).abs() < 1e-6);
println!("{boiling} = {in_f} = {in_k}");
// Verify -40°C == -40°F crossover point:
let minus_40_c: Temperature = "-40C".parse().unwrap();
println!("{minus_40_c} in Fahrenheit = {}", minus_40_c.convert_to(Scale::Fahrenheit));
// Verify temperatures below Absolute Zero (-300°C) are rejected:
let impossible = "-300C".parse::<Temperature>();
println!("Parsing '-300C' -> {}", impossible.unwrap_err());
}
100.00°C = 212.00°F = 373.15K
-40.00°C in Fahrenheit = -40.00°F
Parsing '-300C' -> -300.00°C is below absolute zero (0 K)