Skip to main content

ucal_core/
num.rs

1//! Exact integer numerics (Appendix H).
2//!
3//! Rule E permits exactly five kinds of numeric machinery, and this module is all
4//! five: exact integers, exact rationals over them, scaled fixed-point with a
5//! declared scale, integer square root with directed rounding, and interval pairs
6//! of any of those. There is no sixth kind and no float.
7//!
8//! The organising idea is that **rounding never happens implicitly**. Every
9//! operation is either exact, or it takes a direction and reports it:
10//!
11//! - [`mul_div`] is exact and hands back the remainder rather than dropping it,
12//!   so the caller decides what the remainder means.
13//! - [`isqrt_floor`] and [`isqrt_ceil`] are the two directed square roots; there
14//!   is no undirected `isqrt`.
15//! - [`Ratio`] arithmetic is exact, and overflow is `UCAL-E0021`, never a wrap.
16//! - [`RatInterval`] is where inexactness is allowed to live, and it always
17//!   widens outward.
18//!
19//! H.6: no transcendental function is implemented or called anywhere.
20
21#[cfg(feature = "alloc")]
22use alloc::string::String;
23#[cfg(feature = "alloc")]
24use alloc::vec::Vec;
25use core::cmp::Ordering;
26
27use crate::backend::{TickInt, Ticks};
28use crate::error::{Code, Result, TimeError};
29use crate::value::Rounding;
30
31/// §13 names a distinct error type for the numeric surface. It is the same type
32/// as the rest of the crate's, because the diagnostic codes are the contract and
33/// fragmenting the error type would only make them harder to match on.
34pub type NumError = TimeError;
35
36// ---------------------------------------------------------------------------
37// H.1 — widening multiply-divide
38// ---------------------------------------------------------------------------
39
40/// `a x n / d`, computed through an intermediate twice the backend width so the
41/// product cannot overflow (Appendix H.1).
42///
43/// Returns the truncated quotient **and the remainder**. Nothing rounds
44/// implicitly: a caller that wants a directed result inspects the remainder and
45/// decides. `UCAL-E0021` if the quotient itself leaves the domain.
46///
47/// ```
48/// # use ucal_core::num::mul_div;
49/// # use ucal_core::backend::{TickInt, Ticks};
50/// let a = <Ticks as TickInt>::from_u64(7);
51/// let n = <Ticks as TickInt>::from_u64(5);
52/// let d = <Ticks as TickInt>::from_u64(2);
53/// let (q, r) = mul_div(&a, &n, &d).unwrap();
54/// assert_eq!(q, <Ticks as TickInt>::from_u64(17)); // 35 / 2
55/// assert_eq!(r, <Ticks as TickInt>::from_u64(1));
56/// ```
57pub fn mul_div(a: &Ticks, n: &Ticks, d: &Ticks) -> Result<(Ticks, Ticks)> {
58    if d.is_zero_ticks() {
59        return Err(TimeError::with_context(
60            Code::E0070,
61            "mul_div: zero divisor",
62        ));
63    }
64    let wide = a.wide_mul(n);
65    let (q, r) = <Ticks as TickInt>::wide_quot_rem(&wide, d);
66    let q = <Ticks as TickInt>::narrow(&q).ok_or(TimeError::with_context(
67        Code::E0021,
68        "mul_div: quotient exceeds the domain",
69    ))?;
70    Ok((q, r))
71}
72
73/// `a x n / d`, rounded in a stated direction (Rule R).
74///
75/// Separate from [`mul_div`] so that the exact form stays the default and
76/// rounding is always a visible choice at the call site.
77pub fn mul_div_rounded(a: &Ticks, n: &Ticks, d: &Ticks, mode: Rounding) -> Result<Ticks> {
78    let (q, r) = mul_div(a, n, d)?;
79    if r.is_zero_ticks() {
80        return Ok(q);
81    }
82    let up = match mode {
83        Rounding::Trunc => false,
84        Rounding::Ceil => true,
85        Rounding::HalfUp | Rounding::HalfEven => {
86            let twice = r
87                .try_add(&r)
88                .ok_or(TimeError::with_context(Code::E0021, "2r overflowed"))?;
89            match twice.cmp(d) {
90                Ordering::Greater => true,
91                Ordering::Less => false,
92                Ordering::Equal => match mode {
93                    Rounding::HalfUp => true,
94                    _ => q.is_odd(),
95                },
96            }
97        }
98    };
99    if up {
100        q.try_add(&<Ticks as TickInt>::one())
101            .ok_or(TimeError::new(Code::E0021))
102    } else {
103        Ok(q)
104    }
105}
106
107// ---------------------------------------------------------------------------
108// H.2 — integer square root with directed rounding
109// ---------------------------------------------------------------------------
110
111/// Largest `r` with `r*r <= x`.
112///
113/// Integer Newton iteration. The post-condition `r^2 <= x < (r+1)^2` is asserted,
114/// not assumed — Appendix H.2 requires it, and a silently wrong square root would
115/// corrupt every cosmological enclosure downstream while still looking plausible.
116pub fn isqrt_floor(x: &Ticks) -> Ticks {
117    let one = <Ticks as TickInt>::one();
118    if x.is_zero_ticks() || *x == one {
119        return x.clone();
120    }
121    // Start above the true root: 2^ceil(bits/2) >= sqrt(x).
122    let bits = x.bit_len();
123    let start_exp = bits.div_ceil(2);
124    let mut r = <Ticks as TickInt>::pow2(start_exp)
125        .unwrap_or_else(<Ticks as TickInt>::domain_max);
126    let two = <Ticks as TickInt>::from_u64(2);
127    loop {
128        // next = (r + x/r) / 2
129        let (q, _) = x.quot_rem(&r);
130        let sum = match r.try_add(&q) {
131            Some(s) => s,
132            // r + x/r can exceed the domain only on the first, deliberately
133            // over-large guess; halving the guess keeps the iteration going.
134            None => {
135                let (half, _) = r.quot_rem(&two);
136                r = half;
137                continue;
138            }
139        };
140        let (next, _) = sum.quot_rem(&two);
141        if next >= r {
142            break;
143        }
144        r = next;
145    }
146    debug_assert!(
147        &r.wide_mul(&r) <= &x.wide_mul(&<Ticks as TickInt>::one()),
148        "isqrt_floor post-condition r^2 <= x violated"
149    );
150    debug_assert!(
151        {
152            let rp1 = r.try_add(&one).expect("r+1 within domain for any real root");
153            rp1.wide_mul(&rp1) > x.wide_mul(&one)
154        },
155        "isqrt_floor post-condition x < (r+1)^2 violated"
156    );
157    r
158}
159
160/// Smallest `r` with `r*r >= x`.
161pub fn isqrt_ceil(x: &Ticks) -> Ticks {
162    let f = isqrt_floor(x);
163    let one = <Ticks as TickInt>::one();
164    if f.wide_mul(&f) == x.wide_mul(&one) {
165        f
166    } else {
167        f.try_add(&one)
168            .expect("ceil of a root inside the domain stays inside it")
169    }
170}
171
172// ---------------------------------------------------------------------------
173// Exact rationals
174// ---------------------------------------------------------------------------
175
176/// An exact non-negative rational over [`Ticks`], always in lowest terms.
177///
178/// Non-negative because the value domain is unsigned (Rule Z, N12) and every
179/// quantity this crate builds on rationals — durations, ratios of periods,
180/// redshifts, density parameters — is non-negative. Differences that could go
181/// negative are taken with [`Ratio::abs_diff`], which makes the sign loss
182/// explicit at the call site rather than silent in the type.
183///
184/// Arithmetic is exact. Where a result would leave the domain the operation fails
185/// with `UCAL-E0021`; it never wraps and never approximates (Rules O, E). Operands
186/// are pre-reduced by common factors before multiplying, which keeps intermediates
187/// small enough that overflow is rare in practice without ever making it silent.
188#[cfg_attr(feature = "u512", derive(Copy))]
189#[derive(Clone, PartialEq, Eq, Debug)]
190pub struct Ratio {
191    num: Ticks,
192    den: Ticks,
193}
194
195/// Greatest common divisor by Euclid. Exact and total for non-negative inputs.
196pub fn gcd(a: &Ticks, b: &Ticks) -> Ticks {
197    let mut a = a.clone();
198    let mut b = b.clone();
199    while !b.is_zero_ticks() {
200        let (_, r) = a.quot_rem(&b);
201        a = b;
202        b = r;
203    }
204    a
205}
206
207impl Ratio {
208    /// Construct and reduce. `UCAL-E0070` if the denominator is zero.
209    pub fn new(num: Ticks, den: Ticks) -> Result<Ratio> {
210        if den.is_zero_ticks() {
211            return Err(TimeError::with_context(
212                Code::E0070,
213                "rational with a zero denominator",
214            ));
215        }
216        let g = gcd(&num, &den);
217        let one = <Ticks as TickInt>::one();
218        if g == one || g.is_zero_ticks() {
219            return Ok(Ratio { num, den });
220        }
221        let (n, _) = num.quot_rem(&g);
222        let (d, _) = den.quot_rem(&g);
223        Ok(Ratio { num: n, den: d })
224    }
225
226    /// A whole number.
227    pub fn from_int(n: Ticks) -> Ratio {
228        Ratio {
229            num: n,
230            den: <Ticks as TickInt>::one(),
231        }
232    }
233
234    /// A small whole number.
235    pub fn from_u64(n: u64) -> Ratio {
236        Ratio::from_int(<Ticks as TickInt>::from_u64(n))
237    }
238
239    /// Zero.
240    pub fn zero() -> Ratio {
241        Ratio::from_u64(0)
242    }
243
244    /// One.
245    pub fn one() -> Ratio {
246        Ratio::from_u64(1)
247    }
248
249    /// Parse an exact decimal such as `"365.242190"` or `"1089.80"`.
250    ///
251    /// §10.2 requires redshift inputs to parse exactly: `1100`, `1089.80` and
252    /// `0.5` are all exact rationals, and there is no float path in or out.
253    /// Rejects anything that is not digits with at most one decimal point.
254    pub fn from_decimal_str(s: &str) -> Result<Ratio> {
255        let malformed = TimeError::with_context(Code::E0001, "not an exact decimal");
256        let (int, frac) = match s.split_once('.') {
257            None => (s, ""),
258            Some((i, f)) => (i, f),
259        };
260        if int.is_empty() && frac.is_empty() {
261            return Err(malformed);
262        }
263        if !int.bytes().all(|b| b.is_ascii_digit()) || !frac.bytes().all(|b| b.is_ascii_digit()) {
264            return Err(malformed);
265        }
266        let scale = frac.len() as u32;
267        let mut digits = heapless_concat(int, frac);
268        if digits.is_empty() {
269            digits = "0".into();
270        }
271        let num = <Ticks as TickInt>::from_dec_str(&digits).ok_or(malformed)?;
272        let den = pow10(scale)?;
273        Ratio::new(num, den)
274    }
275
276    /// The numerator, in lowest terms.
277    pub fn numer(&self) -> &Ticks {
278        &self.num
279    }
280
281    /// The denominator, in lowest terms. Never zero.
282    pub fn denom(&self) -> &Ticks {
283        &self.den
284    }
285
286    /// Whether this is exactly zero.
287    pub fn is_zero(&self) -> bool {
288        self.num.is_zero_ticks()
289    }
290
291    /// Whether the value is a whole number.
292    pub fn is_integer(&self) -> bool {
293        self.den == <Ticks as TickInt>::one()
294    }
295
296    /// The whole part, truncated toward zero.
297    pub fn floor(&self) -> Ticks {
298        self.num.quot_rem(&self.den).0
299    }
300
301    /// The fractional part, `self - floor(self)`.
302    pub fn frac(&self) -> Ratio {
303        let (_, r) = self.num.quot_rem(&self.den);
304        Ratio {
305            num: r,
306            den: self.den.clone(),
307        }
308    }
309
310    /// `self + other`, exact.
311    pub fn add(&self, other: &Ratio) -> Result<Ratio> {
312        let g = gcd(&self.den, &other.den);
313        let (od, _) = other.den.quot_rem(&g);
314        // num = self.num * (other.den/g) + other.num * (self.den/g)
315        let (sd, _) = self.den.quot_rem(&g);
316        let a = self.num.try_mul(&od).ok_or(overflow())?;
317        let b = other.num.try_mul(&sd).ok_or(overflow())?;
318        let num = a.try_add(&b).ok_or(overflow())?;
319        let den = self.den.try_mul(&od).ok_or(overflow())?;
320        Ratio::new(num, den)
321    }
322
323    /// `self - other`. `UCAL-E0020` if the result would be negative — the type is
324    /// non-negative by construction, so this is Rule Z applied to rationals.
325    pub fn sub(&self, other: &Ratio) -> Result<Ratio> {
326        if self.cmp_exact(other) == Ordering::Less {
327            return Err(TimeError::with_context(
328                Code::E0020,
329                "rational subtraction would be negative; use abs_diff",
330            ));
331        }
332        let g = gcd(&self.den, &other.den);
333        let (od, _) = other.den.quot_rem(&g);
334        let (sd, _) = self.den.quot_rem(&g);
335        let a = self.num.try_mul(&od).ok_or(overflow())?;
336        let b = other.num.try_mul(&sd).ok_or(overflow())?;
337        let num = a.try_sub(&b).ok_or(overflow())?;
338        let den = self.den.try_mul(&od).ok_or(overflow())?;
339        Ratio::new(num, den)
340    }
341
342    /// `|self - other|`, exact.
343    pub fn abs_diff(&self, other: &Ratio) -> Result<Ratio> {
344        match self.cmp_exact(other) {
345            Ordering::Less => other.sub(self),
346            _ => self.sub(other),
347        }
348    }
349
350    /// `self * other`, exact.
351    pub fn mul(&self, other: &Ratio) -> Result<Ratio> {
352        // Cross-reduce first so the products stay as small as possible.
353        let g1 = gcd(&self.num, &other.den);
354        let g2 = gcd(&other.num, &self.den);
355        let (n1, _) = self.num.quot_rem(&g1);
356        let (d2, _) = other.den.quot_rem(&g1);
357        let (n2, _) = other.num.quot_rem(&g2);
358        let (d1, _) = self.den.quot_rem(&g2);
359        let num = n1.try_mul(&n2).ok_or(overflow())?;
360        let den = d1.try_mul(&d2).ok_or(overflow())?;
361        Ratio::new(num, den)
362    }
363
364    /// `self / other`. `UCAL-E0070` if `other` is zero.
365    pub fn div(&self, other: &Ratio) -> Result<Ratio> {
366        if other.is_zero() {
367            return Err(TimeError::with_context(
368                Code::E0070,
369                "rational division by zero",
370            ));
371        }
372        self.mul(&other.recip()?)
373    }
374
375    /// `1 / self`. `UCAL-E0070` if `self` is zero.
376    pub fn recip(&self) -> Result<Ratio> {
377        if self.is_zero() {
378            return Err(TimeError::with_context(
379                Code::E0070,
380                "reciprocal of zero",
381            ));
382        }
383        Ok(Ratio {
384            num: self.den.clone(),
385            den: self.num.clone(),
386        })
387    }
388
389    /// Exact comparison.
390    ///
391    /// Cross-multiplies in the widened type, so comparison is total and can never
392    /// fail for overflow — which matters, because a comparison that could error
393    /// would make [`Ratio`] unusable as an interval endpoint.
394    pub fn cmp_exact(&self, other: &Ratio) -> Ordering {
395        self.num
396            .wide_mul(&other.den)
397            .cmp(&other.num.wide_mul(&self.den))
398    }
399
400    /// The closest rational with denominator `10^digits`, rounded in the stated
401    /// direction.
402    ///
403    /// Exact rational accumulation is unbounded. Summing `n` terms whose
404    /// denominators are mutually coprime grows the common denominator like their
405    /// product, so a few dozen terms is enough to overflow any fixed-width
406    /// integer — long before a quadrature sum of thousands of panels finishes.
407    ///
408    /// A certified sum therefore snaps each partial result back to a fixed
409    /// decimal grid, rounding **outward**: [`Rounding::Trunc`] for a value that
410    /// bounds from below, [`Rounding::Ceil`] for one that bounds from above. The
411    /// accumulator stays bounded and the enclosure stays rigorous, at the cost of
412    /// one grid step per snap — which is why callers choose a grid far finer than
413    /// the width they are trying to certify.
414    ///
415    /// This is not a rendering: the result is still an exact [`Ratio`], and Rule R
416    /// is untouched. It is a deliberate outward relaxation, and the *only* place
417    /// this crate discards information inside a computation rather than at its
418    /// edge.
419    pub fn snap(&self, digits: u32, mode: Rounding) -> Result<Ratio> {
420        let scale = pow10(digits)?;
421        let num = mul_div_rounded(&self.num, &scale, &self.den, mode)?;
422        Ratio::new(num, scale)
423    }
424
425    /// Render to `digits` decimal places under an explicit mode (Rule R).
426    ///
427    /// This is the only place a `Ratio` becomes inexact, and it is a *rendering*,
428    /// never a construction. `UCAL-W0001` territory: the caller is told the mode
429    /// because it had to choose one.
430    #[cfg(feature = "alloc")]
431    pub fn to_decimal_string(&self, digits: u32, mode: Rounding) -> Result<String> {
432        use alloc::format;
433        let scale = pow10(digits)?;
434        let scaled = mul_div_rounded(&self.num, &scale, &self.den, mode)?;
435        let s = scaled.to_dec_string();
436        if digits == 0 {
437            return Ok(s);
438        }
439        let d = digits as usize;
440        Ok(if s.len() <= d {
441            format!("0.{}{}", "0".repeat(d - s.len()), s)
442        } else {
443            format!("{}.{}", &s[..s.len() - d], &s[s.len() - d..])
444        })
445    }
446
447    /// Render as `numerator/denominator`, which is always exact.
448    #[cfg(feature = "alloc")]
449    pub fn to_ratio_string(&self) -> String {
450        use alloc::format;
451        format!("{}/{}", self.num.to_dec_string(), self.den.to_dec_string())
452    }
453}
454
455impl PartialOrd for Ratio {
456    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
457        Some(self.cmp_exact(other))
458    }
459}
460
461impl Ord for Ratio {
462    fn cmp(&self, other: &Self) -> Ordering {
463        self.cmp_exact(other)
464    }
465}
466
467fn overflow() -> TimeError {
468    TimeError::with_context(Code::E0021, "exact rational arithmetic left the domain")
469}
470
471fn pow10(e: u32) -> Result<Ticks> {
472    let ten = <Ticks as TickInt>::from_u64(10);
473    let mut acc = <Ticks as TickInt>::one();
474    for _ in 0..e {
475        acc = acc.try_mul(&ten).ok_or(overflow())?;
476    }
477    Ok(acc)
478}
479
480#[cfg(feature = "alloc")]
481fn heapless_concat(a: &str, b: &str) -> String {
482    let mut s = String::with_capacity(a.len() + b.len());
483    s.push_str(a);
484    s.push_str(b);
485    s
486}
487
488#[cfg(not(feature = "alloc"))]
489fn heapless_concat(_a: &str, _b: &str) -> &'static str {
490    unreachable!("decimal parsing requires alloc")
491}
492
493// ---------------------------------------------------------------------------
494// H.3 — interval arithmetic
495// ---------------------------------------------------------------------------
496
497/// A closed interval of exact rationals, `lo <= hi` (Appendix H.3).
498///
499/// Because the endpoints are *exact* rationals, `+`, `-` and `x` on intervals are
500/// themselves exact — there is nothing to round outward. Outward rounding enters
501/// at exactly two places, and both are explicit:
502///
503/// - [`RatInterval::sqrt_enclosure`], which uses the directed integer roots;
504/// - fixed-point scaling, where the scale is declared and recorded.
505///
506/// That is a stronger position than a float interval library can take, and it is
507/// what lets Rule X promise a *certified* enclosure rather than a tolerance.
508#[cfg_attr(feature = "u512", derive(Copy))]
509#[derive(Clone, PartialEq, Eq, Debug)]
510pub struct RatInterval {
511    lo: Ratio,
512    hi: Ratio,
513}
514
515impl RatInterval {
516    /// Construct, rejecting inversion with `UCAL-E0022`.
517    pub fn new(lo: Ratio, hi: Ratio) -> Result<RatInterval> {
518        if lo.cmp_exact(&hi) == Ordering::Greater {
519            return Err(TimeError::new(Code::E0022));
520        }
521        Ok(RatInterval { lo, hi })
522    }
523
524    /// The degenerate interval at an exact value.
525    pub fn exact(v: Ratio) -> RatInterval {
526        RatInterval {
527            lo: v.clone(),
528            hi: v,
529        }
530    }
531
532    /// Lower bound.
533    pub fn lo(&self) -> &Ratio {
534        &self.lo
535    }
536
537    /// Upper bound.
538    pub fn hi(&self) -> &Ratio {
539        &self.hi
540    }
541
542    /// Whether the interval is a single point.
543    pub fn is_exact(&self) -> bool {
544        self.lo == self.hi
545    }
546
547    /// `hi - lo`.
548    pub fn width(&self) -> Result<Ratio> {
549        self.hi.sub(&self.lo)
550    }
551
552    /// Whether the interval contains a value.
553    pub fn contains(&self, v: &Ratio) -> bool {
554        self.lo.cmp_exact(v) != Ordering::Greater && v.cmp_exact(&self.hi) != Ordering::Greater
555    }
556
557    /// Whether zero is inside the interval. Division requires this to be false
558    /// (Appendix H.3).
559    pub fn contains_zero(&self) -> bool {
560        self.lo.is_zero()
561    }
562
563    /// Interval addition. Exact: `lo` with `lo`, `hi` with `hi`.
564    pub fn add(&self, other: &RatInterval) -> Result<RatInterval> {
565        RatInterval::new(self.lo.add(&other.lo)?, self.hi.add(&other.hi)?)
566    }
567
568    /// Interval subtraction, `[lo - other.hi, hi - other.lo]`.
569    pub fn sub(&self, other: &RatInterval) -> Result<RatInterval> {
570        RatInterval::new(self.lo.sub(&other.hi)?, self.hi.sub(&other.lo)?)
571    }
572
573    /// Interval multiplication: the min and max of the four endpoint products.
574    ///
575    /// With non-negative endpoints this reduces to `lo*lo` and `hi*hi`, but the
576    /// general form is computed anyway so the method stays correct if the
577    /// endpoint domain is ever widened.
578    pub fn mul(&self, other: &RatInterval) -> Result<RatInterval> {
579        let c = [
580            self.lo.mul(&other.lo)?,
581            self.lo.mul(&other.hi)?,
582            self.hi.mul(&other.lo)?,
583            self.hi.mul(&other.hi)?,
584        ];
585        let mut lo = c[0].clone();
586        let mut hi = c[0].clone();
587        for v in &c[1..] {
588            if v.cmp_exact(&lo) == Ordering::Less {
589                lo = v.clone();
590            }
591            if v.cmp_exact(&hi) == Ordering::Greater {
592                hi = v.clone();
593            }
594        }
595        RatInterval::new(lo, hi)
596    }
597
598    /// Interval division. `UCAL-E0070` if the divisor interval contains zero.
599    pub fn div(&self, other: &RatInterval) -> Result<RatInterval> {
600        if other.contains_zero() {
601            return Err(TimeError::with_context(
602                Code::E0070,
603                "interval division by an interval containing zero",
604            ));
605        }
606        let inv = RatInterval::new(other.hi.recip()?, other.lo.recip()?)?;
607        self.mul(&inv)
608    }
609
610    /// A certified enclosure of the square root, at a declared fixed-point scale
611    /// (Appendix H.2).
612    ///
613    /// Computes `[isqrt_floor(lo x S^2) / S, isqrt_ceil(hi x S^2) / S]`, which
614    /// provably contains the true root of every value in the interval. The scale
615    /// is a parameter rather than a constant because different queries need
616    /// different precision (D-6), and it is returned alongside the result so it
617    /// can be recorded — `CosmoResult` carries it as `scale`.
618    ///
619    /// Widening the interval is always sound; narrowing it would not be. Every
620    /// rounding here goes outward.
621    pub fn sqrt_enclosure(&self, scale_digits: u32) -> Result<(RatInterval, u32)> {
622        let s = pow10(scale_digits)?;
623        let s2 = s.try_mul(&s).ok_or(overflow())?;
624
625        // lo: floor(sqrt(lo.num * S^2 / lo.den)) / S  — rounds down, widening left
626        let (lo_scaled, _) = mul_div(self.lo.numer(), &s2, self.lo.denom())?;
627        let lo_root = isqrt_floor(&lo_scaled);
628
629        // hi: ceil(sqrt(hi.num * S^2 / hi.den)) / S — rounds up, widening right.
630        // The dividend is rounded up too, so the enclosure cannot be too narrow.
631        let (hi_q, hi_r) = mul_div(self.hi.numer(), &s2, self.hi.denom())?;
632        let hi_scaled = if hi_r.is_zero_ticks() {
633            hi_q
634        } else {
635            hi_q.try_add(&<Ticks as TickInt>::one()).ok_or(overflow())?
636        };
637        let hi_root = isqrt_ceil(&hi_scaled);
638
639        Ok((
640            RatInterval::new(Ratio::new(lo_root, s.clone())?, Ratio::new(hi_root, s)?)?,
641            scale_digits,
642        ))
643    }
644
645    /// The smallest interval containing both.
646    pub fn hull(&self, other: &RatInterval) -> RatInterval {
647        RatInterval {
648            lo: if self.lo.cmp_exact(&other.lo) == Ordering::Less {
649                self.lo.clone()
650            } else {
651                other.lo.clone()
652            },
653            hi: if self.hi.cmp_exact(&other.hi) == Ordering::Greater {
654                self.hi.clone()
655            } else {
656                other.hi.clone()
657            },
658        }
659    }
660}
661
662// ---------------------------------------------------------------------------
663// H.5 — continued fractions
664// ---------------------------------------------------------------------------
665
666/// The continued-fraction expansion of an exact rational.
667///
668/// Returns the **full** sequence, not just the terms up to some selected
669/// convergent: §15.2 requires every derivation to be auditable end to end, and a
670/// caller that only sees the chosen answer cannot check the choice.
671///
672/// Terminates naturally when the remainder reaches zero, or at `max_depth`.
673#[cfg(feature = "alloc")]
674pub fn cf_expand(r: &Ratio, max_depth: u32) -> Vec<u64> {
675    let mut out = Vec::new();
676    let mut n = r.numer().clone();
677    let mut d = r.denom().clone();
678    for _ in 0..max_depth {
679        let (a, rem) = n.quot_rem(&d);
680        // A continued-fraction term can exceed u64 only for a ratio whose terms
681        // are astronomically lopsided; the derivations this serves never are, and
682        // saturating here would silently corrupt the expansion.
683        let a_small = a.to_dec_string().parse::<u64>();
684        match a_small {
685            Ok(v) => out.push(v),
686            Err(_) => break,
687        }
688        if rem.is_zero_ticks() {
689            break;
690        }
691        n = d;
692        d = rem;
693    }
694    out
695}
696
697/// The convergents of a continued fraction, in order.
698///
699/// `h[k] = a[k]*h[k-1] + h[k-2]`, `k[k] = a[k]*k[k-1] + k[k-2]`, exact throughout.
700/// Stops early if a convergent would leave the domain rather than wrapping.
701#[cfg(feature = "alloc")]
702pub fn convergents(cf: &[u64]) -> Vec<Ratio> {
703    let one = <Ticks as TickInt>::one();
704    let zero = <Ticks as TickInt>::zero();
705    let (mut hm1, mut hm2) = (one.clone(), zero.clone());
706    let (mut km1, mut km2) = (zero, one);
707    let mut out = Vec::with_capacity(cf.len());
708    for &a in cf {
709        let a = <Ticks as TickInt>::from_u64(a);
710        let Some(h) = a.try_mul(&hm1).and_then(|v| v.try_add(&hm2)) else {
711            break;
712        };
713        let Some(k) = a.try_mul(&km1).and_then(|v| v.try_add(&km2)) else {
714            break;
715        };
716        let Ok(c) = Ratio::new(h.clone(), k.clone()) else {
717            break;
718        };
719        out.push(c);
720        hm2 = hm1;
721        hm1 = h;
722        km2 = km1;
723        km1 = k;
724    }
725    out
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731    use alloc::vec;
732
733    fn t(n: u64) -> Ticks {
734        <Ticks as TickInt>::from_u64(n)
735    }
736    fn r(n: u64, d: u64) -> Ratio {
737        Ratio::new(t(n), t(d)).unwrap()
738    }
739
740    // ---- H.1 ----
741
742    #[test]
743    fn mul_div_is_exact_and_returns_the_remainder() {
744        assert_eq!(mul_div(&t(7), &t(5), &t(2)).unwrap(), (t(17), t(1)));
745        assert_eq!(mul_div(&t(10), &t(10), &t(5)).unwrap(), (t(20), t(0)));
746        assert_eq!(
747            mul_div(&t(1), &t(1), &t(0)).unwrap_err().code,
748            Code::E0070
749        );
750    }
751
752    #[test]
753    fn mul_div_intermediate_cannot_overflow() {
754        // The whole point of Appendix H.1: a product that exceeds the domain is
755        // fine as long as the quotient does not. domain_max^2 / domain_max is
756        // exactly domain_max, and a 512-bit backend cannot hold the intermediate.
757        let m = <Ticks as TickInt>::domain_max();
758        assert!(m.try_mul(&m).is_none(), "the product must not fit");
759        let (q, rem) = mul_div(&m, &m, &m).unwrap();
760        assert_eq!(q, m);
761        assert!(rem.is_zero_ticks());
762    }
763
764    #[test]
765    fn mul_div_reports_a_quotient_that_leaves_the_domain() {
766        let m = <Ticks as TickInt>::domain_max();
767        assert_eq!(
768            mul_div(&m, &t(2), &t(1)).unwrap_err().code,
769            Code::E0021
770        );
771    }
772
773    #[test]
774    fn mul_div_rounded_honours_the_mode() {
775        // 7*1/2 = 3.5 exactly — a tie, so the modes separate.
776        assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::Trunc).unwrap(), t(3));
777        assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::Ceil).unwrap(), t(4));
778        assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::HalfUp).unwrap(), t(4));
779        // half-even: quotient 3 is odd, so it rounds up to 4
780        assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::HalfEven).unwrap(), t(4));
781        // 5/2 = 2.5, quotient 2 is even, so half-even stays at 2
782        assert_eq!(mul_div_rounded(&t(5), &t(1), &t(2), Rounding::HalfEven).unwrap(), t(2));
783        assert_eq!(mul_div_rounded(&t(5), &t(1), &t(2), Rounding::HalfUp).unwrap(), t(3));
784    }
785
786    // ---- H.2 ----
787
788    #[test]
789    fn isqrt_post_conditions_hold() {
790        for n in 0..200u64 {
791            let x = t(n);
792            let f = isqrt_floor(&x);
793            let one = <Ticks as TickInt>::one();
794            // r^2 <= x < (r+1)^2
795            assert!(f.wide_mul(&f) <= x.wide_mul(&one), "floor^2 > x at {n}");
796            let fp1 = f.try_add(&one).unwrap();
797            assert!(fp1.wide_mul(&fp1) > x.wide_mul(&one), "x >= (floor+1)^2 at {n}");
798
799            let c = isqrt_ceil(&x);
800            assert!(c.wide_mul(&c) >= x.wide_mul(&one), "ceil^2 < x at {n}");
801            if f.wide_mul(&f) == x.wide_mul(&one) {
802                assert_eq!(c, f, "a perfect square must have equal floor and ceil");
803            } else {
804                assert_eq!(c, fp1);
805            }
806        }
807    }
808
809    #[test]
810    fn isqrt_on_perfect_squares_and_large_values() {
811        for n in [0u64, 1, 4, 9, 16, 10_000, 1u64 << 40] {
812            let sq = t(n).try_mul(&t(n)).unwrap();
813            assert_eq!(isqrt_floor(&sq), t(n));
814            assert_eq!(isqrt_ceil(&sq), t(n));
815        }
816        // Near the top of the domain.
817        let m = <Ticks as TickInt>::domain_max();
818        let f = isqrt_floor(&m);
819        let one = <Ticks as TickInt>::one();
820        assert!(f.wide_mul(&f) <= m.wide_mul(&one));
821        let fp1 = f.try_add(&one).unwrap();
822        assert!(fp1.wide_mul(&fp1) > m.wide_mul(&one));
823        // 2^512-1 has a root of 2^256-1.
824        assert_eq!(f.bit_len(), 256);
825    }
826
827    // ---- rationals ----
828
829    #[test]
830    fn rationals_reduce_and_compare_exactly() {
831        assert_eq!(r(2, 4), r(1, 2));
832        assert_eq!(r(6, 3), Ratio::from_u64(2));
833        assert!(r(1, 3).cmp_exact(&r(1, 2)) == Ordering::Less);
834        assert!(r(2, 3).cmp_exact(&r(1, 2)) == Ordering::Greater);
835        assert_eq!(r(3, 6).cmp_exact(&r(1, 2)), Ordering::Equal);
836        assert_eq!(Ratio::new(t(1), t(0)).unwrap_err().code, Code::E0070);
837    }
838
839    #[test]
840    fn rational_comparison_cannot_overflow() {
841        // Cross-multiplication of two near-domain-max rationals overflows a
842        // narrow product but must still compare correctly.
843        let m = <Ticks as TickInt>::domain_max();
844        let a = Ratio::new(m.clone(), m.clone()).unwrap(); // == 1
845        let b = Ratio::new(m.clone(), <Ticks as TickInt>::one()).unwrap();
846        assert_eq!(a, Ratio::one());
847        assert_eq!(a.cmp_exact(&b), Ordering::Less);
848    }
849
850    #[test]
851    fn rational_arithmetic_is_exact() {
852        assert_eq!(r(1, 2).add(&r(1, 3)).unwrap(), r(5, 6));
853        assert_eq!(r(1, 2).sub(&r(1, 3)).unwrap(), r(1, 6));
854        assert_eq!(r(2, 3).mul(&r(3, 4)).unwrap(), r(1, 2));
855        assert_eq!(r(2, 3).div(&r(4, 9)).unwrap(), r(3, 2));
856        assert_eq!(r(1, 3).abs_diff(&r(1, 2)).unwrap(), r(1, 6));
857        assert_eq!(r(1, 2).abs_diff(&r(1, 3)).unwrap(), r(1, 6));
858        // Rule Z for rationals: a negative result is an error, not a sign.
859        assert_eq!(r(1, 3).sub(&r(1, 2)).unwrap_err().code, Code::E0020);
860        assert_eq!(r(1, 2).div(&Ratio::zero()).unwrap_err().code, Code::E0070);
861        assert_eq!(Ratio::zero().recip().unwrap_err().code, Code::E0070);
862    }
863
864    #[test]
865    fn decimal_parsing_is_exact() {
866        // §10.2: 1100, 1089.80 and 0.5 must all be exact.
867        assert_eq!(Ratio::from_decimal_str("1100").unwrap(), Ratio::from_u64(1100));
868        assert_eq!(Ratio::from_decimal_str("0.5").unwrap(), r(1, 2));
869        assert_eq!(Ratio::from_decimal_str("1089.80").unwrap(), r(10898, 10));
870        let earth = Ratio::from_decimal_str("365.242190").unwrap();
871        assert_eq!(earth.to_ratio_string(), "36524219/100000");
872        assert_eq!(earth.floor(), t(365));
873        assert_eq!(earth.frac(), r(24219, 100000));
874        for bad in ["", "1.2.3", "abc", "1e5", "-1", "1.2f"] {
875            assert!(Ratio::from_decimal_str(bad).is_err(), "accepted {bad:?}");
876        }
877    }
878
879    #[test]
880    fn decimal_rendering_states_its_mode() {
881        let third = r(1, 3);
882        assert_eq!(third.to_decimal_string(5, Rounding::Trunc).unwrap(), "0.33333");
883        assert_eq!(third.to_decimal_string(5, Rounding::Ceil).unwrap(), "0.33334");
884        let two_thirds = r(2, 3);
885        assert_eq!(two_thirds.to_decimal_string(5, Rounding::Trunc).unwrap(), "0.66666");
886        assert_eq!(two_thirds.to_decimal_string(5, Rounding::HalfEven).unwrap(), "0.66667");
887        // Values below 1 keep their leading zero and pad correctly.
888        assert_eq!(r(1, 100).to_decimal_string(4, Rounding::Trunc).unwrap(), "0.0100");
889        assert_eq!(Ratio::from_u64(7).to_decimal_string(0, Rounding::Trunc).unwrap(), "7");
890    }
891
892    // ---- H.3 ----
893
894    #[test]
895    fn interval_arithmetic_widens_and_rejects_inversion() {
896        let a = RatInterval::new(r(1, 2), r(3, 2)).unwrap();
897        let b = RatInterval::new(r(1, 4), r(1, 2)).unwrap();
898        let s = a.add(&b).unwrap();
899        assert_eq!(s.lo(), &r(3, 4));
900        assert_eq!(s.hi(), &Ratio::from_u64(2));
901        let p = a.mul(&b).unwrap();
902        assert_eq!(p.lo(), &r(1, 8));
903        assert_eq!(p.hi(), &r(3, 4));
904        assert!(a.contains(&Ratio::one()));
905        assert!(!a.contains(&Ratio::from_u64(2)));
906        assert_eq!(
907            RatInterval::new(r(3, 2), r(1, 2)).unwrap_err().code,
908            Code::E0022
909        );
910    }
911
912    #[test]
913    fn interval_division_rejects_a_divisor_spanning_zero() {
914        let a = RatInterval::new(r(1, 2), r(3, 2)).unwrap();
915        let spans_zero = RatInterval::new(Ratio::zero(), Ratio::one()).unwrap();
916        assert_eq!(a.div(&spans_zero).unwrap_err().code, Code::E0070);
917        let ok = RatInterval::new(r(1, 2), Ratio::one()).unwrap();
918        let q = a.div(&ok).unwrap();
919        assert_eq!(q.lo(), &r(1, 2));
920        assert_eq!(q.hi(), &Ratio::from_u64(3));
921    }
922
923    #[test]
924    fn sqrt_enclosure_contains_the_true_root_and_narrows_with_scale() {
925        // sqrt(2) is irrational, so the enclosure must be strict and must bracket.
926        let two = RatInterval::exact(Ratio::from_u64(2));
927        let mut prev_width: Option<Ratio> = None;
928        for digits in [3u32, 6, 9, 12] {
929            let (e, s) = two.sqrt_enclosure(digits).unwrap();
930            assert_eq!(s, digits);
931            // lo^2 <= 2 <= hi^2
932            assert!(e.lo().mul(e.lo()).unwrap().cmp_exact(&Ratio::from_u64(2)) != Ordering::Greater);
933            assert!(e.hi().mul(e.hi()).unwrap().cmp_exact(&Ratio::from_u64(2)) != Ordering::Less);
934            let w = e.width().unwrap();
935            if let Some(p) = prev_width {
936                assert!(w.cmp_exact(&p) == Ordering::Less, "enclosure did not narrow");
937            }
938            prev_width = Some(w);
939        }
940        // A perfect square encloses exactly.
941        let four = RatInterval::exact(Ratio::from_u64(4));
942        let (e, _) = four.sqrt_enclosure(6).unwrap();
943        assert!(e.contains(&Ratio::from_u64(2)));
944    }
945
946    // ---- H.5: Appendix I reproduction, the UC-P3 exit criterion ----
947
948    #[test]
949    fn appendix_i1_earth_intercalation() {
950        let ratio = Ratio::from_decimal_str("365.242190").unwrap();
951        let frac = ratio.frac();
952        let cf = cf_expand(&frac, 32);
953        assert_eq!(cf[..9], [0, 4, 7, 1, 3, 24, 6, 2, 2]);
954
955        let cv = convergents(&cf);
956        let want = [(1u64, 4u64), (7, 29), (8, 33), (31, 128), (752, 3105), (4543, 18758)];
957        for (i, (n, d)) in want.iter().enumerate() {
958            assert_eq!(cv[i + 1], r(*n, *d), "convergent {}", i + 1);
959        }
960
961        // §21.3-6: 1/4 (the Julian rule) is convergent 1...
962        assert_eq!(cv[1], r(1, 4));
963        // ...and 97/400 (Gregorian) appears at NO depth.
964        let gregorian = r(97, 400);
965        assert!(
966            !cv.iter().any(|c| *c == gregorian),
967            "97/400 must not appear as a convergent at any depth"
968        );
969
970        // The RFC's two accuracy claims about 97/400, verified exactly.
971        let e_greg = gregorian.abs_diff(&frac).unwrap();
972        let e_8_33 = r(8, 33).abs_diff(&frac).unwrap();
973        let e_31_128 = r(31, 128).abs_diff(&frac).unwrap();
974        assert!(
975            e_8_33.cmp_exact(&e_greg) == Ordering::Less,
976            "8/33 must be more accurate than 97/400, with a denominator 12x smaller"
977        );
978        assert!(e_31_128.cmp_exact(&e_greg) == Ordering::Less);
979        // 31/128 is ~124x more accurate.
980        let times = e_greg.div(&e_31_128).unwrap();
981        assert_eq!(times.to_decimal_string(1, Rounding::Trunc).unwrap(), "124.0");
982    }
983
984    #[test]
985    fn appendix_i2_earth_grouping_derives_the_metonic_cycle() {
986        let ratio = Ratio::from_decimal_str("12.368266761").unwrap();
987        let cf = cf_expand(&ratio, 32);
988        assert_eq!(cf[..9], [12, 2, 1, 2, 1, 1, 17, 2, 1]);
989        let cv = convergents(&cf);
990        let want = [(12u64, 1u64), (25, 2), (37, 3), (99, 8), (136, 11), (235, 19), (4131, 334)];
991        for (i, (n, d)) in want.iter().enumerate() {
992            assert_eq!(cv[i], r(*n, *d), "convergent {}", i + 1);
993        }
994        // §21.3-7: the Metonic cycle falls out with no special-casing.
995        assert!(cv.iter().any(|c| *c == r(235, 19)));
996    }
997
998    #[test]
999    fn appendix_i3_mars_intercalation() {
1000        let frac = Ratio::from_decimal_str("668.592165627").unwrap().frac();
1001        let cf = cf_expand(&frac, 32);
1002        assert_eq!(cf[..9], [0, 1, 1, 2, 4, 1, 2, 2, 1]);
1003        let cv = convergents(&cf);
1004        let want = [(1u64, 1u64), (1, 2), (3, 5), (13, 22), (16, 27), (45, 76), (106, 179)];
1005        for (i, (n, d)) in want.iter().enumerate() {
1006            assert_eq!(cv[i + 1], r(*n, *d), "convergent {}", i + 1);
1007        }
1008    }
1009
1010    #[test]
1011    fn appendix_i5_titan_intercalation() {
1012        let frac = Ratio::from_decimal_str("673.983719443").unwrap().frac();
1013        let cf = cf_expand(&frac, 32);
1014        assert_eq!(cf[..9], [0, 1, 60, 2, 2, 1, 2, 1, 11]);
1015        let cv = convergents(&cf);
1016        let want = [(1u64, 1u64), (60, 61), (121, 123), (302, 307), (423, 430)];
1017        for (i, (n, d)) in want.iter().enumerate() {
1018            assert_eq!(cv[i + 1], r(*n, *d), "convergent {}", i + 1);
1019        }
1020    }
1021
1022    #[test]
1023    fn convergents_alternate_around_the_value_and_improve() {
1024        // A structural property of continued fractions, and a good check that the
1025        // recurrence is right: successive convergents must strictly improve.
1026        let frac = Ratio::from_decimal_str("365.242190").unwrap().frac();
1027        let cv = convergents(&cf_expand(&frac, 32));
1028        let mut prev: Option<Ratio> = None;
1029        for c in cv.iter().skip(1) {
1030            let e = c.abs_diff(&frac).unwrap();
1031            if let Some(p) = prev {
1032                assert!(
1033                    e.cmp_exact(&p) == Ordering::Less,
1034                    "convergent {} did not improve",
1035                    c.to_ratio_string()
1036                );
1037            }
1038            prev = Some(e);
1039        }
1040        // The last convergent of an exact decimal reproduces it exactly.
1041        assert_eq!(cv.last().unwrap(), &frac);
1042    }
1043
1044    #[test]
1045    fn cf_expand_returns_the_full_sequence() {
1046        // §15.2: derivations must be auditable, so the whole walk is returned.
1047        let cf = cf_expand(&r(649, 200), 32);
1048        let cv = convergents(&cf);
1049        assert_eq!(cv.last().unwrap(), &r(649, 200));
1050        // A whole number has a one-term expansion.
1051        assert_eq!(cf_expand(&Ratio::from_u64(7), 32), vec![7]);
1052        // max_depth truncates rather than looping.
1053        assert_eq!(cf_expand(&frac_of("365.242190"), 3).len(), 3);
1054    }
1055
1056    fn frac_of(s: &str) -> Ratio {
1057        Ratio::from_decimal_str(s).unwrap().frac()
1058    }
1059}