fts_units 0.1.1

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

[![Crate](https://img.shields.io/crates/v/fts_units.svg)](https://crates.io/crates/fts_units)
[![API](https://docs.rs/fts_units/badge.svg)](https://docs.rs/fts_units)

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](https://en.wikipedia.org/wiki/International_System_of_Units). Custom systems are possible, but are not an emphasis of development at this time.

### Basic Math

```no_compile
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.

```rust
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());
    range
}
```

### Type Control
fts_units gives full control over storage type.

```rust
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.

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

### Conversion
Quantities of similar dimension can be converted between.

```rust
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.

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

### Casting
Quantity amounts can be cast following normal casting rules. This feature uses [num-traits](https://github.com/rust-num/num-traits).

```rust
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.

### Display
The SI System supports human readable display output.

```rust
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.

```rust
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>, _>`.


## Implementation
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.

```rust
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.

```rust
pub struct RatioT<NUM, DEN>
    where
        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.

```rust
pub struct SIUnitsT<RATIOS,EXPONENTS>
    where
        RATIOS: SIRatios,
        EXPONENTS: SIExponents
{
    _r: PhantomData<RATIOS>,
    _e: PhantomData<EXPONENTS>,
}

#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct SIRatiosT<L,M,T>
    where
        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>
    where
        L: Integer,
        M: Integer,
        T: Integer,
{
    _l: PhantomData<L>,
    _m: PhantomData<M>,
    _t: PhantomData<T>
}
```

Here are some example types fully spelled out.

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

type CentimetersPerSecondSquared = QuantityT<f64,
    SIUnitsT<
        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`.


## Caveats

### 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](https://github.com/paholg/typenum) crate for compile-time math. Unfortunately this results in _horrible_ error messages.

When [const generics](https://github.com/rust-lang/rust/issues/44580) land this dramatically improve.

This code:

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

Produces this error:

```rust
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](https://github.com/rust-lang/rust/issues/31844) lands it will be much easier to support well.

## FAQ

##### 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](https://github.com/iliekturtles/uom) or [dimensioned](https://github.com/paholg/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