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}