#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use core::cmp::Ordering;
use crate::backend::{TickInt, Ticks};
use crate::error::{Code, Result, TimeError};
use crate::value::Rounding;
pub type NumError = TimeError;
pub fn mul_div(a: &Ticks, n: &Ticks, d: &Ticks) -> Result<(Ticks, Ticks)> {
if d.is_zero_ticks() {
return Err(TimeError::with_context(
Code::E0070,
"mul_div: zero divisor",
));
}
let wide = a.wide_mul(n);
let (q, r) = <Ticks as TickInt>::wide_quot_rem(&wide, d);
let q = <Ticks as TickInt>::narrow(&q).ok_or(TimeError::with_context(
Code::E0021,
"mul_div: quotient exceeds the domain",
))?;
Ok((q, r))
}
pub fn mul_div_rounded(a: &Ticks, n: &Ticks, d: &Ticks, mode: Rounding) -> Result<Ticks> {
let (q, r) = mul_div(a, n, d)?;
if r.is_zero_ticks() {
return Ok(q);
}
let up = match mode {
Rounding::Trunc => false,
Rounding::Ceil => true,
Rounding::HalfUp | Rounding::HalfEven => {
let twice = r
.try_add(&r)
.ok_or(TimeError::with_context(Code::E0021, "2r overflowed"))?;
match twice.cmp(d) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => match mode {
Rounding::HalfUp => true,
_ => q.is_odd(),
},
}
}
};
if up {
q.try_add(&<Ticks as TickInt>::one())
.ok_or(TimeError::new(Code::E0021))
} else {
Ok(q)
}
}
pub fn isqrt_floor(x: &Ticks) -> Ticks {
let one = <Ticks as TickInt>::one();
if x.is_zero_ticks() || *x == one {
return x.clone();
}
let bits = x.bit_len();
let start_exp = bits.div_ceil(2);
let mut r = <Ticks as TickInt>::pow2(start_exp)
.unwrap_or_else(<Ticks as TickInt>::domain_max);
let two = <Ticks as TickInt>::from_u64(2);
loop {
let (q, _) = x.quot_rem(&r);
let sum = match r.try_add(&q) {
Some(s) => s,
None => {
let (half, _) = r.quot_rem(&two);
r = half;
continue;
}
};
let (next, _) = sum.quot_rem(&two);
if next >= r {
break;
}
r = next;
}
debug_assert!(
&r.wide_mul(&r) <= &x.wide_mul(&<Ticks as TickInt>::one()),
"isqrt_floor post-condition r^2 <= x violated"
);
debug_assert!(
{
let rp1 = r.try_add(&one).expect("r+1 within domain for any real root");
rp1.wide_mul(&rp1) > x.wide_mul(&one)
},
"isqrt_floor post-condition x < (r+1)^2 violated"
);
r
}
pub fn isqrt_ceil(x: &Ticks) -> Ticks {
let f = isqrt_floor(x);
let one = <Ticks as TickInt>::one();
if f.wide_mul(&f) == x.wide_mul(&one) {
f
} else {
f.try_add(&one)
.expect("ceil of a root inside the domain stays inside it")
}
}
#[cfg_attr(feature = "u512", derive(Copy))]
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Ratio {
num: Ticks,
den: Ticks,
}
pub fn gcd(a: &Ticks, b: &Ticks) -> Ticks {
let mut a = a.clone();
let mut b = b.clone();
while !b.is_zero_ticks() {
let (_, r) = a.quot_rem(&b);
a = b;
b = r;
}
a
}
impl Ratio {
pub fn new(num: Ticks, den: Ticks) -> Result<Ratio> {
if den.is_zero_ticks() {
return Err(TimeError::with_context(
Code::E0070,
"rational with a zero denominator",
));
}
let g = gcd(&num, &den);
let one = <Ticks as TickInt>::one();
if g == one || g.is_zero_ticks() {
return Ok(Ratio { num, den });
}
let (n, _) = num.quot_rem(&g);
let (d, _) = den.quot_rem(&g);
Ok(Ratio { num: n, den: d })
}
pub fn from_int(n: Ticks) -> Ratio {
Ratio {
num: n,
den: <Ticks as TickInt>::one(),
}
}
pub fn from_u64(n: u64) -> Ratio {
Ratio::from_int(<Ticks as TickInt>::from_u64(n))
}
pub fn zero() -> Ratio {
Ratio::from_u64(0)
}
pub fn one() -> Ratio {
Ratio::from_u64(1)
}
pub fn from_decimal_str(s: &str) -> Result<Ratio> {
let malformed = TimeError::with_context(Code::E0001, "not an exact decimal");
let (int, frac) = match s.split_once('.') {
None => (s, ""),
Some((i, f)) => (i, f),
};
if int.is_empty() && frac.is_empty() {
return Err(malformed);
}
if !int.bytes().all(|b| b.is_ascii_digit()) || !frac.bytes().all(|b| b.is_ascii_digit()) {
return Err(malformed);
}
let scale = frac.len() as u32;
let mut digits = heapless_concat(int, frac);
if digits.is_empty() {
digits = "0".into();
}
let num = <Ticks as TickInt>::from_dec_str(&digits).ok_or(malformed)?;
let den = pow10(scale)?;
Ratio::new(num, den)
}
pub fn numer(&self) -> &Ticks {
&self.num
}
pub fn denom(&self) -> &Ticks {
&self.den
}
pub fn is_zero(&self) -> bool {
self.num.is_zero_ticks()
}
pub fn is_integer(&self) -> bool {
self.den == <Ticks as TickInt>::one()
}
pub fn floor(&self) -> Ticks {
self.num.quot_rem(&self.den).0
}
pub fn frac(&self) -> Ratio {
let (_, r) = self.num.quot_rem(&self.den);
Ratio {
num: r,
den: self.den.clone(),
}
}
pub fn add(&self, other: &Ratio) -> Result<Ratio> {
let g = gcd(&self.den, &other.den);
let (od, _) = other.den.quot_rem(&g);
let (sd, _) = self.den.quot_rem(&g);
let a = self.num.try_mul(&od).ok_or(overflow())?;
let b = other.num.try_mul(&sd).ok_or(overflow())?;
let num = a.try_add(&b).ok_or(overflow())?;
let den = self.den.try_mul(&od).ok_or(overflow())?;
Ratio::new(num, den)
}
pub fn sub(&self, other: &Ratio) -> Result<Ratio> {
if self.cmp_exact(other) == Ordering::Less {
return Err(TimeError::with_context(
Code::E0020,
"rational subtraction would be negative; use abs_diff",
));
}
let g = gcd(&self.den, &other.den);
let (od, _) = other.den.quot_rem(&g);
let (sd, _) = self.den.quot_rem(&g);
let a = self.num.try_mul(&od).ok_or(overflow())?;
let b = other.num.try_mul(&sd).ok_or(overflow())?;
let num = a.try_sub(&b).ok_or(overflow())?;
let den = self.den.try_mul(&od).ok_or(overflow())?;
Ratio::new(num, den)
}
pub fn abs_diff(&self, other: &Ratio) -> Result<Ratio> {
match self.cmp_exact(other) {
Ordering::Less => other.sub(self),
_ => self.sub(other),
}
}
pub fn mul(&self, other: &Ratio) -> Result<Ratio> {
let g1 = gcd(&self.num, &other.den);
let g2 = gcd(&other.num, &self.den);
let (n1, _) = self.num.quot_rem(&g1);
let (d2, _) = other.den.quot_rem(&g1);
let (n2, _) = other.num.quot_rem(&g2);
let (d1, _) = self.den.quot_rem(&g2);
let num = n1.try_mul(&n2).ok_or(overflow())?;
let den = d1.try_mul(&d2).ok_or(overflow())?;
Ratio::new(num, den)
}
pub fn div(&self, other: &Ratio) -> Result<Ratio> {
if other.is_zero() {
return Err(TimeError::with_context(
Code::E0070,
"rational division by zero",
));
}
self.mul(&other.recip()?)
}
pub fn recip(&self) -> Result<Ratio> {
if self.is_zero() {
return Err(TimeError::with_context(
Code::E0070,
"reciprocal of zero",
));
}
Ok(Ratio {
num: self.den.clone(),
den: self.num.clone(),
})
}
pub fn cmp_exact(&self, other: &Ratio) -> Ordering {
self.num
.wide_mul(&other.den)
.cmp(&other.num.wide_mul(&self.den))
}
pub fn snap(&self, digits: u32, mode: Rounding) -> Result<Ratio> {
let scale = pow10(digits)?;
let num = mul_div_rounded(&self.num, &scale, &self.den, mode)?;
Ratio::new(num, scale)
}
#[cfg(feature = "alloc")]
pub fn to_decimal_string(&self, digits: u32, mode: Rounding) -> Result<String> {
use alloc::format;
let scale = pow10(digits)?;
let scaled = mul_div_rounded(&self.num, &scale, &self.den, mode)?;
let s = scaled.to_dec_string();
if digits == 0 {
return Ok(s);
}
let d = digits as usize;
Ok(if s.len() <= d {
format!("0.{}{}", "0".repeat(d - s.len()), s)
} else {
format!("{}.{}", &s[..s.len() - d], &s[s.len() - d..])
})
}
#[cfg(feature = "alloc")]
pub fn to_ratio_string(&self) -> String {
use alloc::format;
format!("{}/{}", self.num.to_dec_string(), self.den.to_dec_string())
}
}
impl PartialOrd for Ratio {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp_exact(other))
}
}
impl Ord for Ratio {
fn cmp(&self, other: &Self) -> Ordering {
self.cmp_exact(other)
}
}
fn overflow() -> TimeError {
TimeError::with_context(Code::E0021, "exact rational arithmetic left the domain")
}
fn pow10(e: u32) -> Result<Ticks> {
let ten = <Ticks as TickInt>::from_u64(10);
let mut acc = <Ticks as TickInt>::one();
for _ in 0..e {
acc = acc.try_mul(&ten).ok_or(overflow())?;
}
Ok(acc)
}
#[cfg(feature = "alloc")]
fn heapless_concat(a: &str, b: &str) -> String {
let mut s = String::with_capacity(a.len() + b.len());
s.push_str(a);
s.push_str(b);
s
}
#[cfg(not(feature = "alloc"))]
fn heapless_concat(_a: &str, _b: &str) -> &'static str {
unreachable!("decimal parsing requires alloc")
}
#[cfg_attr(feature = "u512", derive(Copy))]
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct RatInterval {
lo: Ratio,
hi: Ratio,
}
impl RatInterval {
pub fn new(lo: Ratio, hi: Ratio) -> Result<RatInterval> {
if lo.cmp_exact(&hi) == Ordering::Greater {
return Err(TimeError::new(Code::E0022));
}
Ok(RatInterval { lo, hi })
}
pub fn exact(v: Ratio) -> RatInterval {
RatInterval {
lo: v.clone(),
hi: v,
}
}
pub fn lo(&self) -> &Ratio {
&self.lo
}
pub fn hi(&self) -> &Ratio {
&self.hi
}
pub fn is_exact(&self) -> bool {
self.lo == self.hi
}
pub fn width(&self) -> Result<Ratio> {
self.hi.sub(&self.lo)
}
pub fn contains(&self, v: &Ratio) -> bool {
self.lo.cmp_exact(v) != Ordering::Greater && v.cmp_exact(&self.hi) != Ordering::Greater
}
pub fn contains_zero(&self) -> bool {
self.lo.is_zero()
}
pub fn add(&self, other: &RatInterval) -> Result<RatInterval> {
RatInterval::new(self.lo.add(&other.lo)?, self.hi.add(&other.hi)?)
}
pub fn sub(&self, other: &RatInterval) -> Result<RatInterval> {
RatInterval::new(self.lo.sub(&other.hi)?, self.hi.sub(&other.lo)?)
}
pub fn mul(&self, other: &RatInterval) -> Result<RatInterval> {
let c = [
self.lo.mul(&other.lo)?,
self.lo.mul(&other.hi)?,
self.hi.mul(&other.lo)?,
self.hi.mul(&other.hi)?,
];
let mut lo = c[0].clone();
let mut hi = c[0].clone();
for v in &c[1..] {
if v.cmp_exact(&lo) == Ordering::Less {
lo = v.clone();
}
if v.cmp_exact(&hi) == Ordering::Greater {
hi = v.clone();
}
}
RatInterval::new(lo, hi)
}
pub fn div(&self, other: &RatInterval) -> Result<RatInterval> {
if other.contains_zero() {
return Err(TimeError::with_context(
Code::E0070,
"interval division by an interval containing zero",
));
}
let inv = RatInterval::new(other.hi.recip()?, other.lo.recip()?)?;
self.mul(&inv)
}
pub fn sqrt_enclosure(&self, scale_digits: u32) -> Result<(RatInterval, u32)> {
let s = pow10(scale_digits)?;
let s2 = s.try_mul(&s).ok_or(overflow())?;
let (lo_scaled, _) = mul_div(self.lo.numer(), &s2, self.lo.denom())?;
let lo_root = isqrt_floor(&lo_scaled);
let (hi_q, hi_r) = mul_div(self.hi.numer(), &s2, self.hi.denom())?;
let hi_scaled = if hi_r.is_zero_ticks() {
hi_q
} else {
hi_q.try_add(&<Ticks as TickInt>::one()).ok_or(overflow())?
};
let hi_root = isqrt_ceil(&hi_scaled);
Ok((
RatInterval::new(Ratio::new(lo_root, s.clone())?, Ratio::new(hi_root, s)?)?,
scale_digits,
))
}
pub fn hull(&self, other: &RatInterval) -> RatInterval {
RatInterval {
lo: if self.lo.cmp_exact(&other.lo) == Ordering::Less {
self.lo.clone()
} else {
other.lo.clone()
},
hi: if self.hi.cmp_exact(&other.hi) == Ordering::Greater {
self.hi.clone()
} else {
other.hi.clone()
},
}
}
}
#[cfg(feature = "alloc")]
pub fn cf_expand(r: &Ratio, max_depth: u32) -> Vec<u64> {
let mut out = Vec::new();
let mut n = r.numer().clone();
let mut d = r.denom().clone();
for _ in 0..max_depth {
let (a, rem) = n.quot_rem(&d);
let a_small = a.to_dec_string().parse::<u64>();
match a_small {
Ok(v) => out.push(v),
Err(_) => break,
}
if rem.is_zero_ticks() {
break;
}
n = d;
d = rem;
}
out
}
#[cfg(feature = "alloc")]
pub fn convergents(cf: &[u64]) -> Vec<Ratio> {
let one = <Ticks as TickInt>::one();
let zero = <Ticks as TickInt>::zero();
let (mut hm1, mut hm2) = (one.clone(), zero.clone());
let (mut km1, mut km2) = (zero, one);
let mut out = Vec::with_capacity(cf.len());
for &a in cf {
let a = <Ticks as TickInt>::from_u64(a);
let Some(h) = a.try_mul(&hm1).and_then(|v| v.try_add(&hm2)) else {
break;
};
let Some(k) = a.try_mul(&km1).and_then(|v| v.try_add(&km2)) else {
break;
};
let Ok(c) = Ratio::new(h.clone(), k.clone()) else {
break;
};
out.push(c);
hm2 = hm1;
hm1 = h;
km2 = km1;
km1 = k;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn t(n: u64) -> Ticks {
<Ticks as TickInt>::from_u64(n)
}
fn r(n: u64, d: u64) -> Ratio {
Ratio::new(t(n), t(d)).unwrap()
}
#[test]
fn mul_div_is_exact_and_returns_the_remainder() {
assert_eq!(mul_div(&t(7), &t(5), &t(2)).unwrap(), (t(17), t(1)));
assert_eq!(mul_div(&t(10), &t(10), &t(5)).unwrap(), (t(20), t(0)));
assert_eq!(
mul_div(&t(1), &t(1), &t(0)).unwrap_err().code,
Code::E0070
);
}
#[test]
fn mul_div_intermediate_cannot_overflow() {
let m = <Ticks as TickInt>::domain_max();
assert!(m.try_mul(&m).is_none(), "the product must not fit");
let (q, rem) = mul_div(&m, &m, &m).unwrap();
assert_eq!(q, m);
assert!(rem.is_zero_ticks());
}
#[test]
fn mul_div_reports_a_quotient_that_leaves_the_domain() {
let m = <Ticks as TickInt>::domain_max();
assert_eq!(
mul_div(&m, &t(2), &t(1)).unwrap_err().code,
Code::E0021
);
}
#[test]
fn mul_div_rounded_honours_the_mode() {
assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::Trunc).unwrap(), t(3));
assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::Ceil).unwrap(), t(4));
assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::HalfUp).unwrap(), t(4));
assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::HalfEven).unwrap(), t(4));
assert_eq!(mul_div_rounded(&t(5), &t(1), &t(2), Rounding::HalfEven).unwrap(), t(2));
assert_eq!(mul_div_rounded(&t(5), &t(1), &t(2), Rounding::HalfUp).unwrap(), t(3));
}
#[test]
fn isqrt_post_conditions_hold() {
for n in 0..200u64 {
let x = t(n);
let f = isqrt_floor(&x);
let one = <Ticks as TickInt>::one();
assert!(f.wide_mul(&f) <= x.wide_mul(&one), "floor^2 > x at {n}");
let fp1 = f.try_add(&one).unwrap();
assert!(fp1.wide_mul(&fp1) > x.wide_mul(&one), "x >= (floor+1)^2 at {n}");
let c = isqrt_ceil(&x);
assert!(c.wide_mul(&c) >= x.wide_mul(&one), "ceil^2 < x at {n}");
if f.wide_mul(&f) == x.wide_mul(&one) {
assert_eq!(c, f, "a perfect square must have equal floor and ceil");
} else {
assert_eq!(c, fp1);
}
}
}
#[test]
fn isqrt_on_perfect_squares_and_large_values() {
for n in [0u64, 1, 4, 9, 16, 10_000, 1u64 << 40] {
let sq = t(n).try_mul(&t(n)).unwrap();
assert_eq!(isqrt_floor(&sq), t(n));
assert_eq!(isqrt_ceil(&sq), t(n));
}
let m = <Ticks as TickInt>::domain_max();
let f = isqrt_floor(&m);
let one = <Ticks as TickInt>::one();
assert!(f.wide_mul(&f) <= m.wide_mul(&one));
let fp1 = f.try_add(&one).unwrap();
assert!(fp1.wide_mul(&fp1) > m.wide_mul(&one));
assert_eq!(f.bit_len(), 256);
}
#[test]
fn rationals_reduce_and_compare_exactly() {
assert_eq!(r(2, 4), r(1, 2));
assert_eq!(r(6, 3), Ratio::from_u64(2));
assert!(r(1, 3).cmp_exact(&r(1, 2)) == Ordering::Less);
assert!(r(2, 3).cmp_exact(&r(1, 2)) == Ordering::Greater);
assert_eq!(r(3, 6).cmp_exact(&r(1, 2)), Ordering::Equal);
assert_eq!(Ratio::new(t(1), t(0)).unwrap_err().code, Code::E0070);
}
#[test]
fn rational_comparison_cannot_overflow() {
let m = <Ticks as TickInt>::domain_max();
let a = Ratio::new(m.clone(), m.clone()).unwrap(); let b = Ratio::new(m.clone(), <Ticks as TickInt>::one()).unwrap();
assert_eq!(a, Ratio::one());
assert_eq!(a.cmp_exact(&b), Ordering::Less);
}
#[test]
fn rational_arithmetic_is_exact() {
assert_eq!(r(1, 2).add(&r(1, 3)).unwrap(), r(5, 6));
assert_eq!(r(1, 2).sub(&r(1, 3)).unwrap(), r(1, 6));
assert_eq!(r(2, 3).mul(&r(3, 4)).unwrap(), r(1, 2));
assert_eq!(r(2, 3).div(&r(4, 9)).unwrap(), r(3, 2));
assert_eq!(r(1, 3).abs_diff(&r(1, 2)).unwrap(), r(1, 6));
assert_eq!(r(1, 2).abs_diff(&r(1, 3)).unwrap(), r(1, 6));
assert_eq!(r(1, 3).sub(&r(1, 2)).unwrap_err().code, Code::E0020);
assert_eq!(r(1, 2).div(&Ratio::zero()).unwrap_err().code, Code::E0070);
assert_eq!(Ratio::zero().recip().unwrap_err().code, Code::E0070);
}
#[test]
fn decimal_parsing_is_exact() {
assert_eq!(Ratio::from_decimal_str("1100").unwrap(), Ratio::from_u64(1100));
assert_eq!(Ratio::from_decimal_str("0.5").unwrap(), r(1, 2));
assert_eq!(Ratio::from_decimal_str("1089.80").unwrap(), r(10898, 10));
let earth = Ratio::from_decimal_str("365.242190").unwrap();
assert_eq!(earth.to_ratio_string(), "36524219/100000");
assert_eq!(earth.floor(), t(365));
assert_eq!(earth.frac(), r(24219, 100000));
for bad in ["", "1.2.3", "abc", "1e5", "-1", "1.2f"] {
assert!(Ratio::from_decimal_str(bad).is_err(), "accepted {bad:?}");
}
}
#[test]
fn decimal_rendering_states_its_mode() {
let third = r(1, 3);
assert_eq!(third.to_decimal_string(5, Rounding::Trunc).unwrap(), "0.33333");
assert_eq!(third.to_decimal_string(5, Rounding::Ceil).unwrap(), "0.33334");
let two_thirds = r(2, 3);
assert_eq!(two_thirds.to_decimal_string(5, Rounding::Trunc).unwrap(), "0.66666");
assert_eq!(two_thirds.to_decimal_string(5, Rounding::HalfEven).unwrap(), "0.66667");
assert_eq!(r(1, 100).to_decimal_string(4, Rounding::Trunc).unwrap(), "0.0100");
assert_eq!(Ratio::from_u64(7).to_decimal_string(0, Rounding::Trunc).unwrap(), "7");
}
#[test]
fn interval_arithmetic_widens_and_rejects_inversion() {
let a = RatInterval::new(r(1, 2), r(3, 2)).unwrap();
let b = RatInterval::new(r(1, 4), r(1, 2)).unwrap();
let s = a.add(&b).unwrap();
assert_eq!(s.lo(), &r(3, 4));
assert_eq!(s.hi(), &Ratio::from_u64(2));
let p = a.mul(&b).unwrap();
assert_eq!(p.lo(), &r(1, 8));
assert_eq!(p.hi(), &r(3, 4));
assert!(a.contains(&Ratio::one()));
assert!(!a.contains(&Ratio::from_u64(2)));
assert_eq!(
RatInterval::new(r(3, 2), r(1, 2)).unwrap_err().code,
Code::E0022
);
}
#[test]
fn interval_division_rejects_a_divisor_spanning_zero() {
let a = RatInterval::new(r(1, 2), r(3, 2)).unwrap();
let spans_zero = RatInterval::new(Ratio::zero(), Ratio::one()).unwrap();
assert_eq!(a.div(&spans_zero).unwrap_err().code, Code::E0070);
let ok = RatInterval::new(r(1, 2), Ratio::one()).unwrap();
let q = a.div(&ok).unwrap();
assert_eq!(q.lo(), &r(1, 2));
assert_eq!(q.hi(), &Ratio::from_u64(3));
}
#[test]
fn sqrt_enclosure_contains_the_true_root_and_narrows_with_scale() {
let two = RatInterval::exact(Ratio::from_u64(2));
let mut prev_width: Option<Ratio> = None;
for digits in [3u32, 6, 9, 12] {
let (e, s) = two.sqrt_enclosure(digits).unwrap();
assert_eq!(s, digits);
assert!(e.lo().mul(e.lo()).unwrap().cmp_exact(&Ratio::from_u64(2)) != Ordering::Greater);
assert!(e.hi().mul(e.hi()).unwrap().cmp_exact(&Ratio::from_u64(2)) != Ordering::Less);
let w = e.width().unwrap();
if let Some(p) = prev_width {
assert!(w.cmp_exact(&p) == Ordering::Less, "enclosure did not narrow");
}
prev_width = Some(w);
}
let four = RatInterval::exact(Ratio::from_u64(4));
let (e, _) = four.sqrt_enclosure(6).unwrap();
assert!(e.contains(&Ratio::from_u64(2)));
}
#[test]
fn appendix_i1_earth_intercalation() {
let ratio = Ratio::from_decimal_str("365.242190").unwrap();
let frac = ratio.frac();
let cf = cf_expand(&frac, 32);
assert_eq!(cf[..9], [0, 4, 7, 1, 3, 24, 6, 2, 2]);
let cv = convergents(&cf);
let want = [(1u64, 4u64), (7, 29), (8, 33), (31, 128), (752, 3105), (4543, 18758)];
for (i, (n, d)) in want.iter().enumerate() {
assert_eq!(cv[i + 1], r(*n, *d), "convergent {}", i + 1);
}
assert_eq!(cv[1], r(1, 4));
let gregorian = r(97, 400);
assert!(
!cv.iter().any(|c| *c == gregorian),
"97/400 must not appear as a convergent at any depth"
);
let e_greg = gregorian.abs_diff(&frac).unwrap();
let e_8_33 = r(8, 33).abs_diff(&frac).unwrap();
let e_31_128 = r(31, 128).abs_diff(&frac).unwrap();
assert!(
e_8_33.cmp_exact(&e_greg) == Ordering::Less,
"8/33 must be more accurate than 97/400, with a denominator 12x smaller"
);
assert!(e_31_128.cmp_exact(&e_greg) == Ordering::Less);
let times = e_greg.div(&e_31_128).unwrap();
assert_eq!(times.to_decimal_string(1, Rounding::Trunc).unwrap(), "124.0");
}
#[test]
fn appendix_i2_earth_grouping_derives_the_metonic_cycle() {
let ratio = Ratio::from_decimal_str("12.368266761").unwrap();
let cf = cf_expand(&ratio, 32);
assert_eq!(cf[..9], [12, 2, 1, 2, 1, 1, 17, 2, 1]);
let cv = convergents(&cf);
let want = [(12u64, 1u64), (25, 2), (37, 3), (99, 8), (136, 11), (235, 19), (4131, 334)];
for (i, (n, d)) in want.iter().enumerate() {
assert_eq!(cv[i], r(*n, *d), "convergent {}", i + 1);
}
assert!(cv.iter().any(|c| *c == r(235, 19)));
}
#[test]
fn appendix_i3_mars_intercalation() {
let frac = Ratio::from_decimal_str("668.592165627").unwrap().frac();
let cf = cf_expand(&frac, 32);
assert_eq!(cf[..9], [0, 1, 1, 2, 4, 1, 2, 2, 1]);
let cv = convergents(&cf);
let want = [(1u64, 1u64), (1, 2), (3, 5), (13, 22), (16, 27), (45, 76), (106, 179)];
for (i, (n, d)) in want.iter().enumerate() {
assert_eq!(cv[i + 1], r(*n, *d), "convergent {}", i + 1);
}
}
#[test]
fn appendix_i5_titan_intercalation() {
let frac = Ratio::from_decimal_str("673.983719443").unwrap().frac();
let cf = cf_expand(&frac, 32);
assert_eq!(cf[..9], [0, 1, 60, 2, 2, 1, 2, 1, 11]);
let cv = convergents(&cf);
let want = [(1u64, 1u64), (60, 61), (121, 123), (302, 307), (423, 430)];
for (i, (n, d)) in want.iter().enumerate() {
assert_eq!(cv[i + 1], r(*n, *d), "convergent {}", i + 1);
}
}
#[test]
fn convergents_alternate_around_the_value_and_improve() {
let frac = Ratio::from_decimal_str("365.242190").unwrap().frac();
let cv = convergents(&cf_expand(&frac, 32));
let mut prev: Option<Ratio> = None;
for c in cv.iter().skip(1) {
let e = c.abs_diff(&frac).unwrap();
if let Some(p) = prev {
assert!(
e.cmp_exact(&p) == Ordering::Less,
"convergent {} did not improve",
c.to_ratio_string()
);
}
prev = Some(e);
}
assert_eq!(cv.last().unwrap(), &frac);
}
#[test]
fn cf_expand_returns_the_full_sequence() {
let cf = cf_expand(&r(649, 200), 32);
let cv = convergents(&cf);
assert_eq!(cv.last().unwrap(), &r(649, 200));
assert_eq!(cf_expand(&Ratio::from_u64(7), 32), vec![7]);
assert_eq!(cf_expand(&frac_of("365.242190"), 3).len(), 3);
}
fn frac_of(s: &str) -> Ratio {
Ratio::from_decimal_str(s).unwrap().frac()
}
}