axiolid_exact_arrangement/
ratio.rs1use axiolid_exact::Dyadic;
4use num_bigint::{BigUint, Sign as BigSign};
5
6pub fn round_ratio(numerator: &Dyadic, denominator: &Dyadic) -> Option<f64> {
10 let n = numerator.mantissa().magnitude();
11 let d = denominator.mantissa().magnitude();
12 if d.bits() == 0 {
13 return None;
14 }
15 if n.bits() == 0 {
16 return Some(0.0);
17 }
18 let negative = (numerator.mantissa().sign() == BigSign::Minus)
19 != (denominator.mantissa().sign() == BigSign::Minus);
20
21 let k = 55 + d.bits() as i64 - n.bits() as i64;
23 let (q, sticky) = if k >= 0 {
24 let scaled = n << k as u64;
25 (&scaled / d, (&scaled % d).bits() != 0)
26 } else {
27 let scaled = d << (-k) as u64;
28 (n / &scaled, (n % &scaled).bits() != 0)
29 };
30 let e = numerator.exponent() - denominator.exponent() - k;
31 let q_bits = q.bits() as i64;
32 let lsb = (e + q_bits - 1 - 52).max(-1074);
34 let drop = lsb - e;
35 if drop > q_bits {
36 return Some(0.0);
37 }
38 let kept = &q >> drop as u64;
39 let rest = &q - (&kept << drop as u64);
40 let half = BigUint::from(1u8) << (drop - 1) as u64;
41 let round_up = match rest.cmp(&half) {
42 std::cmp::Ordering::Greater => true,
43 std::cmp::Ordering::Equal => sticky || kept.bit(0),
44 std::cmp::Ordering::Less => false,
45 };
46 let mantissa = u64::try_from(&kept).ok()? + u64::from(round_up);
47 if mantissa == 0 {
48 return Some(0.0);
49 }
50 let top = lsb + (64 - mantissa.leading_zeros() as i64);
51 if top > 1024 {
52 return None;
53 }
54 let magnitude = scale_pow2(mantissa as f64, lsb);
55 Some(if negative { -magnitude } else { magnitude })
56}
57
58fn scale_pow2(mut x: f64, mut e: i64) -> f64 {
60 let pow2 = |s: i64| f64::from_bits(((1023 + s) as u64) << 52);
61 while e != 0 {
62 let step = e.clamp(-500, 500);
63 x *= pow2(step);
64 e -= step;
65 }
66 x
67}
68
69#[cfg(test)]
70mod tests {
71 use axiolid_exact::Arith;
72
73 use super::*;
74
75 fn dyadic(x: f64) -> Dyadic {
76 Dyadic::try_from_f64(x).expect("finite")
77 }
78
79 struct Lcg(u64);
81
82 impl Lcg {
83 fn next(&mut self) -> u64 {
84 self.0 = self
85 .0
86 .wrapping_mul(6364136223846793005)
87 .wrapping_add(1442695040888963407);
88 self.0 >> 11
89 }
90 }
91
92 #[test]
93 fn division_of_doubles_matches_ieee() {
94 let mut rng = Lcg(7);
95 for _ in 0..4000 {
96 let scale = |r: u64| {
97 let mantissa = (r >> 3) as f64 + 1.0;
98 let shift = (r % 61) as i32 - 30;
99 mantissa * 2f64.powi(shift) * if r & 1 == 0 { 1.0 } else { -1.0 }
100 };
101 let (n, d) = (scale(rng.next()), scale(rng.next()));
102 let got = round_ratio(&dyadic(n), &dyadic(d)).expect("in range");
103 assert!(got.to_bits() == (n / d).to_bits(), "{n} / {d}");
104 }
105 }
106
107 #[test]
108 fn ties_round_to_even_and_sticky_bits_round_up() {
109 let ulp = f64::EPSILON;
110 let tie = dyadic(1.0).add(&dyadic(ulp / 2.0));
112 assert_eq!(round_ratio(&tie, &dyadic(1.0)), Some(1.0));
113 let tie = dyadic(1.0).add(&dyadic(1.5 * ulp));
115 assert_eq!(round_ratio(&tie, &dyadic(1.0)), Some(1.0 + 2.0 * ulp));
116 let above = dyadic(1.0)
118 .add(&dyadic(ulp / 2.0))
119 .add(&dyadic(2f64.powi(-200)));
120 assert_eq!(round_ratio(&above, &dyadic(1.0)), Some(1.0 + ulp));
121 let sticky = dyadic(3.0)
124 .add(&dyadic(1.5 * ulp))
125 .add(&dyadic(2f64.powi(-200)));
126 assert_eq!(round_ratio(&sticky, &dyadic(3.0)), Some(1.0 + ulp));
127 let tie = dyadic(3.0).add(&dyadic(1.5 * ulp));
129 assert_eq!(round_ratio(&tie, &dyadic(3.0)), Some(1.0));
130 }
131
132 #[test]
133 fn subnormals_and_underflow_round_correctly() {
134 let min = f64::from_bits(1);
135 assert_eq!(round_ratio(&dyadic(min), &dyadic(1.0)), Some(min));
136 let half = Dyadic::from_parts(1.into(), -1075);
138 assert_eq!(round_ratio(&half, &dyadic(1.0)), Some(0.0));
139 let above = half.add(&Dyadic::from_parts(1.into(), -1200));
141 assert_eq!(round_ratio(&above, &dyadic(1.0)), Some(min));
142 let three_halves = Dyadic::from_parts(3.into(), -1075);
144 assert_eq!(round_ratio(&three_halves, &dyadic(1.0)), Some(2.0 * min));
145 let tiny = Dyadic::from_parts(1.into(), -3000);
147 assert_eq!(round_ratio(&tiny, &dyadic(1.0)), Some(0.0));
148 }
149
150 #[test]
151 fn overflow_and_zero_denominator_are_refused() {
152 let huge = Dyadic::from_parts(1.into(), 1024);
153 assert_eq!(round_ratio(&huge, &dyadic(1.0)), None);
154 assert_eq!(round_ratio(&dyadic(1.0), &Dyadic::zero()), None);
155 let max = dyadic(f64::MAX);
156 assert_eq!(round_ratio(&max, &dyadic(1.0)), Some(f64::MAX));
157 let past = max.add(&Dyadic::from_parts(1.into(), 970));
159 assert_eq!(round_ratio(&past, &dyadic(1.0)), None);
160 }
161
162 #[test]
163 fn signs_combine_and_zero_is_positive_zero() {
164 assert_eq!(round_ratio(&dyadic(-3.0), &dyadic(4.0)), Some(-0.75));
165 assert_eq!(round_ratio(&dyadic(3.0), &dyadic(-4.0)), Some(-0.75));
166 assert_eq!(round_ratio(&dyadic(-3.0), &dyadic(-4.0)), Some(0.75));
167 let zero = round_ratio(&Dyadic::zero(), &dyadic(-4.0)).expect("zero");
168 assert_eq!(zero.to_bits(), 0.0f64.to_bits());
169 }
170}