1#[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
31pub type NumError = TimeError;
35
36pub 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
73pub 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
107pub 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 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 let (q, _) = x.quot_rem(&r);
130 let sum = match r.try_add(&q) {
131 Some(s) => s,
132 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
160pub 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#[cfg_attr(feature = "u512", derive(Copy))]
189#[derive(Clone, PartialEq, Eq, Debug)]
190pub struct Ratio {
191 num: Ticks,
192 den: Ticks,
193}
194
195pub 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 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 pub fn from_int(n: Ticks) -> Ratio {
228 Ratio {
229 num: n,
230 den: <Ticks as TickInt>::one(),
231 }
232 }
233
234 pub fn from_u64(n: u64) -> Ratio {
236 Ratio::from_int(<Ticks as TickInt>::from_u64(n))
237 }
238
239 pub fn zero() -> Ratio {
241 Ratio::from_u64(0)
242 }
243
244 pub fn one() -> Ratio {
246 Ratio::from_u64(1)
247 }
248
249 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 pub fn numer(&self) -> &Ticks {
278 &self.num
279 }
280
281 pub fn denom(&self) -> &Ticks {
283 &self.den
284 }
285
286 pub fn is_zero(&self) -> bool {
288 self.num.is_zero_ticks()
289 }
290
291 pub fn is_integer(&self) -> bool {
293 self.den == <Ticks as TickInt>::one()
294 }
295
296 pub fn floor(&self) -> Ticks {
298 self.num.quot_rem(&self.den).0
299 }
300
301 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 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 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 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 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 pub fn mul(&self, other: &Ratio) -> Result<Ratio> {
352 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 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 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 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 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 #[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 #[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#[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 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 pub fn exact(v: Ratio) -> RatInterval {
526 RatInterval {
527 lo: v.clone(),
528 hi: v,
529 }
530 }
531
532 pub fn lo(&self) -> &Ratio {
534 &self.lo
535 }
536
537 pub fn hi(&self) -> &Ratio {
539 &self.hi
540 }
541
542 pub fn is_exact(&self) -> bool {
544 self.lo == self.hi
545 }
546
547 pub fn width(&self) -> Result<Ratio> {
549 self.hi.sub(&self.lo)
550 }
551
552 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 pub fn contains_zero(&self) -> bool {
560 self.lo.is_zero()
561 }
562
563 pub fn add(&self, other: &RatInterval) -> Result<RatInterval> {
565 RatInterval::new(self.lo.add(&other.lo)?, self.hi.add(&other.hi)?)
566 }
567
568 pub fn sub(&self, other: &RatInterval) -> Result<RatInterval> {
570 RatInterval::new(self.lo.sub(&other.hi)?, self.hi.sub(&other.lo)?)
571 }
572
573 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 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 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 let (lo_scaled, _) = mul_div(self.lo.numer(), &s2, self.lo.denom())?;
627 let lo_root = isqrt_floor(&lo_scaled);
628
629 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 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#[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 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#[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 #[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 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 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 assert_eq!(mul_div_rounded(&t(7), &t(1), &t(2), Rounding::HalfEven).unwrap(), t(4));
781 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 #[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 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 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 assert_eq!(f.bit_len(), 256);
825 }
826
827 #[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 let m = <Ticks as TickInt>::domain_max();
844 let a = Ratio::new(m.clone(), m.clone()).unwrap(); 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 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 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 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 #[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 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 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 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 #[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 assert_eq!(cv[1], r(1, 4));
963 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 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 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 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 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 assert_eq!(cv.last().unwrap(), &frac);
1042 }
1043
1044 #[test]
1045 fn cf_expand_returns_the_full_sequence() {
1046 let cf = cf_expand(&r(649, 200), 32);
1048 let cv = convergents(&cf);
1049 assert_eq!(cv.last().unwrap(), &r(649, 200));
1050 assert_eq!(cf_expand(&Ratio::from_u64(7), 32), vec![7]);
1052 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}