#units #gamedev #unit


fts_units is a library that enables compile-time type-safe mathematical operations using units of measurement

2 releases

0.1.1 Jun 12, 2019
0.1.0 Jun 10, 2019

#257 in Rendering

Used in fts_gamemath

Unlicense OR MIT



Crate API

fts_units is a Rust library that enables compile-time type-safe mathematical operations using units of measurement.

It offers a series of attractive features.

International System of Units

Robust support for SI Units. Custom systems are possible, but are not an emphasis of development at this time.

Basic Math

use fts_units::si_system::quantities::f32::*;

let d = Meters::new(10.0);  // units are meters
let t = Seconds::new(2.0);  // units are seconds
let v = d / t;              // units are m·s⁻¹ (MetersPerSecond)

// compile error! meters plus time doesn't make sense
let _ = d + t; // compile errors! meters + time doesn't make sense

// compile error! can't mix different ratios (Unit and Kilo)
let _ = d + Kilometers::new(2.3);

Combined Operations

Basic math operations can be arbitraily combined. Valid types are not predefined. If your computation has a value with meters to the fourteenth power it'll work just fine.

use fts_units::si_system::quantities::*;

fn calc_ballistic_range(speed: MetersPerSecond<f32>, gravity: MetersPerSecond2<f32>, initial_height: Meters<f32>)
-> Meters<f32>
    let d2r = 0.01745329252;
    let angle : f32 = 45.0 * d2r;
    let cos = Dimensionless::<f32>::new(angle.cos());
    let sin = Dimensionless::<f32>::new(angle.sin());

    let range = (speed*cos/gravity) * (speed*sin + (speed*speed*sin*sin + Dimensionless::<f32>::new(2.0)*gravity*initial_height).sqrt());

Type Control

fts_units gives full control over storage type.

let s = Seconds::<f32>::new(22.3);
let ns = Nanoseconds::<i64>::new(237_586_538);

If you're working primarily with f32 values then convenience modules wrap all common types.

use fts_units::si_system::quantities::f32::*;


Quantities of similar dimension can be converted between.

let d = Kilometers::new(15.3);
let t = Hours::new(2.7);
let kph = d / t; // KilometersPerHour

let mps : MetersPerSecond = kph.convert_into();
let mps = MetersPerSecond::convert_from(kph);

Attempting to convert to a quantity of different dimension produce a compile-time error.

let d = Meters::<f32>::new(5.5);
let _ : Seconds<f32> = d.convert_into(); // compile error!
let _ : Meters<f64> = d.convert_into(); // also compile error!


Quantity amounts can be cast following normal casting rules. This feature uses num-traits.

let m = Meters::<f32>::new(7.73);
let i : Meters<i32> = m.cast_into();
assert_eq!(i.amount(), 7);

No conversion or casting is ever performed implicitly. This ensures full control when working with disparate scales. For example converting between nanoseconds and years.


The SI System supports human readable display output.

println!("{}", MetersPerSecond2::<f32>::new(9.8));
// 9.8 m·s⁻²

println!("{}", KilometersPerHour::<f32>::new(65.5));
// 65.5 km·h⁻¹

Arbitrary Ratios

si_system quantities can have completely arbitrary ratios.

type R = RatioT<P37,P10>;
let q : QuantityT<f32, SIUnitsT<SIRatiosT<R, Zero, Zero>, SIExponentsT<P1, Z0, Z0>>> = 1.1.into();

No Macros or Build Scripts

This crate is vanilla Rust code. There are no macros or build scripts to auto-generate anything.

This was an explicit choice to make the source code very easy to read, understand, and extend.

Custom Amounts

struct QuantityT<T,U> works for any T where T:Amount.

Amount is implemented for built-in in types: u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f32, and f64`.

Amount can be also implemented for any custom types. For example Vector3<f32>. QuantityT<Vector3<f32>, _> will correctly support, or not support, operators how you see fit. If Vector3<f32> impls std::ops::Add<Vector3f<f32>> but NOT std::ops::Mul<Vector3<f32>> the same will be true for QuantityT<Vector3<f32>, _>.


fts_units is entirely compile-time with no run-time cost. Units are stored as zero-sized-types which compile away to nothing.

None of this actually matters if you're using the provided SI System. You'll never have to type any of these types ever. However if you make a mistake and produce a compile error then knowing the underlying types will help you understand the source of the error.

The best way to understand the implementation is a quick tour of a few important structs. For most structs there is a matching trait. I've chose to use the T suffix for structs. For example Quantity (trait) and QuantityT (struct). And Ratio (trait) and RatiotT (struct). The T signifies than the struct must provide a type.

QuantityT is the basic struct. Meters, Seconds, and MetersPerSecond are all QuantityT structs with different U types.

pub struct QuantityT<T:Amount, U> {
    amount : T,
    _u: PhantomData<U>

RatioT is a struct which stores a numerator and a denometer in the form of a type. The ratio for a kilometer is 1000 / 1. A nanometer is 1 / 1_000_000_000.

pub struct RatioT<NUM, DEN>
        NUM: Integer + NonZero,
        DEN: Integer + NonZero,
    _num: PhantomData<NUM>,
    _den: PhantomData<DEN>,

Here's where it gets a little complicated. A quantity with SIUnits has a list of Ratios and Exponents.

        RATIOS: SIRatios,
        EXPONENTS: SIExponents
    _r: PhantomData<RATIOS>,
    _e: PhantomData<EXPONENTS>,

#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct SIRatiosT<L,M,T>
        L: Ratio,
        M: Ratio,
        T: Ratio
    _l: PhantomData<L>,
    _m: PhantomData<M>,
    _t: PhantomData<T>

#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct SIExponentsT<L,M,T>
        L: Integer,
        M: Integer,
        T: Integer,
    _l: PhantomData<L>,
    _m: PhantomData<M>,
    _t: PhantomData<T>

Here are some example types fully spelled out.

// Ratios and exponents are stored in Length/Mass/Time order
type Kilometers = QuantityT<f32,
        SIRatiosT<Kilo, Zero, Zero>,
        SIExponentsT<P1, Z0, Z0>>>;

type CentimetersPerSecondSquared = QuantityT<f64,
        SIRatiosT<Centi, Zero, Unit>,
        SIExponentsT<P1, Z0, N2>>>;

Quantity operations such as add, multiple, divide, and sqrt are supported so long as the Units type supports that operation.

When working with the si_system that means we're working with QuantityT<T,SIUnitsT<R,E>>. All operations require matching T types. Add and subtract are implement is SIUnitsT<R,E> is the same. Multiply and divide are implemented if R types do not conflict. Sqrt is implemented if T supports sqrt and all E values are even.

You can change T by using the CastAmount trait. You can change U by using ConvertUnits.


SI System

The SI System currently only support length, mass, and time dimensions. This is all most games ever need.

Electric current, temperature, amount of substance, and luminous intensity will be added later. It's a trivial task, but requires a moderate amount of copy/paste. These dimensions will be added once the basic API settles down.

Bad Error Messages

fts_units leverages the fantastic typenum crate for compile-time math. Unfortunately this results in horrible error messages.

When const generics land this dramatically improve.

This code:

let _ = Meters::new(5.0) + Seconds::new(2.0);

Produces this error:

error[E0308]: mismatched types
  --> examples\sandbox.rs:82:32
82 |     let _ = Meters::new(5.0) + Seconds::new(2.0);
   |                                ^^^^^^^^^^^^^^^^^ expected struct `fts_units::ratio::RatioT`, found struct `fts_units::ratio::RatioZero`
   = note: expected type `fts_units::quantity::QuantityT<_, fts_units::si_system::SIUnitsT<fts_units::si_system::SIRatiosT<fts_units::ratio::RatioT<typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>, typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>>, _, fts_units::ratio::RatioZero>, fts_units::si_system::SIExponentsT<typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>, _, typenum::int::Z0>>>`
              found type `fts_units::quantity::QuantityT<_, fts_units::si_system::SIUnitsT<fts_units::si_system::SIRatiosT<fts_units::ratio::RatioZero, _, fts_units::ratio::RatioT<typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>, typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>>>, fts_units::si_system::SIExponentsT<typenum::int::Z0, _, typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>>>>`

It's a visual nightmare. But it can be understood!

When lined up or put in a diff tool the difference is easy to spot. The 'found type' has a RatioZero type in the first SIUnitsT slot when it expected a non-zero type. If you remember the slots are length/mass/time this should make sense. The Meters value has a non-zero length ratio. The Seconds value has a zero length ratio. To add two SIUnitsT quantities they must have the exact same Ratios and Exponents.

Orders of Magnitude

Femto through Peta are supported. Unfortunately Atto/Zepto/Yocto and Exa/Zetta/Yotta are no supported. They require 128-bit ratios and fts_units is currently constrained to 64-bits due to typenum.

When const generics land this should change.

Derived Units

One of the nice things about the SI System is derived units. Everyone knows that Force = Mass * Acceleration. Force is such a common quantity it has a name, Newton. Where a Newton is stored in kg⋅m⋅s⁻². This also allows units such as KiloNewtons of force or TerraWatts of power.

Unfortunately fts_units does not support derived units. When specialization lands it will be much easier to support well.


What does fts mean?

They're my initials.

Why did you make this?

Because I've always wanted it to exist.

Why use fts_units?

Why should someone use fts_units instead of uom or dimensioned?

Good question. You might prefer one of those crates instead! I think fts_units has a better API. I like having explicit control over casting and conversion. I like that it doesn't use macros so the code is easy to read and understand.

This does what I want the way I want.

License: Unlicense OR MIT