Skip to main content

fsum/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::mem;
4use std::ops::{AddAssign, SubAssign};
5use std::borrow::Borrow;
6
7/// Accumulator that represents the exact sum of `f64` values and
8/// allows additional `f64` values to be added without loss of precision.
9#[derive(Default, Clone, Debug)]
10pub struct FSum {
11    partials: Vec<f64>
12}
13
14impl FSum {
15    /// Constructs zeroed accumulator.
16    ///
17    /// # Example
18    ///
19    /// ```
20    /// use fsum::FSum;
21    ///
22    /// assert_eq!(FSum::new().value(), 0.0);
23    /// ```
24    pub fn new() -> FSum {
25        FSum{ partials: Vec::new() }
26    }
27
28    /// Constructs accumulator with given `initial_value`.
29    ///
30    /// # Example
31    ///
32    /// ```
33    /// use fsum::FSum;
34    ///
35    /// assert_eq!(FSum::with_value(5.0).value(), 5.0);
36    /// ```
37    pub fn with_value(initial_value: f64) -> FSum {
38        FSum{ partials: vec![initial_value] }
39    }
40
41    /// Constructs accumulator with all values from `iter`.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// use fsum::FSum;
47    ///
48    /// assert_eq!(FSum::with_all((0..10).map(|_| 0.1)).value(), 1.0);
49    /// ```
50    pub fn with_all<Bf64, InIter>(values: InIter) -> FSum
51        where Bf64: Borrow<f64>, InIter: IntoIterator<Item=Bf64>
52    {
53        let mut result = Self::new();
54        result.add_all(values);
55        result
56    }
57
58    /// Increases the sum by `x` and returns `self`.
59    ///
60    /// # Example
61    ///
62    /// ```
63    /// use fsum::FSum;
64    ///
65    /// let mut s = FSum::new();
66    /// assert_eq!(s.value(), 0.0);
67    /// s.add(1.0);
68    /// assert_eq!(s.value(), 1.0);
69    /// s.add(2.0);
70    /// assert_eq!(s.value(), 3.0);
71    /// assert_eq!(s.add(5.0).value(), 8.0);
72    /// ```
73    ///
74    /// # Complexity
75    ///
76    /// The complexities are:
77    /// - time: from *O(1)* (optimistic) to *O(n)* (pessimistic), where *n* is the number of values added so far,
78    /// - memory: *O(1)*, but internal vector stored in `self` can be increased by 1 element.
79    ///
80    /// Usually the time complexity is close to optimistic.
81    pub fn add(&mut self, mut x: f64) -> &mut FSum {
82        // https://github.com/python/cpython/blob/master/Modules/mathmodule.c#L1323
83        let mut j = 0usize;
84        // This inner loop applies `hi`/`lo` summation to each
85        // partial so that the list of partial sums remains exact.
86        for i in 0..self.partials.len() {
87            let mut y: f64 = self.partials[i];
88            if x.abs() < y.abs() { mem::swap(&mut x, &mut y); }
89            // Rounded `x+y` is stored in `hi` with round-off stored in
90            // `lo`. Together `hi+lo` are exactly equal to `x+y`.
91            let hi = x + y;
92            let lo = y - (hi - x);
93            if lo != 0.0 {
94                self.partials[j] = lo;
95                j += 1;
96            }
97            x = hi;
98        }
99        if j >= self.partials.len() {
100            self.partials.push(x);
101        } else {
102            self.partials[j] = x;
103            self.partials.truncate(j + 1);
104        }
105        self
106    }
107
108    /// Increases the sum by all values from `iter`. Returns `self`.
109    ///
110    /// # Example
111    ///
112    /// ```
113    /// use fsum::FSum;
114    ///
115    /// assert_eq!(FSum::new().add_all((0..10).map(|_| 0.1)).value(), 1.0);
116    /// ```
117    pub fn add_all<InIter, Bf64>(&mut self, values: InIter) -> &mut FSum
118        where Bf64: Borrow<f64>, InIter: IntoIterator<Item=Bf64>
119    {
120        for x in values { self.add(*x.borrow()); }
121        self
122    }
123
124    /// Returns the current value of the sum.
125    ///
126    /// The complexities are:
127    /// - time: from *O(1)* (optimistic) to *O(n)* (pessimistic), where *n* is the number of values added so far,
128    /// - memory: *O(1)*.
129    ///
130    /// Usually the time complexity is close to optimistic.
131    ///
132    /// # Example
133    ///
134    /// ```
135    /// use fsum::FSum;
136    ///
137    /// assert_eq!(FSum::with_value(2.0).value(), 2.0);
138    /// ```
139    pub fn value(&self) -> f64 {
140        // https://github.com/python/cpython/blob/2b7411df5ca0b6ef714377730fd4d94693f26abd/Lib/test/test_math.py#L647
141        let mut n = self.partials.len();
142        if n == 0 { return 0.0; }
143        n -= 1;
144        let mut total = self.partials[n];
145        if n == 0 { return total; }
146        loop {  // sum partials from the top, stop when the sum becomes inexact:
147            let old_total = total;
148            n -= 1;
149            let x = self.partials[n];
150            total = old_total + x;
151            if n == 0 { return total; }
152            let error = x - (total - old_total);
153            if error != 0.0 {
154                /* Make half-even rounding work across multiple partials.
155                Needed so that sum([1e-16, 1, 1e16]) will round-up the last
156                digit to two instead of down to zero (the 1e-16 makes the 1
157                slightly closer to two).  With a potential 1 ULP rounding
158                error fixed-up, math.fsum() can guarantee commutativity. */
159                if (error < 0.0 && self.partials[n - 1] < 0.0)
160                    || (error > 0.0 && self.partials[n - 1] > 0.0)
161                {
162                    let y = error * 2.0;
163                    let x = total + y;
164                    if y == x - total { return x; }
165                }
166                return total;
167            }
168        };
169        //self.partials.iter().fold(0.0f64, |p, q| p + *q)
170    }
171
172    /// Sets the current sum to `0` and returns `self`.
173    pub fn reset(&mut self) -> &mut FSum {
174        self.partials.clear();
175        self
176    }
177
178    /// Sets the current sum to `value` and returns `self`.
179    ///
180    /// # Example
181    ///
182    /// ```
183    /// use fsum::FSum;
184    ///
185    /// assert_eq!(FSum::new().set(1.0).value(), 1.0);
186    /// ```
187    pub fn set(&mut self, value: f64) -> &mut FSum {
188        self.partials = vec![value];
189        self
190    }
191}
192
193impl AddAssign<f64> for FSum {
194    #[inline] fn add_assign(&mut self, other: f64) { self.add(other); }
195}
196
197impl SubAssign<f64> for FSum {
198    #[inline] fn sub_assign(&mut self, other: f64) { self.add(- other); }
199}
200
201impl From<f64> for FSum {
202    #[inline] fn from(initial_value: f64) -> Self {
203        Self::with_value(initial_value)
204    }
205}
206
207impl From<FSum> for f64 {
208    #[inline] fn from(fsum: FSum) -> Self {
209        fsum.value()
210    }
211}
212
213impl From<&FSum> for f64 {
214    #[inline] fn from(fsum: &FSum) -> Self {
215        fsum.value()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn fsum() {
225        assert_eq!(FSum::new().add(2.0).add(3.0).value(), 5.0);
226        assert_eq!(FSum::with_value(2.0).add(3.0).value(), 5.0);
227        assert_eq!(FSum::with_all((0..10).map(|_| 0.1)).value(), 1.0);
228        assert_eq!(FSum::new().add(1e100).add(1.0).add(-1e100).value(), 1.0);
229        assert_eq!(
230            FSum::with_all([1e100, 1.0, -1e100, 1e-100, 1e50, -1.0, -1e50]).value(),
231            1e-100
232        );
233        assert_eq!(FSum::with_all([-1e308, 1e308, 1e308]).value(), 1e308);
234        assert_eq!(FSum::with_all([1e308, -1e308, 1e308]).value(), 1e308);
235    }
236
237    #[test]
238    fn fsum_add_sub_assign_reset_into() {
239        let mut s: FSum = Default::default();
240        assert_eq!(s.value(), 0.0);
241        s += 1.0;
242        assert_eq!(s.value(), 1.0);
243        s += 2.0;
244        assert_eq!(s.value(), 3.0);
245        s -= 1.0;
246        assert_eq!(s.value(), 2.0);
247        s.reset();
248        assert_eq!(s.value(), 0.0);
249        s.set(5.0);
250        assert_eq!(s.value(), 5.0);
251        assert_eq!(5.0, s.into());
252    }
253}