use astro_float::{BigFloat, RoundingMode};
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)
}
pub(crate) fn bigint_to_bigfloat(n: &BigInt, prec: usize) -> BigFloat {
if let Some(v) = n.to_i128() {
let mut out = BigFloat::from_i128(v, prec.max(128));
if prec < 128 {
let _ = out.set_precision(prec, RoundingMode::ToEven);
}
return out;
}
let (sign, digits) = n.to_u32_digits();
let per_word = astro_float::WORD_BIT_SIZE / 32;
let words: Vec<astro_float::Word> = digits
.chunks(per_word)
.map(|chunk| {
chunk
.iter()
.enumerate()
.fold(0 as astro_float::Word, |w, (i, &d)| {
w | (astro_float::Word::from(d) << (32 * i))
})
})
.collect();
let sign = if sign == Sign::Minus {
astro_float::Sign::Neg
} else {
astro_float::Sign::Pos
};
let exponent = words
.len()
.checked_mul(astro_float::WORD_BIT_SIZE)
.and_then(|b| astro_float::Exponent::try_from(b).ok());
let Some(exponent) = exponent else {
return if sign == astro_float::Sign::Neg {
astro_float::INF_NEG
} else {
astro_float::INF_POS
};
};
let mut out = BigFloat::from_words(&words, sign, exponent);
let _ = out.set_precision(prec, RoundingMode::ToEven);
out
}
pub(crate) fn ratio_to_bigfloat(r: &Ratio<BigInt>, prec: usize, rm: RoundingMode) -> BigFloat {
if r.is_zero() {
return BigFloat::from_i32(0, prec);
}
if r.denom().is_one() {
return bigint_to_bigfloat(r.numer(), prec);
}
let wp = prec + 64;
let n = bigint_to_bigfloat(r.numer(), wp);
let d = bigint_to_bigfloat(r.denom(), wp);
n.div(&d, prec, rm)
}
fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
}
pub(crate) fn gcd_big(a: &BigInt, b: &BigInt) -> BigInt {
let (x, y) = (a.magnitude(), b.magnitude());
let (mut big, mut small) = if x >= y {
(x.clone(), y.clone())
} else {
(y.clone(), x.clone())
};
loop {
if small.is_zero() {
return BigInt::from(big);
}
if let Some(s) = small.to_u64() {
let t = (&big % s).to_u64().unwrap_or(0);
return BigInt::from(gcd_u64(s, t));
}
if big.bits() <= small.bits() + 64 {
return BigInt::from(num_integer::Integer::gcd(&big, &small));
}
let r = &big % &small;
big = small;
small = r;
}
}
fn div_by(x: &BigInt, g: &BigInt) -> BigInt {
if g.is_one() { x.clone() } else { x / g }
}
pub(crate) fn q_add(a: &Q, b: &Q) -> Q {
if a.is_zero() {
return b.clone();
}
if b.is_zero() {
return a.clone();
}
match (a.is_integer(), b.is_integer()) {
(true, true) => Ratio::from_integer(a.numer() + b.numer()),
(false, true) => Ratio::new_raw(a.numer() + b.numer() * a.denom(), a.denom().clone()),
(true, false) => Ratio::new_raw(b.numer() + a.numer() * b.denom(), b.denom().clone()),
(false, false) => {
let (u, u1, v, v1) = (a.numer(), a.denom(), b.numer(), b.denom());
let d1 = gcd_big(u1, v1);
if d1.is_one() {
return Ratio::new_raw(u * v1 + v * u1, u1 * v1);
}
let t = u * div_by(v1, &d1) + v * div_by(u1, &d1);
if t.is_zero() {
return Ratio::zero();
}
let d2 = gcd_big(&t, &d1);
Ratio::new_raw(div_by(&t, &d2), div_by(u1, &d1) * div_by(v1, &d2))
}
}
}
pub(crate) fn q_mul(a: &Q, b: &Q) -> Q {
if a.is_zero() || b.is_zero() {
return Ratio::zero();
}
if a.is_one() {
return b.clone();
}
if b.is_one() {
return a.clone();
}
if a.is_integer() && b.is_integer() {
return Ratio::from_integer(a.numer() * b.numer());
}
let g1 = gcd_big(a.numer(), b.denom());
let g2 = gcd_big(b.numer(), a.denom());
let n = div_by(a.numer(), &g1) * div_by(b.numer(), &g2);
let d = div_by(a.denom(), &g2) * div_by(b.denom(), &g1);
if d.is_negative() {
Ratio::new_raw(-n, -d)
} else {
Ratio::new_raw(n, d)
}
}
#[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)));
}
#[test]
fn fast_rational_arithmetic_matches_ratio() {
use num_traits::Pow;
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let big = |k: u32, c: i64| Pow::pow(BigInt::from(10), k) + BigInt::from(c);
let mut ints: Vec<BigInt> = vec![
BigInt::from(0),
BigInt::from(1),
BigInt::from(-1),
BigInt::from(2),
BigInt::from(6),
BigInt::from(-12),
BigInt::from(u64::MAX),
big(40, 7),
big(40, 7) * 6,
big(300, -3),
Pow::pow(BigInt::from(2), 200u32) * 3,
-big(25, 1),
];
for _ in 0..12 {
let v = BigInt::from(next()) * BigInt::from(next()) - BigInt::from(next());
ints.push(if next() % 2 == 0 { v } else { -v });
}
let mut qs: Vec<Q> = Vec::new();
for (i, n) in ints.iter().enumerate() {
qs.push(Ratio::from_integer(n.clone()));
let d = &ints[(i * 7 + 3) % ints.len()];
if !d.is_zero() {
qs.push(Ratio::new(n.clone(), d.clone()));
}
}
for a in &qs {
for b in &qs {
let (sum, prod) = (q_add(a, b), q_mul(a, b));
assert_eq!(sum, a.clone() + b.clone(), "{a} + {b}");
assert_eq!(prod, a.clone() * b.clone(), "{a} * {b}");
for q in [&sum, &prod] {
assert!(q.denom().is_positive(), "{q}");
assert!(
num_integer::Integer::gcd(q.numer(), q.denom()).is_one(),
"{q}"
);
}
}
}
for a in &ints {
for b in &ints {
assert_eq!(
gcd_big(a, b),
num_integer::Integer::gcd(a, b),
"gcd({a}, {b})"
);
}
}
}
}