// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2021 Datadog, Inc.
package stat
import (
"fmt"
"math"
)
// SummaryStatistics keeps track of the count, the sum, the min and the max of
// recorded values. We use a compensated sum to avoid accumulating rounding
// errors (see https://en.wikipedia.org/wiki/Kahan_summation_algorithm).
type SummaryStatistics struct {
count float64
sum float64
sumCompensation float64
simpleSum float64
min float64
max float64
}
func NewSummaryStatistics() *SummaryStatistics {
return &SummaryStatistics{
count: 0,
sum: 0,
sumCompensation: 0,
simpleSum: 0,
min: math.Inf(1),
max: math.Inf(-1),
}
}
// NewSummaryStatisticsFromData constructs SummaryStatistics from the provided data.
func NewSummaryStatisticsFromData(count, sum, min, max float64) (*SummaryStatistics, error) {
if !(count >= 0) {
return nil, fmt.Errorf("count (%g) must be positive or zero", count)
}
if count > 0 && min > max {
return nil, fmt.Errorf("min (%g) cannot be greater than max (%g) if count (%g) is positive", min, max, count)
}
if count == 0 && (min != math.Inf(1) || max != math.Inf(-1)) {
return nil, fmt.Errorf("empty summary statistics must have min (%g) and max (%g) equal to positive and negative infinities respectively", min, max)
}
return &SummaryStatistics{
count: count,
sum: sum,
sumCompensation: 0,
simpleSum: sum,
min: min,
max: max,
}, nil
}
func (s *SummaryStatistics) Count() float64 {
return s.count
}
func (s *SummaryStatistics) Sum() float64 {
// Better error bounds to add both terms as the final sum
tmp := s.sum + s.sumCompensation
if math.IsNaN(tmp) && math.IsInf(s.simpleSum, 0) {
// If the compensated sum is spuriously NaN from accumulating one or more same-signed infinite
// values, return the correctly-signed infinity stored in simpleSum.
return s.simpleSum
} else {
return tmp
}
}
func (s *SummaryStatistics) Min() float64 {
return s.min
}
func (s *SummaryStatistics) Max() float64 {
return s.max
}
func (s *SummaryStatistics) Add(value, count float64) {
s.AddToCount(count)
s.AddToSum(value * count)
if value < s.min {
s.min = value
}
if value > s.max {
s.max = value
}
}
func (s *SummaryStatistics) AddToCount(addend float64) {
s.count += addend
}
func (s *SummaryStatistics) AddToSum(addend float64) {
s.sumWithCompensation(addend)
s.simpleSum += addend
}
func (s *SummaryStatistics) MergeWith(o *SummaryStatistics) {
s.count += o.count
s.sumWithCompensation(o.sum)
s.sumWithCompensation(o.sumCompensation)
s.simpleSum += o.simpleSum
if o.min < s.min {
s.min = o.min
}
if o.max > s.max {
s.max = o.max
}
}
func (s *SummaryStatistics) sumWithCompensation(value float64) {
tmp := value - s.sumCompensation
velvel := s.sum + tmp // little wolf of rounding error
s.sumCompensation = velvel - s.sum - tmp
s.sum = velvel
}
// Reweight adjusts the statistics so that they are equal to what they would
// have been if AddWithCount had been called with counts multiplied by factor.
func (s *SummaryStatistics) Reweight(factor float64) {
s.count *= factor
s.sum *= factor
s.sumCompensation *= factor
s.simpleSum *= factor
if factor == 0 {
s.min = math.Inf(1)
s.max = math.Inf(-1)
}
}
// Rescale adjusts the statistics so that they are equal to what they would have
// been if AddWithCount had been called with values multiplied by factor.
func (s *SummaryStatistics) Rescale(factor float64) {
s.sum *= factor
s.sumCompensation *= factor
s.simpleSum *= factor
if factor > 0 {
s.min *= factor
s.max *= factor
} else if factor < 0 {
tmp := s.max * factor
s.max = s.min * factor
s.min = tmp
} else if s.count != 0 {
s.min = 0
s.max = 0
}
}
func (s *SummaryStatistics) Clear() {
s.count = 0
s.sum = 0
s.sumCompensation = 0
s.simpleSum = 0
s.min = math.Inf(1)
s.max = math.Inf(-1)
}
func (s *SummaryStatistics) Copy() *SummaryStatistics {
return &SummaryStatistics{
count: s.count,
sum: s.sum,
sumCompensation: s.sumCompensation,
simpleSum: s.simpleSum,
min: s.min,
max: s.max,
}
}