Skip to main content

dashu_float/
iter.rs

1//! Implementation of core::iter traits
2
3use crate::{
4    error::assert_finite,
5    fbig::FBig,
6    repr::{Context, Repr, Word},
7    round::Round,
8};
9use core::iter::{Product, Sum};
10
11/// Correctly-rounded summation of finite floats.
12///
13/// Every addend is accumulated exactly at the [`Repr`] level (no per-step rounding); the exact total
14/// is then rounded a single time to the target context. The target context is `Context::max` over all
15/// addend contexts (matching chained `+`), and the final round reuses [`Context::repr_round`] — this
16/// yields the same result as rounding the mathematically exact sum (MPFR `mpfr_sum` semantics).
17///
18/// Because the accumulator is exact, summing addends with widely differing exponents can grow the
19/// intermediate significand to span the full exponent range of the inputs.
20fn precise_sum<R: Round, const B: Word>(
21    mut iter: impl Iterator<Item = (Repr<B>, Context<R>)>,
22) -> FBig<R, B> {
23    let (mut acc, mut context) = match iter.next() {
24        Some((repr, ctx)) => {
25            assert_finite(&repr);
26            (repr, ctx)
27        }
28        None => return FBig::ZERO, // empty iterator → additive identity
29    };
30    for (repr, ctx) in iter {
31        assert_finite(&repr);
32        acc = acc + &repr;
33        context = Context::max(context, ctx);
34    }
35    // Exact cancellation can leave `acc` with a zero significand whose exponent coincides with the
36    // `-0` sentinel (-1) — `Repr::new` then mislabels it `-0`. Canonicalize the sign per IEEE 754
37    // §6.3 (x + (-x) = +0 except under roundTowardNegative), mirroring `Add`'s `cancel_zero`.
38    if acc.significand.is_zero() {
39        acc = if R::IS_ROUND_TOWARD_NEGATIVE {
40            Repr::neg_zero()
41        } else {
42            Repr::zero()
43        };
44    }
45    FBig::new(context.repr_round(acc).value(), context)
46}
47
48impl<R: Round, const B: Word> Sum for FBig<R, B> {
49    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
50        precise_sum(iter.map(|v| (v.repr, v.context)))
51    }
52}
53
54impl<'a, R: Round, const B: Word> Sum<&'a FBig<R, B>> for FBig<R, B> {
55    fn sum<I: Iterator<Item = &'a FBig<R, B>>>(iter: I) -> Self {
56        precise_sum(iter.map(|v| (v.repr.clone(), v.context)))
57    }
58}
59
60impl<R: Round, const B: Word> Product for FBig<R, B> {
61    fn product<I: Iterator<Item = Self>>(iter: I) -> Self {
62        iter.fold(FBig::ONE, |acc, x| acc * x)
63    }
64}
65
66impl<'a, R: Round, const B: Word> Product<&'a FBig<R, B>> for FBig<R, B> {
67    fn product<I: Iterator<Item = &'a FBig<R, B>>>(iter: I) -> Self {
68        iter.fold(FBig::ONE, |acc, x| acc * x)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::round::mode::{HalfAway, HalfEven, Zero};
76    use alloc::vec::Vec;
77    use core::str::FromStr;
78    use dashu_int::IBig;
79
80    type F = FBig<HalfAway, 10>;
81
82    fn r<const B: Word>(sig: i128, exp: isize) -> Repr<B> {
83        Repr::new(IBig::from(sig), exp)
84    }
85
86    #[test]
87    fn sum_empty() {
88        let s: F = core::iter::empty::<F>().sum();
89        assert_eq!(s, F::ZERO);
90        assert!(s.repr().is_pos_zero());
91    }
92
93    #[test]
94    fn sum_single() {
95        let a = F::from_str("1.234").unwrap();
96        let s: F = core::iter::once(a.clone()).sum();
97        assert_eq!(s, a);
98    }
99
100    #[test]
101    fn sum_cancellation_is_positive_zero() {
102        // 0.5 + (-0.5) must yield +0 under round-to-nearest (IEEE 754 §6.3), matching chained `+` —
103        // the exact-cancellation exponent (-1) must not be mistaken for the `-0` sentinel.
104        let half = F::from_str("0.5").unwrap();
105        let s: F = [half.clone(), -half].into_iter().sum();
106        assert!(s.repr().is_pos_zero());
107        assert!(!s.repr().is_neg_zero());
108    }
109
110    #[test]
111    fn sum_ref_iter() {
112        let a = F::from_str("1.23").unwrap();
113        let b = F::from_str("4.56").unwrap();
114        let vals = [a, b];
115        let s: F = vals.iter().sum();
116        assert_eq!(s, F::from_str("5.79").unwrap());
117    }
118
119    #[test]
120    fn sum_exact_cancellation() {
121        let a = F::from_str("1.00").unwrap();
122        let b = F::from_str("-1.00").unwrap();
123        let s: F = [a, b].into_iter().sum();
124        assert!(s.repr().is_pos_zero()); // exact zero → +0
125    }
126
127    #[test]
128    fn sum_uses_max_precision() {
129        let a = F::from_str("1.234").unwrap(); // precision 4
130        let b = F::from_str("5.6").unwrap(); // precision 2
131        let s: F = [a, b].into_iter().sum();
132        assert_eq!(s, F::from_str("6.834").unwrap());
133        assert_eq!(s.precision(), 4); // max of the two operand precisions
134    }
135
136    // The headline regression: per-step truncation loses almost all the mass, the precise sum does
137    // not. 11 copies of 0.9 at precision 1, truncation: exact sum 9.9 → 9, naive fold → 1.
138    #[test]
139    fn sum_precise_beats_naive_truncation() {
140        type Z = FBig<Zero, 10>;
141        let vals: Vec<Z> = (0..11)
142            .map(|_| Z::from_parts(9.into(), -1)) // 0.9, precision 1
143            .collect();
144
145        let precise: Z = vals.iter().cloned().sum();
146        assert_eq!(precise, Z::from_parts(9.into(), 0)); // 9.9 truncated to 1 digit = 9
147
148        let naive: Z = vals.iter().cloned().fold(Z::ZERO, |acc, v| acc + v);
149        assert_eq!(naive, Z::from_parts(1.into(), 0)); // 0.9+0.9=1.8→1, then stuck at 1
150        assert_ne!(precise, naive);
151    }
152
153    // Cross-check against an independent oracle: an exact fold at unlimited precision (where `+` is
154    // exact) rounded once must equal the precise sum.
155    #[test]
156    fn sum_matches_unlimited_oracle() {
157        fn check<const B: Word>(strs: &[&str]) {
158            let vals: Vec<FBig<HalfEven, B>> = strs
159                .iter()
160                .map(|s| FBig::<HalfEven, B>::from_str(s).unwrap())
161                .collect();
162            let target = vals.iter().map(|v| v.precision()).max().unwrap_or(0);
163
164            let precise: FBig<HalfEven, B> = vals.clone().into_iter().sum();
165
166            let exact: FBig<HalfEven, B> = vals
167                .iter()
168                .map(|v| v.clone().with_precision(0).value())
169                .fold(FBig::<HalfEven, B>::ZERO, |acc, v| acc + v);
170            let oracle = exact.with_precision(target).value();
171
172            assert_eq!(precise.precision(), oracle.precision());
173            assert_eq!(precise, oracle);
174        }
175        check::<10>(&["1.234", "5.6", "0.001"]);
176        check::<10>(&["9.99", "0.009", "0.0009"]);
177        check::<2>(&["1.1", "0.01", "0.001", "0.0001"]); // 1.5, 0.25, 0.125, 0.0625
178    }
179
180    #[test]
181    fn product_owned_and_ref() {
182        let a = F::from_str("1.2").unwrap();
183        let b = F::from_str("3.0").unwrap();
184        let expected = &a * &b;
185        let owned: F = [a.clone(), b.clone()].into_iter().product();
186        let by_ref: F = [a, b].iter().product();
187        assert_eq!(owned, expected);
188        assert_eq!(by_ref, expected);
189    }
190
191    #[test]
192    #[should_panic(expected = "arithmetic operations with the infinity are not allowed")]
193    fn sum_infinite_panics() {
194        let _: F = core::iter::once(F::INFINITY).sum();
195    }
196
197    // Exercises the zero short-circuit in `impl Add for &Repr`: a `-0` sentinel exponent must not
198    // corrupt the accumulator exponent.
199    #[test]
200    fn repr_add_neg_zero_is_identity() {
201        let nz = Repr::<10>::neg_zero();
202        let x = r::<10>(5, -2); // 0.05
203                                // -0 + x == x, x + -0 == x (exact, pre-rounding)
204        assert_eq!(&nz + &x, x);
205        assert_eq!(&x + &nz, x);
206        assert_eq!(&nz + &nz, Repr::<10>::neg_zero());
207        // 1 + -1 cancels to +0 (not -0)
208        assert_eq!(&r::<10>(1, 0) + &r::<10>(-1, 0), Repr::<10>::zero());
209    }
210}