1use num_bigint::{BigInt, Sign};
21use num_rational::Ratio;
22use num_traits::{One, Signed, ToPrimitive, Zero};
23
24#[must_use]
44pub fn f64_to_ratio_exact(x: f64) -> Option<Ratio<BigInt>> {
45 if !x.is_finite() {
46 return None;
47 }
48 if x == 0.0 {
49 return Some(Ratio::zero());
50 }
51
52 let bits = x.to_bits();
53 let sign = if bits >> 63 == 0 {
54 Sign::Plus
55 } else {
56 Sign::Minus
57 };
58 let exponent = ((bits >> 52) & 0x7ff) as i64;
59 let fraction = bits & 0x000f_ffff_ffff_ffff;
60
61 let (mantissa, exp2) = if exponent == 0 {
65 (fraction, -1074_i64)
66 } else {
67 (fraction | (1_u64 << 52), exponent - 1075)
68 };
69
70 let mantissa = BigInt::from_biguint(sign, mantissa.into());
71 let ratio = if exp2 >= 0 {
72 Ratio::from_integer(mantissa << (exp2 as usize))
73 } else {
74 Ratio::new(mantissa, BigInt::one() << ((-exp2) as usize))
75 };
76 Some(ratio)
77}
78
79#[must_use]
105pub fn f64_to_ratio_approx(x: f64, max_denom: u64) -> Option<Ratio<BigInt>> {
106 if max_denom == 0 {
107 return None;
108 }
109 let exact = f64_to_ratio_exact(x)?;
110 Some(best_rational_approx(&exact, &BigInt::from(max_denom)))
111}
112
113#[must_use]
118pub fn best_rational_approx(target: &Ratio<BigInt>, max_denom: &BigInt) -> Ratio<BigInt> {
119 debug_assert!(max_denom.is_positive());
120 if target.denom() <= max_denom {
121 return target.clone();
122 }
123
124 let negative = target.is_negative();
125 let t = target.abs();
126
127 let (mut h_prev, mut h) = (BigInt::one(), BigInt::zero()); let (mut k_prev, mut k) = (BigInt::zero(), BigInt::one()); let mut rem = t.clone();
131
132 loop {
133 let a = rem.floor().to_integer();
134 let h_next = &a * &h_prev + &h;
135 let k_next = &a * &k_prev + &k;
136
137 if &k_next > max_denom {
138 let m = (max_denom - &k) / &k_prev;
145 let candidate = if m.is_zero() {
146 None
147 } else {
148 Some(Ratio::new(&h + &m * &h_prev, &k + &m * &k_prev))
149 };
150 let conv = Ratio::new(h_prev.clone(), k_prev.clone());
151 let best = match candidate {
152 Some(semi) if (&semi - &t).abs() < (&conv - &t).abs() => semi,
153 _ => conv,
154 };
155 return if negative { -best } else { best };
156 }
157
158 h = std::mem::replace(&mut h_prev, h_next);
159 k = std::mem::replace(&mut k_prev, k_next);
160
161 let frac = &rem - Ratio::from_integer(a);
162 if frac.is_zero() {
163 let exact = Ratio::new(h_prev.clone(), k_prev.clone());
165 return if negative { -exact } else { exact };
166 }
167 rem = frac.recip();
168 }
169}
170
171#[must_use]
174pub fn ratio_to_f64(r: &Ratio<BigInt>) -> Option<f64> {
175 if let Some(v) = r.to_f64()
178 && v.is_finite()
179 {
180 return Some(v);
181 }
182 let shift = r.numer().bits().max(r.denom().bits()).saturating_sub(1000) as usize;
184 let n = r.numer() >> shift;
185 let d = r.denom() >> shift;
186 if d.is_zero() {
187 return None;
188 }
189 let v = n.to_f64()? / d.to_f64()?;
190 v.is_finite().then_some(v)
191}
192
193#[must_use]
195pub fn is_perfect_square(n: &BigInt) -> bool {
196 if n.is_negative() {
197 return false;
198 }
199 let r = n.sqrt();
200 &r * &r == *n
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 fn r(p: i64, q: i64) -> Ratio<BigInt> {
208 Ratio::new(BigInt::from(p), BigInt::from(q))
209 }
210
211 #[test]
212 fn exact_round_trips_through_f64() {
213 for &x in &[
214 0.0,
215 -0.0,
216 1.0,
217 -1.0,
218 0.5,
219 0.1,
220 1.0 / 3.0,
221 1e300,
222 -1e-300,
223 f64::MIN_POSITIVE,
224 f64::MAX,
225 5e-324, 123_456_789.987_654_3,
227 ] {
228 let ratio = f64_to_ratio_exact(x).unwrap();
229 let back = ratio_to_f64(&ratio).unwrap();
230 assert_eq!(back.to_bits(), (x + 0.0).to_bits(), "x = {x:e}");
231 }
232 }
233
234 #[test]
235 fn exact_rejects_non_finite() {
236 assert!(f64_to_ratio_exact(f64::NAN).is_none());
237 assert!(f64_to_ratio_exact(f64::INFINITY).is_none());
238 assert!(f64_to_ratio_exact(f64::NEG_INFINITY).is_none());
239 }
240
241 #[test]
242 fn exact_known_values() {
243 assert_eq!(f64_to_ratio_exact(0.75), Some(r(3, 4)));
244 assert_eq!(f64_to_ratio_exact(-1024.0), Some(r(-1024, 1)));
245 assert_eq!(f64_to_ratio_exact(1.5e3), Some(r(1500, 1)));
246 }
247
248 #[test]
249 fn approx_recovers_human_decimals() {
250 let cases = [
251 (0.1, 1, 10),
252 (0.2, 1, 5),
253 (0.3, 3, 10),
254 (0.25, 1, 4),
255 (0.125, 1, 8),
256 (2.0 / 3.0, 2, 3),
257 (0.142857142857, 1, 7),
258 ];
259 for &(x, p, q) in &cases {
260 assert_eq!(f64_to_ratio_approx(x, 1_000_000), Some(r(p, q)), "x = {x}");
261 }
262 let best = f64_to_ratio_approx(std::f64::consts::SQRT_2, 100).unwrap();
266 assert!(*best.denom() <= BigInt::from(100));
267 let exact = f64_to_ratio_exact(std::f64::consts::SQRT_2).unwrap();
268 for &(p, q) in &[(99_i64, 70_i64), (141, 100), (17, 12), (7, 5)] {
269 assert!(
270 (&best - &exact).abs() <= (&r(p, q) - &exact).abs(),
271 "{best} is worse than {p}/{q}"
272 );
273 }
274 }
275
276 #[test]
277 fn approx_respects_denominator_bound() {
278 for &md in &[1u64, 2, 3, 7, 10, 100, 1000, 1_000_000] {
279 for &x in &[
280 0.1,
281 0.7,
282 std::f64::consts::PI,
283 -std::f64::consts::E,
284 12345.6789,
285 ] {
286 let a = f64_to_ratio_approx(x, md).unwrap();
287 assert!(*a.denom() <= BigInt::from(md), "x={x}, md={md}, got {a}");
288 let naive = Ratio::new(
290 BigInt::from((x * md as f64).round() as i64),
291 BigInt::from(md),
292 );
293 let exact = f64_to_ratio_exact(x).unwrap();
294 assert!(
295 (&a - &exact).abs() <= (&naive - &exact).abs(),
296 "x={x}, md={md}: {a} worse than naive {naive}"
297 );
298 }
299 }
300 }
301
302 #[test]
303 fn approx_negative_and_integer() {
304 assert_eq!(f64_to_ratio_approx(-0.5, 10), Some(r(-1, 2)));
305 assert_eq!(f64_to_ratio_approx(-7.0, 10), Some(r(-7, 1)));
306 assert_eq!(f64_to_ratio_approx(0.0, 10), Some(r(0, 1)));
307 assert!(f64_to_ratio_approx(1.0, 0).is_none());
308 }
309
310 #[test]
311 fn best_rational_exact_when_within_bound() {
312 let t = r(22, 7);
313 assert_eq!(best_rational_approx(&t, &BigInt::from(7)), t);
314 assert_eq!(best_rational_approx(&t, &BigInt::from(1000)), t);
315 }
316
317 #[test]
318 fn perfect_square() {
319 assert!(is_perfect_square(&BigInt::from(0)));
320 assert!(is_perfect_square(&BigInt::from(144)));
321 assert!(!is_perfect_square(&BigInt::from(145)));
322 assert!(!is_perfect_square(&BigInt::from(-4)));
323 }
324}