Beginner Projects

Unit Conversion Tool

A multi-domain unit conversion tool converts measurements across different physical dimensions—such as Length, Mass, Data Storage, and Time—while enforcing dimensional analysis so incompatible conversions (such as converting Kilograms to Meters) are rejected with a clear dimensional mismatch error.

Dimensional Base-Unit Conversion Architecture:

  Source Quantity `(value, from_unit)`
        |
        v  1. Check `from_unit.dimension() == to_unit.dimension()`
        |     If mismatch (e.g., `Length != Mass`) ---> `Err(DimensionMismatch)`!
        |
        v  2. Convert to Base SI Unit (`base = value * from_unit.to_base_factor()`)
        |     - Length Base:       Meter (`m`)
        |     - Mass Base:         Gram (`g`)
        |     - Data Storage Base: Byte (`B`)
        |
        v  3. Convert from Base SI Unit (`result = base / to_unit.to_base_factor()`)

Supported Dimensions and Base Conversion Factors

Dimension Base Unit (factor = 1.0) Supported Units & Exact Base Factors
Length Meter (m) mm ($0.001$), cm ($0.01$), m ($1.0$), km ($1000.0$), in ($0.0254$), ft ($0.3048$), mi ($1609.344$)
Mass Gram (g) mg ($0.001$), g ($1.0$), kg ($1000.0$), oz ($28.349523125$), lb ($453.59237$)
Data Byte (B) B ($1.0$), KiB ($1024.0$), MiB ($1048576.0$), GiB ($1073741824.0$)

Complete Runnable Implementation

use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dimension {
    Length,
    Mass,
    DataSize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unit {
    // Length
    Millimeter,
    Centimeter,
    Meter,
    Kilometer,
    Inch,
    Foot,
    Mile,
    // Mass
    Milligram,
    Gram,
    Kilogram,
    Ounce,
    Pound,
    // DataSize
    Byte,
    Kibibyte,
    Mebibyte,
    Gibibyte,
}

impl Unit {
    pub fn parse(symbol: &str) -> Option<Self> {
        match symbol.trim() {
            "mm" => Some(Unit::Millimeter),
            "cm" => Some(Unit::Centimeter),
            "m" => Some(Unit::Meter),
            "km" => Some(Unit::Kilometer),
            "in" => Some(Unit::Inch),
            "ft" => Some(Unit::Foot),
            "mi" => Some(Unit::Mile),
            "mg" => Some(Unit::Milligram),
            "g" => Some(Unit::Gram),
            "kg" => Some(Unit::Kilogram),
            "oz" => Some(Unit::Ounce),
            "lb" => Some(Unit::Pound),
            "B" => Some(Unit::Byte),
            "KiB" => Some(Unit::Kibibyte),
            "MiB" => Some(Unit::Mebibyte),
            "GiB" => Some(Unit::Gibibyte),
            _ => None,
        }
    }

    pub const fn dimension(self) -> Dimension {
        match self {
            Unit::Millimeter
            | Unit::Centimeter
            | Unit::Meter
            | Unit::Kilometer
            | Unit::Inch
            | Unit::Foot
            | Unit::Mile => Dimension::Length,
            Unit::Milligram | Unit::Gram | Unit::Kilogram | Unit::Ounce | Unit::Pound => {
                Dimension::Mass
            }
            Unit::Byte | Unit::Kibibyte | Unit::Mebibyte | Unit::Gibibyte => Dimension::DataSize,
        }
    }

    /// Factor to multiply by to convert `1.0` of `self` into the dimension's base unit (`m`, `g`, or `B`).
    pub const fn to_base_factor(self) -> f64 {
        match self {
            Unit::Millimeter => 0.001,
            Unit::Centimeter => 0.01,
            Unit::Meter => 1.0,
            Unit::Kilometer => 1000.0,
            Unit::Inch => 0.0254,
            Unit::Foot => 0.3048,
            Unit::Mile => 1609.344,
            Unit::Milligram => 0.001,
            Unit::Gram => 1.0,
            Unit::Kilogram => 1000.0,
            Unit::Ounce => 28.349_523_125,
            Unit::Pound => 453.592_37,
            Unit::Byte => 1.0,
            Unit::Kibibyte => 1024.0,
            Unit::Mebibyte => 1024.0 * 1024.0,
            Unit::Gibibyte => 1024.0 * 1024.0 * 1024.0,
        }
    }

    pub const fn symbol(self) -> &'static str {
        match self {
            Unit::Millimeter => "mm",
            Unit::Centimeter => "cm",
            Unit::Meter => "m",
            Unit::Kilometer => "km",
            Unit::Inch => "in",
            Unit::Foot => "ft",
            Unit::Mile => "mi",
            Unit::Milligram => "mg",
            Unit::Gram => "g",
            Unit::Kilogram => "kg",
            Unit::Ounce => "oz",
            Unit::Pound => "lb",
            Unit::Byte => "B",
            Unit::Kibibyte => "KiB",
            Unit::Mebibyte => "MiB",
            Unit::Gibibyte => "GiB",
        }
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum ConversionError {
    UnknownUnit(String),
    DimensionMismatch {
        from: Unit,
        to: Unit,
        from_dim: Dimension,
        to_dim: Dimension,
    },
}

impl fmt::Display for ConversionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConversionError::UnknownUnit(u) => write!(f, "unrecognized unit '{u}'"),
            ConversionError::DimensionMismatch {
                from,
                to,
                from_dim,
                to_dim,
            } => write!(
                f,
                "cannot convert {} ({from_dim:?}) to {} ({to_dim:?})",
                from.symbol(),
                to.symbol()
            ),
        }
    }
}

pub fn convert_value(value: f64, from: Unit, to: Unit) -> Result<f64, ConversionError> {
    if from.dimension() != to.dimension() {
        return Err(ConversionError::DimensionMismatch {
            from,
            to,
            from_dim: from.dimension(),
            to_dim: to.dimension(),
        });
    }
    let base_value = value * from.to_base_factor();
    Ok(base_value / to.to_base_factor())
}

fn main() {
    // 1. Length conversion: 5 miles -> kilometers
    let km = convert_value(5.0, Unit::Mile, Unit::Kilometer).unwrap();
    assert!((km - 8.04672).abs() < 1e-5);
    println!("5.0 mi = {km:.4} km");

    // 2. Data size conversion: 2.5 GiB -> MiB
    let mib = convert_value(2.5, Unit::Gibibyte, Unit::Mebibyte).unwrap();
    assert_eq!(mib, 2560.0);
    println!("2.5 GiB = {mib:.0} MiB");

    // 3. Dimensional mismatch check (`kg` -> `km` is rejected!):
    let mismatch = convert_value(10.0, Unit::Kilogram, Unit::Kilometer).unwrap_err();
    println!("Dimensional check caught: {mismatch}");
}
5.0 mi = 8.0467 km
2.5 GiB = 2560 MiB
Dimensional check caught: cannot convert kg (Mass) to km (Length)