fix_rat/
lib.rs

1#![no_std]
2#![cfg_attr(feature = "nightly", feature(generic_const_exprs))]
3//! fix-rat is a rational number with the denominator chosen at compile time.
4//!
5//! It has a fixed valid range.
6//!
7//!     # use fix_rat::Rational;
8//!     type R = Rational<{ i64::MAX / 64}>;
9//!
10//!     let a: R = 60.into();
11//!     let b: R = 5.0.into();
12//!     let c = a.wrapping_add(b);
13//!
14//!     let c = c.to_i64();
15//!     assert_eq!(c, -63);
16//!
17//! # Intended Use-Cases
18//!
19//! It is meant to replace regular floating point numbers
20//!     (f64/f32)
21//! in the following cases:
22//!
23//! 1. Requiring reliable precision,
24//!     for example for handling money.
25//! If you have a requirement like
26//!     "x decimal/binary places after the dot"
27//! then this crate might be for you.
28//!
29//! 2. Requiring "deterministic"
30//!     (actually commutative and associative)
31//! behaviour for multithreading.
32//! If you want to apply updates to a value and
33//! the result should be the same no matter in what order the updates were applied then
34//! this crate might be for you.
35//!
36//! 3. Better precision if plausible value range is known.
37//! Floating point numbers are multi-scalar,
38//!     they can represent extremely small and extremely big numbers.
39//! If your calculations stay within a known interval,
40//!     for example [-1, 1],
41//! fixed-rat might be able to provide better precision and performance.
42//!
43//! 4. todo: low precision and high performance requirements:
44//! Floating point numbers only come in two variants,
45//! f32 and f64, using 32 and 64 bits respectively.
46//! If you do not require that much precision,
47//!     if 16 or even 8 bits are enough for your usecase
48//! you can store more numbers in the same space.
49//!     Due to lower memory bandwidth and availability of SIMD-instructions
50//! this can lead to close to a 2x or 4x respectively speed up of affected pieces of code.
51//!
52//! # Gotchas (tips&tricks)
53//! For reliable precision:
54//!     remember that you are still loosing precision on every operation,
55//!     there is no way around this except bigints.
56//!
57//! Unlike floats Rationals have a valid range and it is easy to over/underflow it.
58//! It might be advisable to choose the representable range slightly larger.
59//!
60//! Using rationals does not automatically make multithreaded code deterministic.
61//! The determinism loss with floating point numbers happens when
62//!     a calculation changes the scale (exponent) of the number.
63//! Rationals are always on the same scale but you now have to deal with range overflows.
64//!
65//! The easiest behaviour is to use wrapping\_op.
66//! It always succeeds and can be executed in any order with any values without loosing determinism.
67//! This might not be sensible behaviour for your use-case though.
68//!
69//! The second-easiest is to use is checked\_op with unwrap.
70//! This is probably fine if you have a base value that is only changed in small increments.
71//! Choose a slightly bigger representable range and do the overflow handling in synchronous code,
72//!     for example by clamping to the valid range
73//!         (which is smaller than the representable range).
74//!
75//! You can not,
76//!     at least not naively,
77//! actually check the checked\_op,
78//!     as that would generally lead to behaviour differences on different execution orders.
79//! Correctly doing this is the hardest option,
80//!     but might be required for correctness.
81//!
82//! Using saturating\_op can be perfectly valid,
83//!     but you need to be careful that the value can only be pushed into one direction
84//!         (either towards max or towards min).
85//! Otherwise different execution orders lead to different results.
86//! Reminder that adding negative values is also subtraction!
87//!
88//! Assuming [-10,10]:
89//!     `9 + 2 = 10, 10 - 1 =  9`
90//! but
91//!     `9 - 1 =  8,  8 + 2 = 10`
92//! 9 != 10.
93//!
94//! Moving through different scales,
95//!     mainly by multiplying/dividing
96//! costs more precision than you might be used from floating point numbers.
97//! For example diving by 2 costs no precision in floating point numbers,
98//!     it simply decreases the exponent by one.
99//! In rationals it costs one bit of precsion.
100//! Remember that rationals start out with 63 bits though,
101//! while f64 only has 53.
102//!
103//! # Implementation
104//! This is a super simple wrapper around an integer,
105//! basically all operations are passed straight through.
106//! So an addition of two rationals is really just a simple integer addition.
107//!
108//! Converting an integer/float to a rational simply multiplies it by the chosen DENOM,
109//! rational to integer/float divides.
110//!
111//! The code is very simple.
112//! The main value of this crate is the tips&tricks and ease of use.
113//!
114//! # todos/notes
115//! Currently being generic over intergers is a bit.. annoying. Being generic over intergers while
116//! also taking a value of that type as a const generic is.. currently not typechecking. so
117//! supporting usecase 4 would need some macroing (i&u 8,16,32,64). For now its just always i64.
118//!
119//! I should probably provide some atomic operations for comfort, at least add&sub.
120//! Even though they are simply identical to just adding/subbing on the converted atomic.
121//!
122//! Currently there is no rat-rat interaction with different denominatiors.
123//! That could be improved,
124//! but might need to wait for better const generics.
125//!
126//! # Nightly
127//! if you enable nightly the generic\_const\_exprs feature is used to provide primitive multiplication.
128
129/// Can store -10 to 10 with a bit of wiggle room.
130pub type TenRat = Rational<{ i64::MAX / 16 }>;
131
132/// Can store -100 to 100 with a bit of wiggle room.
133pub type HundRat = Rational<{ i64::MAX / 128 }>;
134
135#[cfg(feature = "serde1")]
136use serde as sd;
137/// A ratonal number with a fixed denominator,
138/// therefore `size_of<Rational>() == size_of<i64>()`.
139///
140/// Plus operations have more intuitive valid ranges
141///
142/// If you want to represent numbers in range `-x` to `x`
143/// choose DENOM as `i64::MAX / x`.
144///
145/// [Rational] then subdivides the whole range into i64::MAX equally-sized parts.
146/// Smaller operations are lost,
147/// going outside the range overflows.
148///
149/// DENOM needs to be positive or you will enter the bizarro universe.
150///
151/// I would strongly recommend to choose DENOM as `1 << x` for x in 0..63.
152///
153/// The regular operations (+,-, *, /) behave just like on a regular integer,
154///     panic on overflow in debug mode,
155///     wrapping in release mode.
156/// Use the wrapping_op, checked_op or saturating_op methods
157///     to explicitly chose a behaviour.
158#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
159#[repr(transparent)]
160#[cfg_attr(feature = "serde1", derive(sd::Serialize, sd::Deserialize))]
161pub struct Rational<const DENOM: i64> {
162    numer: i64,
163}
164impl<const DENOM: i64> Rational<DENOM> {
165    /// Returns the underlying integer type,
166    /// for example for storing in an atomic number.
167    ///
168    /// This should compile to a no-op.
169    pub fn to_storage(self) -> i64 {
170        self.numer
171    }
172    /// Builds from the underlying integer type,
173    /// for example after retrieving from an atomic number.
174    ///
175    /// This should compile to a no-op.
176    ///
177    /// Use [from_int](Self::from_int) if you have an integer that you want to convert to a rational.
178    pub fn from_storage(storage: i64) -> Self {
179        Self { numer: storage }
180    }
181
182    /// Converts an integer to a Rational.
183    pub fn from_int(i: i64) -> Self {
184        Self { numer: i * DENOM }
185    }
186    /// Since rational numbers can not represent inf, nan and other fuckery,
187    /// this returns None if the input is wrong.
188    ///
189    /// This will loose precision,
190    /// try to only convert at the start and end of your calculation.
191    pub fn aprox_float_fast(f: f64) -> Option<Self> {
192        use core::num::FpCategory as Cat;
193        if let Cat::Subnormal | Cat::Normal | Cat::Zero = f.classify() {
194        } else {
195            return None;
196        }
197
198        // this is really not very accurate
199        // as the expansion step kills a lot of accuracy the float might have had
200        // (denom is really big, int_max/representable_range)
201        let expanded = f * (DENOM as f64);
202        Some(Self {
203            numer: expanded as i64,
204        })
205    }
206
207    /// Assumes denom to be a power of two.
208    /// kind of experimental.
209    #[doc(hidden)]
210    pub fn aprox_float(f: f64) -> Option<Self> {
211        use core::num::FpCategory as Cat;
212        match f.classify() {
213            //fixme: im reasonably sure that subnormal needs to be handled different
214            //(implicit 1 or something along those lines)
215            Cat::Subnormal | Cat::Normal => {}
216            Cat::Zero => return Self::from(0).into(),
217            _ => return None,
218        }
219        use num_traits::float::FloatCore;
220        let (mant, f_exp, sign) = f.integer_decode();
221        //exp is << or >> on the mant depending on sign
222        let d_exp = 64 - (DENOM as u64).leading_zeros();
223        //let rest = DENOM - (1 << d_exp);
224
225        let exp = f_exp + (d_exp as i16);
226        let neg = exp.is_negative();
227        let exp = exp.abs() as u32;
228        let numer = if !neg {
229            // make sure we have enough headroom
230            // cheked_shl/r does not! do this check
231            if mant.leading_zeros() < exp {
232                return None;
233            }
234            mant << exp
235        } else {
236            // not checking for "bottom"-room here as we are
237            // "just" loosing precision, not orders of magnitude
238            mant >> exp
239        };
240        let numer = numer as i64;
241        let numer = if sign.is_negative() { -numer } else { numer };
242        // fixme: do something about the rest??
243        // hm, or just allow 1<<x as denom, sounds senible too
244        Self { numer }.into()
245    }
246
247    /// This will loose precision,
248    /// try to only convert at the start and end of your calculation.
249    pub fn to_f64(self) -> f64 {
250        //todo: can i do a *thing* with this to get some more precision?
251        //(i.e. bitbang the float?)
252        self.numer as f64 / DENOM as f64
253    }
254
255    /// this will integer-round, potentially loosing a lot of precision.
256    pub fn to_i64(self) -> i64 {
257        self.numer / DENOM
258    }
259
260    pub fn clamp(self, low: Self, high: Self) -> Self {
261        self.max(low).min(high)
262    }
263    /// The maximum representable number.
264    ///
265    /// Note that unlike floats rationals do not have pos/neg inf.
266    pub const fn max() -> Self {
267        Self { numer: i64::MAX }
268    }
269    /// The minimum representable number.
270    ///
271    /// Note that unlike floats rationals do not have pos/neg inf.
272    pub const fn min() -> Self {
273        Self { numer: i64::MIN }
274    }
275
276    pub fn checked_add(self, other: Self) -> Option<Self> {
277        Self {
278            numer: self.numer.checked_add(other.numer)?,
279        }
280        .into()
281    }
282    pub fn checked_mul(self, other: Self) -> Option<Self> {
283        Self {
284            numer: self.numer.checked_mul(other.numer)?,
285        }
286        .into()
287    }
288    pub fn checked_sub(self, other: Self) -> Option<Self> {
289        Self {
290            numer: self.numer.checked_sub(other.numer)?,
291        }
292        .into()
293    }
294    pub fn checked_div(self, other: Self) -> Option<Self> {
295        Self {
296            numer: self.numer.checked_div(other.numer)?,
297        }
298        .into()
299    }
300
301    pub fn wrapping_add(self, other: Self) -> Self {
302        Self {
303            numer: self.numer.wrapping_add(other.numer),
304        }
305    }
306    pub fn wrapping_mul(self, other: i64) -> Self {
307        Self {
308            numer: self.numer.wrapping_mul(other),
309        }
310    }
311    pub fn wrapping_sub(self, other: Self) -> Self {
312        Self {
313            numer: self.numer.wrapping_sub(other.numer),
314        }
315    }
316    pub fn wrapping_div(self, other: i64) -> Self {
317        Self {
318            numer: self.numer.wrapping_div(other),
319        }
320    }
321
322    /// Don't use this in parallel code if other parallel code is also subtracting,
323    /// otherwise you loose determinism.
324    ///
325    /// ```
326    /// use fix_rat::Rational;
327    /// let max = Rational::<{1024}>::max();
328    /// let one = Rational::<{1024}>::from_int(1);
329    /// assert_ne!(max.saturating_add(one)-max, (max-max).saturating_add(one));
330    /// ```
331    pub fn saturating_add(self, other: Self) -> Self {
332        Self {
333            numer: self.numer.saturating_add(other.numer),
334        }
335    }
336    pub fn saturating_mul(self, other: i64) -> Self {
337        Self {
338            numer: self.numer.saturating_mul(other),
339        }
340    }
341    pub fn saturating_sub(self, other: Self) -> Self {
342        Self {
343            numer: self.numer.saturating_sub(other.numer),
344        }
345    }
346}
347
348impl<const DENOM: i64> From<f64> for Rational<DENOM> {
349    fn from(o: f64) -> Self {
350        // apparently _fast is not less precise than "regular" so using that for now
351        // might change at a moments notice though
352        Self::aprox_float_fast(o).unwrap()
353    }
354}
355impl<const DENOM: i64> From<i64> for Rational<DENOM> {
356    fn from(o: i64) -> Self {
357        Self::from_int(o)
358    }
359}
360
361impl<const DENOM: i64> core::ops::Add for Rational<DENOM> {
362    type Output = Self;
363    fn add(self, other: Self) -> Self {
364        Self {
365            numer: self.numer + other.numer,
366        }
367    }
368}
369
370impl<const DENOM: i64> core::ops::Sub for Rational<DENOM> {
371    type Output = Self;
372    fn sub(self, other: Self) -> Self {
373        Self {
374            numer: self.numer - other.numer,
375        }
376    }
377}
378
379impl<const DENOM: i64> core::ops::Mul<i64> for Rational<DENOM> {
380    type Output = Self;
381    fn mul(self, other: i64) -> Self {
382        Self {
383            numer: self.numer * other,
384        }
385    }
386}
387
388impl<const DENOM: i64> core::ops::Div<i64> for Rational<DENOM> {
389    type Output = Self;
390    fn div(self, other: i64) -> Self {
391        Self {
392            numer: self.numer / other,
393        }
394    }
395}
396
397impl<const DENOM: i64> core::iter::Sum for Rational<DENOM> {
398    fn sum<I>(i: I) -> Self
399    where
400        I: Iterator<Item = Self>,
401    {
402        i.fold(Self::from(0), |sum, new| sum + new)
403    }
404}
405
406/// SAFETY: this is literally just an i64, transparent representation and all, this is therefore
407/// safe.
408/// can not auto-derive cause the derive macro can not very alignment in generic types.
409unsafe impl<const DENOM: i64> bytemuck::NoUninit for Rational<DENOM> {}
410
411#[cfg(feature = "nightly")]
412impl<const DENOMS: i64, const DENOMO: i64> core::ops::Mul<Rational<DENOMO>> for Rational<DENOMS>
413where
414    [(); { DENOMS * DENOMO } as usize]:,
415{
416    type Output = Rational<{ DENOMS * DENOMO }>;
417    fn mul(self, other: Rational<DENOMO>) -> Self::Output {
418        Self::Output {
419            numer: self.numer * other.numer,
420        }
421    }
422}
423
424#[test]
425fn converts() {
426    let _tenrat: TenRat = 0.0.into();
427    let _tenrat: TenRat = 1.0.into();
428    let _tenrat: TenRat = (-1.0).into();
429    let _tenrat: TenRat = 10.0.into();
430    let _tenrat: TenRat = (-10.0).into();
431}
432
433#[test]
434fn maths() {
435    let num: Rational<{ 1 << 30 }> = 0.1.into();
436
437    let add = (num + num).to_f64() - 0.2;
438    assert!(add.abs() < 0.00001);
439
440    let sub = (num - num - num).to_f64() + 0.1;
441    assert!(sub.abs() < 0.00001);
442
443    #[cfg(feature = "nightly")]
444    {
445        let bignum: Rational<{ 1 << 30 }> = 10.0.into();
446        let mul = (num * num).to_f64() - 0.01;
447        assert!(mul.abs() < 0.00001);
448        let mul2 = (num * bignum).to_f64() - 1.0;
449        assert!(mul2.abs() < 0.00001);
450    }
451
452    /* TODO: add div..maybe
453    let div = (num / num).to_f64() - 1.0;
454    assert!(div.abs() < 0.00001);
455    let div2 = (num / bignum).to_f64() - 0.01;
456    assert!(div2.abs() < 0.00001)
457    */
458}
459
460#[test]
461fn precision() {
462    type R = Rational<{ i64::MAX / (1 << 10) }>;
463    extern crate std;
464    use std::dbg;
465    let f = 640.132143234189097_f64;
466    let r: R = Rational::aprox_float(f).unwrap();
467    let r2: R = Rational::aprox_float_fast(f).unwrap();
468    let rf = r.to_f64();
469    // ok so turns out that the fast conversion is actually not worse.
470    // i guess multiplying/diving by 2potn is kinda what floats are good at
471    let rl = r2.to_f64();
472
473    let absdiff = (f - rf).abs();
474    dbg!(f, r, r2, rf, rl, absdiff);
475    assert!(absdiff < 1e20);
476}
477
478#[test]
479fn displaytest() {
480    let tenrat: TenRat = (-10.0).into();
481    extern crate std;
482    use std::println;
483    println!("{:#?}", tenrat);
484}
485
486#[cfg(feature = "serde1")]
487#[test]
488fn serde_test() {
489    let r: TenRat = 3.0.into();
490    use bincode;
491    let s = bincode::serde::encode_to_vec(&r, bincode::config::standard()).unwrap();
492    let d = bincode::serde::decode_from_slice(&s, bincode::config::standard()).unwrap();
493    assert_eq!(r, d.0);
494}