use astro_float::{BigFloat, RoundingMode};
use num_bigint::{BigInt, BigUint, 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)
}
pub(crate) fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
if a == 0 {
return b;
}
if b == 0 {
return a;
}
let shift = (a | b).trailing_zeros();
a >>= a.trailing_zeros();
loop {
b >>= b.trailing_zeros();
if a > b {
std::mem::swap(&mut a, &mut b);
}
b -= a;
if b == 0 {
return a << shift;
}
}
}
pub(crate) fn gcd_u128(mut a: u128, mut b: u128) -> u128 {
while b != 0 {
(a, b) = (b, a % b);
}
a
}
pub(crate) fn gcd_big(a: &BigInt, b: &BigInt) -> BigInt {
let (x, y) = (a.magnitude(), b.magnitude());
let (big, small) = if x >= y { (x, y) } else { (y, x) };
if small.is_zero() {
return BigInt::from(big.clone());
}
if let Some(s) = small.to_u64() {
let t = match big.to_u64() {
Some(b) => b % s,
None => (big % s).to_u64().unwrap_or(0),
};
return BigInt::from(gcd_u64(s, t));
}
if big.bits() <= 128 {
let (x, y) = (big.to_u128().unwrap_or(0), small.to_u128().unwrap_or(0));
return BigInt::from(gcd_u128(x, y));
}
BigInt::from_biguint(Sign::Plus, uint_gcd(big, small))
}
fn uint_gcd(a: &BigUint, b: &BigUint) -> BigUint {
let (mut u, mut v) = if a >= b {
(a.to_u64_digits(), b.to_u64_digits())
} else {
(b.to_u64_digits(), a.to_u64_digits())
};
loop {
limbs_trim(&mut u);
limbs_trim(&mut v);
if v.len() <= 2 {
let (ub, vb) = (limbs_to_big(&u), limbs_to_big(&v));
if vb.is_zero() {
return ub;
}
let r = (&ub % &vb).to_u128().unwrap_or(0);
return BigUint::from(gcd_u128(vb.to_u128().unwrap_or(0), r));
}
let (ubits, vbits) = (limbs_bits(&u), limbs_bits(&v));
let division_step = |u: &mut Vec<u64>, v: &mut Vec<u64>| {
let (ub, vb) = (limbs_to_big(u), limbs_to_big(v));
let r = &ub % &vb;
*u = std::mem::take(v);
*v = r.to_u64_digits();
};
if ubits - vbits > 32 {
division_step(&mut u, &mut v);
continue;
}
let shift = ubits - 62;
let x = limbs_window(&u, shift) & ((1 << 62) - 1);
let y = limbs_window(&v, shift) & ((1 << 62) - 1);
let coeffs = lehmer_cosequence(x, y);
let bounded = coeffs.iter().all(|c| c.unsigned_abs() < 1u64 << 62);
if coeffs[1] == 0 || !bounded {
division_step(&mut u, &mut v);
continue;
}
v.resize(u.len(), 0);
if !lehmer_apply(&mut u, &mut v, coeffs) {
return num_integer::Integer::gcd(a, b);
}
limbs_trim(&mut u);
limbs_trim(&mut v);
if limbs_cmp(&u, &v) == std::cmp::Ordering::Less {
std::mem::swap(&mut u, &mut v);
}
}
}
fn lehmer_apply(u: &mut [u64], v: &mut [u64], [a, b, c, d]: [i64; 4]) -> bool {
let (a, b, c, d) = (i128::from(a), i128::from(b), i128::from(c), i128::from(d));
let (mut cu, mut cv) = (0i128, 0i128);
for (ui, vi) in u.iter_mut().zip(v.iter_mut()) {
let (x, y) = (i128::from(*ui), i128::from(*vi));
let su = a * x + b * y + cu;
let sv = c * x + d * y + cv;
*ui = su as u64;
*vi = sv as u64;
cu = su >> 64;
cv = sv >> 64;
}
cu == 0 && cv == 0
}
fn limbs_trim(x: &mut Vec<u64>) {
while x.last() == Some(&0) {
x.pop();
}
}
fn limbs_bits(x: &[u64]) -> u64 {
x.last().map_or(0, |&top| {
64 * (x.len() as u64 - 1) + (64 - u64::from(top.leading_zeros()))
})
}
fn limbs_window(x: &[u64], shift: u64) -> u64 {
let (w, off) = ((shift / 64) as usize, (shift % 64) as u32);
let lo = x.get(w).copied().unwrap_or(0) >> off;
let hi = if off == 0 {
0
} else {
x.get(w + 1).copied().unwrap_or(0) << (64 - off)
};
lo | hi
}
fn limbs_cmp(x: &[u64], y: &[u64]) -> std::cmp::Ordering {
x.len()
.cmp(&y.len())
.then_with(|| x.iter().rev().cmp(y.iter().rev()))
}
fn limbs_to_big(x: &[u64]) -> BigUint {
let digits: Vec<u32> = x
.iter()
.flat_map(|&w| [w as u32, (w >> 32) as u32])
.collect();
BigUint::new(digits)
}
fn lehmer_cosequence(x: u64, y: u64) -> [i64; 4] {
let (mut a, mut b, mut c, mut d) = (1i64, 0i64, 0i64, 1i64);
let (mut x, mut y) = (x as i64, y as i64);
loop {
let (yc, yd) = (y + c, y + d);
if yc <= 0 || yd <= 0 {
break;
}
let (xa, xb) = (x + a, x + b);
if xa < 0 || xb < 0 {
break;
}
let q = xa / yc;
if q != xb / yd {
break;
}
(a, c) = (c, a.wrapping_sub(q.wrapping_mul(c)));
(b, d) = (d, b.wrapping_sub(q.wrapping_mul(d)));
(x, y) = (y, x.wrapping_sub(q.wrapping_mul(y)));
if [a, b, c, d].iter().any(|t| t.unsigned_abs() >= 1u64 << 62) {
break;
}
}
[a, b, c, d]
}
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})"
);
}
}
}
}