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