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;