Skip to main content

ferroui_dynamic_data/aggregation/
std_dev_ex.rs

1//! `StdDevEx` (`Aggregation/StdDevEx.cs`): the running sample standard deviation of the values of
2//! the items.
3//!
4//! The overloads of upstream for `int`, `long`, `double` and `float` are one generic method over
5//! [`StdDevValue`]; the deviation is a `double` for each. `fallbackValue`, the value for fewer than
6//! two items, is an argument of every form (upstream's forms over aggregate change sets and of
7//! `float` default it to zero). The operators are methods of
8//! [`AggregationCacheEx`](super::AggregationCacheEx), [`AggregationListEx`](super::AggregationListEx)
9//! and [`AggregateChangeSetEx`](super::AggregateChangeSetEx).
10//!
11//! Not ported: the `decimal` overloads - no decimal type (phase 1b).
12
13use super::{AggregateChangeSets, AggregateType, StdDev};
14use ferroui_reactive::ObservableExt;
15use ferroui_system::IObservable;
16use std::rc::Rc;
17
18/// A value whose standard deviation the aggregations compute (`int`, `long`, `float`, `double`).
19pub trait StdDevValue: Copy + 'static {
20    /// The type of the sums of the values and of their squares: `i64` for `i32` and `i64` (upstream
21    /// widens `int` to `long`), the type itself for `f32` and `f64`.
22    type Moment: Copy + 'static;
23
24    /// The sum of no values.
25    const ZERO: Self::Moment;
26
27    /// The value as a moment.
28    fn moment(self) -> Self::Moment;
29
30    /// `left + right`, wrapping on overflow for the integers (C#'s unchecked arithmetic).
31    fn plus(left: Self::Moment, right: Self::Moment) -> Self::Moment;
32
33    /// `left - right`, wrapping on overflow for the integers.
34    fn minus(left: Self::Moment, right: Self::Moment) -> Self::Moment;
35
36    /// `left * right`, wrapping on overflow for the integers.
37    fn times(left: Self::Moment, right: Self::Moment) -> Self::Moment;
38
39    /// The sample standard deviation of `count` values (at least two) whose sum is
40    /// `sum_of_items` and the sum of whose squares is `sum_of_squares`.
41    fn deviation(count: i32, sum_of_items: Self::Moment, sum_of_squares: Self::Moment) -> f64;
42
43    /// The value as the result (the implicit conversion of upstream's `fallbackValue`).
44    fn to_f64(self) -> f64;
45}
46
47/// `IntegralStdDev`: the moments cross-multiplied in exact integer arithmetic, so that values far
48/// from zero, whose moments exceed the integers a `double` holds exactly, keep their variance.
49///
50/// Deviation (DEVIATIONS.md, DynamicData): the products are computed in `i128` where upstream uses
51/// `decimal` (phase 1b), and the quotient of the sum of the squared deviations by
52/// `count * (count - 1)` is its integral part plus its fraction in `f64` (exact under the same
53/// condition as upstream's: the accumulators have not overflowed).
54fn integral_std_dev(count: i32, sum_of_items: i64, sum_of_squares: i64) -> f64 {
55    let count = i128::from(count);
56    let sum_of_items = i128::from(sum_of_items);
57    let scaled_sum_of_squared_deviations = i128::from(sum_of_squares) * count - sum_of_items * sum_of_items;
58    let divisor = count * (count - 1);
59    let quotient = scaled_sum_of_squared_deviations / divisor;
60    let remainder = scaled_sum_of_squared_deviations % divisor;
61    (quotient as f64 + remainder as f64 / divisor as f64).sqrt()
62}
63
64impl StdDevValue for i32 {
65    type Moment = i64;
66    const ZERO: i64 = 0;
67
68    fn moment(self) -> i64 {
69        i64::from(self)
70    }
71
72    fn plus(left: i64, right: i64) -> i64 {
73        left.wrapping_add(right)
74    }
75
76    fn minus(left: i64, right: i64) -> i64 {
77        left.wrapping_sub(right)
78    }
79
80    fn times(left: i64, right: i64) -> i64 {
81        left.wrapping_mul(right)
82    }
83
84    fn deviation(count: i32, sum_of_items: i64, sum_of_squares: i64) -> f64 {
85        integral_std_dev(count, sum_of_items, sum_of_squares)
86    }
87
88    fn to_f64(self) -> f64 {
89        f64::from(self)
90    }
91}
92
93impl StdDevValue for i64 {
94    type Moment = i64;
95    const ZERO: i64 = 0;
96
97    fn moment(self) -> i64 {
98        self
99    }
100
101    fn plus(left: i64, right: i64) -> i64 {
102        left.wrapping_add(right)
103    }
104
105    fn minus(left: i64, right: i64) -> i64 {
106        left.wrapping_sub(right)
107    }
108
109    fn times(left: i64, right: i64) -> i64 {
110        left.wrapping_mul(right)
111    }
112
113    fn deviation(count: i32, sum_of_items: i64, sum_of_squares: i64) -> f64 {
114        integral_std_dev(count, sum_of_items, sum_of_squares)
115    }
116
117    fn to_f64(self) -> f64 {
118        self as f64
119    }
120}
121
122impl StdDevValue for f64 {
123    type Moment = f64;
124    const ZERO: f64 = 0.0;
125
126    fn moment(self) -> f64 {
127        self
128    }
129
130    fn plus(left: f64, right: f64) -> f64 {
131        left + right
132    }
133
134    fn minus(left: f64, right: f64) -> f64 {
135        left - right
136    }
137
138    fn times(left: f64, right: f64) -> f64 {
139        left * right
140    }
141
142    fn deviation(count: i32, sum_of_items: f64, sum_of_squares: f64) -> f64 {
143        let count = f64::from(count);
144        ((sum_of_squares - (sum_of_items * sum_of_items) / count) / (count - 1.0)).sqrt()
145    }
146
147    fn to_f64(self) -> f64 {
148        self
149    }
150}
151
152impl StdDevValue for f32 {
153    type Moment = f32;
154    const ZERO: f32 = 0.0;
155
156    fn moment(self) -> f32 {
157        self
158    }
159
160    fn plus(left: f32, right: f32) -> f32 {
161        left + right
162    }
163
164    fn minus(left: f32, right: f32) -> f32 {
165        left - right
166    }
167
168    fn times(left: f32, right: f32) -> f32 {
169        left * right
170    }
171
172    /// The variance in `float`, as upstream computes it, and its square root in `double`.
173    fn deviation(count: i32, sum_of_items: f32, sum_of_squares: f32) -> f64 {
174        let variance = (sum_of_squares - (sum_of_items * sum_of_items) / count as f32) / (count - 1) as f32;
175        f64::from(variance).sqrt()
176    }
177
178    fn to_f64(self) -> f64 {
179        f64::from(self)
180    }
181}
182
183/// `StdDev<T>(IObservable<IAggregateChangeSet<T>>, Func<T, TValue>, TValue fallbackValue)` with
184/// `StdDevCalc`: the count, the sum and the sum of the squares of the values, accumulated from
185/// zero, and the sample standard deviation; `fallback_value` for fewer than two items.
186pub(crate) fn std_dev<T: 'static, V: StdDevValue>(source: AggregateChangeSets<T>, value_selector: impl Fn(&T) -> V + 'static, fallback_value: V) -> Rc<dyn IObservable<f64>> {
187    let fallback_value = fallback_value.to_f64();
188    source
189        .scan_with(StdDev::new(0, V::ZERO, V::ZERO), move |state: StdDev<V::Moment>, changes| {
190            changes.items().fold(state, |current, aggregate_item| {
191                let item = value_selector(aggregate_item.item()).moment();
192                let square = V::times(item, item);
193                match aggregate_item.aggregate_type() {
194                    AggregateType::Add => {
195                        StdDev::new(current.count() + 1, V::plus(current.sum_of_items(), item), V::plus(current.sum_of_squares(), square))
196                    }
197                    AggregateType::Remove => {
198                        StdDev::new(current.count() - 1, V::minus(current.sum_of_items(), item), V::minus(current.sum_of_squares(), square))
199                    }
200                }
201            })
202        })
203        .select(move |values: StdDev<V::Moment>| {
204            if values.count() < 2 {
205                fallback_value
206            } else {
207                V::deviation(values.count(), values.sum_of_items(), values.sum_of_squares())
208            }
209        })
210}