use num_bigint::{BigInt, Sign};
use num_rational::Ratio;
use num_traits::{One, Signed, ToPrimitive, Zero};
pub type Q = Ratio<BigInt>;
pub fn q(n: i64, d: i64) -> Q {
Ratio::new(BigInt::from(n), BigInt::from(d))
}
pub fn qi(n: i64) -> Q {
Ratio::from_integer(BigInt::from(n))
}
#[must_use]
pub fn f64_to_ratio_exact(x: f64) -> Option<Ratio<BigInt>> {
if !x.is_finite() {
return None;
}
if x == 0.0 {
return Some(Ratio::zero());
}
let bits = x.to_bits();
let sign = if bits >> 63 == 0 {
Sign::Plus
} else {
Sign::Minus
};
let exponent = ((bits >> 52) & 0x7ff) as i64;
let fraction = bits & 0x000f_ffff_ffff_ffff;
let (mantissa, exp2) = if exponent == 0 {
(fraction, -1074_i64)
} else {
(fraction | (1_u64 << 52), exponent - 1075)
};
let mantissa = BigInt::from_biguint(sign, mantissa.into());
let ratio = if exp2 >= 0 {
Ratio::from_integer(mantissa << (exp2 as usize))
} else {
Ratio::new(mantissa, BigInt::one() << ((-exp2) as usize))
};
Some(ratio)
}
#[must_use]
pub fn f64_to_ratio_approx(x: f64, max_denom: u64) -> Option<Ratio<BigInt>> {
if max_denom == 0 {
return None;
}
let exact = f64_to_ratio_exact(x)?;
Some(best_rational_approx(&exact, &BigInt::from(max_denom)))
}
#[must_use]
pub fn best_rational_approx(target: &Ratio<BigInt>, max_denom: &BigInt) -> Ratio<BigInt> {
debug_assert!(max_denom.is_positive());
if target.denom() <= max_denom {
return target.clone();
}
let negative = target.is_negative();
let t = target.abs();
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();
loop {
let a = rem.floor().to_integer();
let h_next = &a * &h_prev + &h;
let k_next = &a * &k_prev + &k;
if &k_next > max_denom {
let m = (max_denom - &k) / &k_prev;
let candidate = if m.is_zero() {
None
} else {
Some(Ratio::new(&h + &m * &h_prev, &k + &m * &k_prev))
};
let conv = Ratio::new(h_prev.clone(), k_prev.clone());
let best = match candidate {
Some(semi) if (&semi - &t).abs() < (&conv - &t).abs() => semi,
_ => conv,
};
return if negative { -best } else { best };
}
h = std::mem::replace(&mut h_prev, h_next);
k = std::mem::replace(&mut k_prev, k_next);
let frac = &rem - Ratio::from_integer(a);
if frac.is_zero() {
let exact = Ratio::new(h_prev.clone(), k_prev.clone());
return if negative { -exact } else { exact };
}
rem = frac.recip();
}
}
#[must_use]
pub fn ratio_to_f64(r: &Ratio<BigInt>) -> Option<f64> {
if let Some(v) = r.to_f64()
&& v.is_finite()
{
return Some(v);
}
let shift = r.numer().bits().max(r.denom().bits()).saturating_sub(1000) as usize;
let n = r.numer() >> shift;
let d = r.denom() >> shift;
if d.is_zero() {
return None;
}
let v = n.to_f64()? / d.to_f64()?;
v.is_finite().then_some(v)
}
#[must_use]
pub fn is_perfect_square(n: &BigInt) -> bool {
if n.is_negative() {
return false;
}
let r = n.sqrt();
&r * &r == *n
}
#[cfg(test)]
mod tests {
use super::*;
fn r(p: i64, q: i64) -> Ratio<BigInt> {
Ratio::new(BigInt::from(p), BigInt::from(q))
}
#[test]
fn exact_round_trips_through_f64() {
for &x in &[
0.0,
-0.0,
1.0,
-1.0,
0.5,
0.1,
1.0 / 3.0,
1e300,
-1e-300,
f64::MIN_POSITIVE,
f64::MAX,
5e-324, 123_456_789.987_654_3,
] {
let ratio = f64_to_ratio_exact(x).unwrap();
let back = ratio_to_f64(&ratio).unwrap();
assert_eq!(back.to_bits(), (x + 0.0).to_bits(), "x = {x:e}");
}
}
#[test]
fn exact_rejects_non_finite() {
assert!(f64_to_ratio_exact(f64::NAN).is_none());
assert!(f64_to_ratio_exact(f64::INFINITY).is_none());
assert!(f64_to_ratio_exact(f64::NEG_INFINITY).is_none());
}
#[test]
fn exact_known_values() {
assert_eq!(f64_to_ratio_exact(0.75), Some(r(3, 4)));
assert_eq!(f64_to_ratio_exact(-1024.0), Some(r(-1024, 1)));
assert_eq!(f64_to_ratio_exact(1.5e3), Some(r(1500, 1)));
}
#[test]
fn approx_recovers_human_decimals() {
let cases = [
(0.1, 1, 10),
(0.2, 1, 5),
(0.3, 3, 10),
(0.25, 1, 4),
(0.125, 1, 8),
(2.0 / 3.0, 2, 3),
(0.142857142857, 1, 7),
];
for &(x, p, q) in &cases {
assert_eq!(f64_to_ratio_approx(x, 1_000_000), Some(r(p, q)), "x = {x}");
}
let best = f64_to_ratio_approx(std::f64::consts::SQRT_2, 100).unwrap();
assert!(*best.denom() <= BigInt::from(100));
let exact = f64_to_ratio_exact(std::f64::consts::SQRT_2).unwrap();
for &(p, q) in &[(99_i64, 70_i64), (141, 100), (17, 12), (7, 5)] {
assert!(
(&best - &exact).abs() <= (&r(p, q) - &exact).abs(),
"{best} is worse than {p}/{q}"
);
}
}
#[test]
fn approx_respects_denominator_bound() {
for &md in &[1u64, 2, 3, 7, 10, 100, 1000, 1_000_000] {
for &x in &[
0.1,
0.7,
std::f64::consts::PI,
-std::f64::consts::E,
12345.6789,
] {
let a = f64_to_ratio_approx(x, md).unwrap();
assert!(*a.denom() <= BigInt::from(md), "x={x}, md={md}, got {a}");
let naive = Ratio::new(
BigInt::from((x * md as f64).round() as i64),
BigInt::from(md),
);
let exact = f64_to_ratio_exact(x).unwrap();
assert!(
(&a - &exact).abs() <= (&naive - &exact).abs(),
"x={x}, md={md}: {a} worse than naive {naive}"
);
}
}
}
#[test]
fn approx_negative_and_integer() {
assert_eq!(f64_to_ratio_approx(-0.5, 10), Some(r(-1, 2)));
assert_eq!(f64_to_ratio_approx(-7.0, 10), Some(r(-7, 1)));
assert_eq!(f64_to_ratio_approx(0.0, 10), Some(r(0, 1)));
assert!(f64_to_ratio_approx(1.0, 0).is_none());
}
#[test]
fn best_rational_exact_when_within_bound() {
let t = r(22, 7);
assert_eq!(best_rational_approx(&t, &BigInt::from(7)), t);
assert_eq!(best_rational_approx(&t, &BigInt::from(1000)), t);
}
#[test]
fn perfect_square() {
assert!(is_perfect_square(&BigInt::from(0)));
assert!(is_perfect_square(&BigInt::from(144)));
assert!(!is_perfect_square(&BigInt::from(145)));
assert!(!is_perfect_square(&BigInt::from(-4)));
}
}