use oxinum_core::{OxiNumError, OxiNumResult};
use oxinum_int::native::{divrem_int, BigInt, BigUint};
use super::BigRational;
fn floor_div(num: &BigInt, den: &BigInt) -> (BigInt, BigInt) {
let (mut q, mut r) = divrem_int(num, den);
if r.is_negative() {
q = &q - &BigInt::one();
r = &r + den;
}
(q, r)
}
impl BigRational {
pub fn continued_fraction(&self) -> Vec<BigInt> {
let mut coeffs = Vec::new();
let mut num = self.num.clone();
let mut den = BigInt::from(self.den.clone());
loop {
let (a, r) = floor_div(&num, &den);
coeffs.push(a);
if r.is_zero() {
break;
}
num = den;
den = r;
}
coeffs
}
pub fn from_continued_fraction(coeffs: &[BigInt]) -> OxiNumResult<Self> {
let (last, rest) = coeffs
.split_last()
.ok_or_else(|| OxiNumError::Parse("empty continued fraction".into()))?;
let mut result = BigRational::from_integer(last.clone());
for coeff in rest.iter().rev() {
if result.is_zero() {
return Err(OxiNumError::DivByZero);
}
let reciprocal = result.recip()?;
result = BigRational::from_integer(coeff.clone()) + reciprocal;
}
Ok(result)
}
pub fn convergents(&self) -> Vec<BigRational> {
let coeffs = self.continued_fraction();
let mut h_prev2 = BigInt::ZERO; let mut h_prev1 = BigInt::one(); let mut k_prev2 = BigInt::one(); let mut k_prev1 = BigInt::ZERO;
let mut out = Vec::with_capacity(coeffs.len());
for a in &coeffs {
let h = &(a * &h_prev1) + &h_prev2;
let k = &(a * &k_prev1) + &k_prev2;
out.push(convergent_from_signed(&h, &k));
h_prev2 = h_prev1;
h_prev1 = h;
k_prev2 = k_prev1;
k_prev1 = k;
}
out
}
pub fn best_rational_approximation(&self, max_den: &BigUint) -> BigRational {
if max_den.is_zero() {
let coeffs = self.continued_fraction();
return match coeffs.first() {
Some(a0) => BigRational::from_integer(a0.clone()),
None => BigRational::zero(),
};
}
let coeffs = self.continued_fraction();
let max_den_i = BigInt::from(max_den.clone());
let mut h_prev2 = BigInt::ZERO; let mut h_prev1 = BigInt::one(); let mut k_prev2 = BigInt::one(); let mut k_prev1 = BigInt::ZERO;
let mut best = match coeffs.first() {
Some(a0) => BigRational::from_integer(a0.clone()),
None => return BigRational::zero(),
};
let mut have_best = false;
for a in &coeffs {
let h = &(a * &h_prev1) + &h_prev2;
let k = &(a * &k_prev1) + &k_prev2;
if k > max_den_i {
let (t, _) = floor_div(&(&max_den_i - &k_prev2), &k_prev1);
let semi_h = &(&t * &h_prev1) + &h_prev2;
let semi_k = &(&t * &k_prev1) + &k_prev2;
let semi = convergent_from_signed(&semi_h, &semi_k);
best = pick_closer(self, best, have_best, semi);
break;
}
best = convergent_from_signed(&h, &k);
have_best = true;
h_prev2 = h_prev1;
h_prev1 = h;
k_prev2 = k_prev1;
k_prev1 = k;
}
best
}
}
fn convergent_from_signed(h: &BigInt, k: &BigInt) -> BigRational {
let (_k_sign, k_mag) = k.clone().into_parts();
BigRational::reduce_unchecked(h.clone(), k_mag)
}
fn pick_closer(
target: &BigRational,
current: BigRational,
have_current: bool,
candidate: BigRational,
) -> BigRational {
if !have_current {
return candidate;
}
let err_current = (¤t - target).abs();
let err_candidate = (&candidate - target).abs();
match err_candidate.cmp(&err_current) {
core::cmp::Ordering::Less => candidate,
core::cmp::Ordering::Greater => current,
core::cmp::Ordering::Equal => {
if candidate.den() < current.den() {
candidate
} else {
current
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn br(n: i64, d: u64) -> BigRational {
BigRational::from_parts(BigInt::from(n), BigUint::from_u64(d)).expect("br")
}
fn ints(vals: &[i64]) -> Vec<BigInt> {
vals.iter().map(|&v| BigInt::from(v)).collect()
}
#[test]
fn cf_classic_vector() {
assert_eq!(br(415, 93).continued_fraction(), ints(&[4, 2, 6, 7]));
}
#[test]
fn cf_pi_convergent() {
assert_eq!(br(355, 113).continued_fraction(), ints(&[3, 7, 16]));
}
#[test]
fn cf_negative_uses_floor() {
assert_eq!(br(-415, 93).continued_fraction(), ints(&[-5, 1, 1, 6, 7]));
}
#[test]
fn cf_integer_single_term() {
assert_eq!(br(5, 1).continued_fraction(), ints(&[5]));
assert_eq!(br(-7, 1).continued_fraction(), ints(&[-7]));
assert_eq!(BigRational::zero().continued_fraction(), ints(&[0]));
}
#[test]
fn cf_unit_fraction() {
assert_eq!(br(1, 7).continued_fraction(), ints(&[0, 7]));
}
#[test]
fn from_cf_roundtrip_classic() {
let r = br(415, 93);
let back = BigRational::from_continued_fraction(&r.continued_fraction()).expect("ok");
assert_eq!(back, r);
}
#[test]
fn from_cf_empty_errors() {
assert_eq!(
BigRational::from_continued_fraction(&[]),
Err(OxiNumError::Parse("empty continued fraction".into()))
);
}
#[test]
fn from_cf_zero_chain_div_by_zero() {
assert_eq!(
BigRational::from_continued_fraction(&ints(&[0, 0])),
Err(OxiNumError::DivByZero)
);
}
#[test]
fn convergents_last_equals_self() {
let r = br(355, 113);
let convs = r.convergents();
assert_eq!(*convs.last().expect("non-empty"), r);
}
#[test]
fn convergents_strictly_improve() {
let r = br(415, 93);
let convs = r.convergents();
let mut prev_err: Option<BigRational> = None;
for c in &convs {
let err = (c - &r).abs();
if let Some(p) = prev_err {
assert!(err < p, "convergent errors must strictly decrease");
}
prev_err = Some(err);
}
}
#[test]
fn best_approx_semiconvergent() {
assert_eq!(
br(355, 113).best_rational_approximation(&BigUint::from_u64(100)),
br(311, 99)
);
}
#[test]
fn best_approx_exact_when_bound_allows() {
assert_eq!(
br(355, 113).best_rational_approximation(&BigUint::from_u64(113)),
br(355, 113)
);
}
#[test]
fn best_approx_convergent_bounds() {
assert_eq!(
br(355, 113).best_rational_approximation(&BigUint::from_u64(10)),
br(22, 7)
);
assert_eq!(
br(355, 113).best_rational_approximation(&BigUint::from_u64(7)),
br(22, 7)
);
}
#[test]
fn best_approx_denom_zero_is_floor() {
assert_eq!(
br(7, 2).best_rational_approximation(&BigUint::ZERO),
BigRational::from_i64(3)
);
assert_eq!(
br(-7, 2).best_rational_approximation(&BigUint::ZERO),
BigRational::from_i64(-4)
);
}
#[test]
fn floor_div_matches_floor_semantics() {
let (q, r) = floor_div(&BigInt::from(-7i64), &BigInt::from(3i64));
assert_eq!(q, BigInt::from(-3i64));
assert_eq!(r, BigInt::from(2i64));
let (q, r) = floor_div(&BigInt::from(7i64), &BigInt::from(3i64));
assert_eq!(q, BigInt::from(2i64));
assert_eq!(r, BigInt::from(1i64));
}
}