ferrunitas 0.2.0

A type-safe unit conversion library with compile-time dimensional analysis
Documentation

Ferrunitas - Type-Safe Unit Conversion Library

A Rust library for compile-time dimensional analysis and unit conversions. Ferrunitas provides type safety for physical quantities and prevents dimensional errors at compile time through Rust's powerful type system.

Current build and test state: Build + Test

Features

  • Compile-time dimensional analysis: Prevents adding incompatible quantities (e.g. mass + length) at compile time
  • Type-safe unit conversions: Zero runtime overhead conversions between compatible units
  • Flexible API: Work with specific units (Measure<Metre>) or generic quantities (Length)
  • Extensive unit system: Complete SI base units, derived units, and common non-SI units
  • Prefix support: Metric prefixes (kilo, milli, etc.) with easy unit definition
  • Custom unit definition: Macros for defining your own units, quantities, and prefixes
  • Quantity tagging (optional): Distinguish between dimensionally equivalent but semantically different quantities (e.g., Angle vs Information)
  • Zero-cost abstractions: All dimensional checking happens at compile time

Usage

Basics

use ferrunitas::system::*;
use ferrunitas::{Measure, Unit};

fn main() {
    // Create measures with specific units
    let distance = 100 * Metre;
    let time = Second::new(10.0);

    // Convert between compatible units
    let feet: Measure<Foot> = distance.convert();
    let minutes: Measure<Minute> = time.convert();
    println!("Distance: {:.2} or {:.2}", distance, feet);
    println!("Time: {:.2} or {:.2}", time, minutes);

    // Arithmetic operations with automatic dimensional checking
    let velocity: Velocity = distance / time;
    let speed_mps = velocity.as_measure::<MetrePerSecond>();
    let speed_mph = velocity.as_measure::<MilePerHour>();

    println!("Speed: {:.3} or {:.3}", speed_mps, speed_mph);
}

Relevant imports

// For dimensional formatting
use ferrunitas::common::{format_dims, format_unit_dims};

// For access to all definitions to units, quantities and prefixes
use ferrunitas::system::*;

// Crate root access to most common struct Measure and trait Unit to make methods available
use ferrunitas::{Measure, Unit};

// Macros for own definitions (typenum consts for quantity macro)
use ferrunitas::typenum_consts::*;
use ferrunitas::{prefix, quantity, unit};

Working with Quantities vs Measures

Ferrunitas offers two main ways to work with physical values (they are compatitble with each other regarding arithmetics):

Measures - Values with specific units:

let mass = Kilogram::new(5.0);        // Measure<Kilogram>
let length = Foot::new(10.0);         // Measure<Foot>
let sum = mass + Gram::new(500.0);    // Addition works across compatible units

Quantities - Dimensioned values without specific units:

let mass: Mass = Kilogram::new(5.0).into_q();     // Convert to quantity
let length: Length = Foot::new(10.0).into_q();    // Convert to quantity
let force: Force = mass * acceleration;           // Multiplication creates new quantity

Unit Conversions

// Multiple ways to convert units
let liters = Litre::new(30.64);

// Via quantity (intermediate step)
let volume: Volume = liters.into_q();
let cubic_cm: Measure<CubicCentimetre> = volume.as_measure();

// Direct conversion
let cubic_cm_direct: Measure<CubicCentimetre> = liters.convert();

// One-line conversion
let cubic_cm_oneline = Volume::convert::<Litre, CubicCentimetre>(30.64);

Physics Calculations

// All operations are dimensionally checked at compile time
let mass = Kilogram::new(2.0);
let acceleration = MetrePerSecondSquared::new(9.81);
let force: Force = mass * acceleration;  // F = ma

let distance = Metre::new(5.0);
let work: Energy = force * distance;     // W = F·d

let time = Second::new(2.0);
let power: Power = work / time;          // P = W/t

println!("Power: {:.2}", power.as_measure::<Watt>());

Custom Units and Quantities

Define your own units using the provided macros:

use ferrunitas::{quantity, unit, prefix, typenum_consts::*};

// Define a new quantity (7 SI base dimensions: M, L, T, I, Th, N, J)
quantity!(MyLength: M Z0, L P1, T Z0, I Z0, Th Z0, N Z0, J Z0);

// Define a new quantity with tagging (requires `quantity_tags` feature)
quantity!(SpecialAngle: M Z0, L Z0, T Z0, I Z0, Th Z0, N Z0, J Z0; marked);

// Define a new prefix
prefix!(Magic, 42, "M");

// Define new units
unit!(base: Elbow, "elbow", MyLength; prefixable);
unit!(derived: Yard, "yd", (0.9144, Metre));
unit!(prefix: Magicmetre, Magic, Metre);

// Compound units with quantity tagging (requires `quantity_tags` feature)
unit!(compound: MyAngleUnit, "mau", [(Radian, P1)]; marked SpecialAngle);

Quantity Tagging (Optional Feature)

When the quantity_tags feature is enabled, Ferrunitas can distinguish between quantities that are dimensionally identical but represent different physical concepts:

// Enable with: cargo build --features="quantity_tags"
use ferrunitas::system::*;
use ferrunitas::Unit;

// These are both dimensionless, but represent different concepts
let data = Bit::new(8.0);
let angle = Radian::new(1.0);

// Without quantity_tags: these can be added directly
// With quantity_tags: compilation error - different quantity types!
// let invalid = data + angle;  // Error with quantity_tags enabled

// Instead, you need to explicitly specify the target quantity:
let combined = data.into_q().specify::<Dimensionless>() +
               angle.into_q().specify::<Dimensionless>();

// This helps prevent mixing semantically different values
let area1 = SquareMetre::new(4.0);
let area2 = SquareMetre::new(1.0);
let solid_angle = Steradian::new(0.5);

// Without tags: this works (both are dimensionless ratios)
// With tags: need to specify the result type
let result = (area1 / area2).specify::<SolidAngle>() + solid_angle;

Function Signatures

Ferrunitas provides flexible function signatures for different use cases:

// Accept specific units
fn kinetic_energy(mass: Measure<Gram>, velocity: Measure<MetrePerSecond>) -> Measure<Joule> {
    (0.5 * mass * velocity * velocity).as_measure()
}

// Accept any unit of a quantity type
fn calculate_force(
    mass: Measure<impl Unit<Quantity = Mass>>,
    acceleration: Measure<impl Unit<Quantity = Acceleration>>,
) -> Measure<Newton> {
    (mass.into_q() * acceleration.into_q()).as_measure()
}

// Work directly with quantities
fn calculate_acceleration(velocity_change: Velocity, time: Time) -> Acceleration {
    velocity_change / time
}

Examples

The examples/ directory contains comprehensive usage examples:

  • basics.rs - Core concepts: measures, quantities, conversions, and arithmetic
  • functions.rs - Different patterns for writing functions with dimensional types
  • advanced.rs - Custom unit definitions using macros
  • misc.rs - Dimensional introspection, debugging utilities, and quantity tagging demonstration

Run examples with:

cargo run --example basics

cargo run --example functions

cargo run --example advanced

cargo run --example misc


# To see quantity tagging in action:

cargo run --example misc --features="quantity_tags"

Limitations / Notes

  • Internal storage uses f64; typical floating point caveats apply, see examples/misc.rs.
  • Rounding / formatting of display values is a caller concern, however usual format specifiers are respected.
  • Offsets are supported for temperature scales, but are quite limited in usage besides simple conversion.

Contributing

Contributions are welcome! Please feel free to submit a pull request or open an issue for any suggestions or improvements.

License

This project is licensed under the Apache-2.0 License.