1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
/*! [![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::*; # use fts_units::ops::Sqrt; 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 # use fts_units::si_system::quantities::*; 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 # use fts_units::si_system::quantities::f32::*; # use fts_units::quantity::ConvertUnits; 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 # use fts_units::si_system::quantities::*; # use fts_units::quantity::{CastAmount, Quantity}; 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 # use fts_units::si_system::quantities::*; 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 # use fts_units::ratio::*; # use fts_units::quantity::*; # use fts_units::si_system::*; # use typenum::consts::*; 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 # use fts_units::quantity::Amount; # use std::marker::PhantomData; 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 # use typenum::{Integer, NonZero}; # use std::marker::PhantomData; 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 # use fts_units::ratio::*; # use fts_units::quantity::*; # use fts_units::si_system::{SIRatios, SIExponents}; # use std::marker::PhantomData; # use typenum::{Integer, NonZero}; 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 # use fts_units::quantity::*; # use fts_units::si_system::*; # use typenum::consts::*; // 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: ``` # /* 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. */ pub mod ops; pub mod quantity; pub mod ratio; pub mod si_system; pub mod typenum_ext;