Skip to main content

moddef_core/codec/
rat.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Exact rational arithmetic over i128 (spec ยง10). A 64-bit raw register
4//! value times an i64/i64 rational fits i128 with headroom; gcd
5//! normalization after each operation keeps magnitudes small. `no_std`.
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct Rat {
9    n: i128,
10    d: i128, // > 0
11}
12
13const fn gcd(mut a: i128, mut b: i128) -> i128 {
14    if a < 0 {
15        a = -a;
16    }
17    if b < 0 {
18        b = -b;
19    }
20    while b != 0 {
21        let t = a % b;
22        a = b;
23        b = t;
24    }
25    if a == 0 {
26        1
27    } else {
28        a
29    }
30}
31
32// Plain methods rather than ops traits: every operation is used in call
33// chains where the by-value Copy semantics read clearer than `impl Mul`.
34#[allow(clippy::should_implement_trait)]
35impl Rat {
36    pub fn new(n: i128, d: i128) -> Rat {
37        let (mut n, mut d) = if d < 0 { (-n, -d) } else { (n, d) };
38        let g = gcd(n, d);
39        n /= g;
40        d /= g;
41        Rat { n, d }
42    }
43
44    pub fn int(v: i64) -> Rat {
45        Rat { n: v as i128, d: 1 }
46    }
47
48    pub fn from_u64(v: u64) -> Rat {
49        Rat { n: v as i128, d: 1 }
50    }
51
52    pub fn mul(self, o: Rat) -> Rat {
53        // Cross-reduce before multiplying to avoid overflow.
54        let g1 = gcd(self.n, o.d);
55        let g2 = gcd(o.n, self.d);
56        Rat::new((self.n / g1) * (o.n / g2), (self.d / g2) * (o.d / g1))
57    }
58
59    pub fn div(self, o: Rat) -> Rat {
60        self.mul(Rat::new(o.d, o.n))
61    }
62
63    pub fn add(self, o: Rat) -> Rat {
64        Rat::new(self.n * o.d + o.n * self.d, self.d * o.d)
65    }
66
67    pub fn sub(self, o: Rat) -> Rat {
68        Rat::new(self.n * o.d - o.n * self.d, self.d * o.d)
69    }
70
71    /// 10^exp as an exact rational (|exp| <= 38 fits i128; clamp beyond).
72    pub fn pow10(exp: i64) -> Rat {
73        let e = exp.unsigned_abs().min(38);
74        let mut p: i128 = 1;
75        let mut i = 0;
76        while i < e {
77            p *= 10;
78            i += 1;
79        }
80        if exp >= 0 {
81            Rat { n: p, d: 1 }
82        } else {
83            Rat { n: 1, d: p }
84        }
85    }
86
87    /// Round to nearest integer, half away from zero (matches Go ratRound).
88    pub fn round(self) -> i64 {
89        let half = self.d / 2;
90        let n = if self.n >= 0 {
91            self.n + half
92        } else {
93            self.n - half
94        };
95        (n / self.d) as i64
96    }
97
98    pub fn to_f64(self) -> f64 {
99        // Split to keep precision for large numerators.
100        let q = self.n / self.d;
101        let r = self.n % self.d;
102        q as f64 + (r as f64) / (self.d as f64)
103    }
104
105    /// Parse an f64 into an exact rational via its shortest decimal display
106    /// (keeps typical engineering values like 230.1 exact). Fallback: rounded.
107    pub fn from_f64(v: f64) -> Rat {
108        if !v.is_finite() {
109            return Rat::int(0);
110        }
111        if v == (v as i64) as f64 {
112            return Rat::int(v as i64);
113        }
114        // Scale by powers of 10 until integral (max 12 fractional digits).
115        // f64::trunc/round live in std, not core; saturating float->int casts
116        // give no_std-safe equivalents within our value range.
117        let mut d: i128 = 1;
118        let mut x = v;
119        for _ in 0..12 {
120            if x == (x as i128) as f64 {
121                break;
122            }
123            x *= 10.0;
124            d *= 10;
125        }
126        let n = if x >= 0.0 {
127            (x + 0.5) as i128
128        } else {
129            (x - 0.5) as i128
130        };
131        Rat::new(n, d)
132    }
133}