# 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, gravity: MetersPerSecond2, initial_height: Meters) -> Meters { let d2r = 0.01745329252; let angle : f32 = 45.0 * d2r; let cos = Dimensionless::::new(angle.cos()); let sin = Dimensionless::::new(angle.sin()); let range = (speed*cos/gravity) * (speed*sin + (speed*speed*sin*sin + Dimensionless::::new(2.0)*gravity*initial_height).sqrt()); range } ``` ### Type Control fts_units gives full control over storage type. ```rust let s = Seconds::::new(22.3); let ns = Nanoseconds::::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::::new(5.5); let _ : Seconds = d.convert_into(); // compile error! let _ : Meters = 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::::new(7.73); let i : Meters = 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::::new(9.8)); // 9.8 m·s⁻² println!("{}", KilometersPerHour::::new(65.5)); // 65.5 km·h⁻¹ ``` ### Arbitrary Ratios `si_system` quantities can have completely arbitrary ratios. ```rust type R = RatioT; let q : QuantityT, SIExponentsT>> = 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` 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`. `QuantityT, _>` will correctly support, or not support, operators how you see fit. If `Vector3` impls `std::ops::Add>` but NOT `std::ops::Mul>` the same will be true for `QuantityT, _>`. ## 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 { amount : T, _u: PhantomData } ``` 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 where NUM: Integer + NonZero, DEN: Integer + NonZero, { _num: PhantomData, _den: PhantomData, } ``` Here's where it gets a little complicated. A quantity with SIUnits has a list of Ratios and Exponents. ```rust pub struct SIUnitsT where RATIOS: SIRatios, EXPONENTS: SIExponents { _r: PhantomData, _e: PhantomData, } #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct SIRatiosT where L: Ratio, M: Ratio, T: Ratio { _l: PhantomData, _m: PhantomData, _t: PhantomData } #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct SIExponentsT where L: Integer, M: Integer, T: Integer, { _l: PhantomData, _m: PhantomData, _t: PhantomData } ``` Here are some example types fully spelled out. ```rust // Ratios and exponents are stored in Length/Mass/Time order type Kilometers = QuantityT, SIExponentsT>>; type CentimetersPerSecondSquared = QuantityT, SIExponentsT>>; ``` 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>`. All operations require matching `T` types. Add and subtract are implement is `SIUnitsT` 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>, typenum::int::PInt>>, _, fts_units::ratio::RatioZero>, fts_units::si_system::SIExponentsT>, _, typenum::int::Z0>>>` found type `fts_units::quantity::QuantityT<_, fts_units::si_system::SIUnitsT>, typenum::int::PInt>>>, fts_units::si_system::SIExponentsT>>>>` ``` 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