github.com/haraldrudell/parl@v0.4.176/ptime/averager.go (about)

     1  /*
     2  © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package ptime
     7  
     8  import (
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/haraldrudell/parl/perrors"
    13  	"github.com/haraldrudell/parl/pslices"
    14  	"golang.org/x/exp/constraints"
    15  )
    16  
    17  const (
    18  	averagerDefaultPeriod = time.Second
    19  	averagerDefaultCount  = 10
    20  )
    21  
    22  // Averager is a container for averaging integer values providing perioding and history.
    23  //   - maxCount is the maximum interval over which average is calculated
    24  //   - average is the average value of provided datapoints during all periods
    25  //   - because averaging is over 10 or many periods, the average will change slowly
    26  type Averager[T constraints.Integer] struct {
    27  	// period provides linear interval-numbering from a time.Time timestamp
    28  	period   Period
    29  	maxCount int
    30  
    31  	sliceLock sync.RWMutex
    32  	// each pointer has a period index, datapoint count and datapoint aggregate
    33  	//	- may be nil
    34  	//	- if no value was provided during a particular period, that pointer is nil
    35  	//	- if length >0, first and last intervals are not nil
    36  	//	- thread-safe behind sliceLock
    37  	intervals []*AverageInterval
    38  }
    39  
    40  // NewAverager returns an object that calculates average over a number of interval periods.
    41  //   - interval-length is 1 s
    42  //   - averaging over 10 intervals
    43  func NewAverager[T constraints.Integer]() (averager *Averager[T]) {
    44  	return &Averager[T]{
    45  		period:   *NewPeriod(averagerDefaultPeriod),
    46  		maxCount: averagerDefaultCount,
    47  	}
    48  }
    49  
    50  // NewAverager2 returns an object that calculates average over a number of interval periods.
    51  //   - period 0 means default 1 s interval-length
    52  //   - periodCount 0 means averaging over 10 intervals
    53  func NewAverager2[T constraints.Integer](period time.Duration, periodCount int) (averager *Averager[T]) {
    54  	if period == 0 {
    55  		period = averagerDefaultPeriod
    56  	} else if period < 1 {
    57  		perrors.ErrorfPF("period cannot be less than 1 ns: %s", period)
    58  	}
    59  	if periodCount == 0 {
    60  		periodCount = averagerDefaultCount
    61  	} else if periodCount < 2 {
    62  		perrors.ErrorfPF("periodCount cannot be less than 2: %d", periodCount)
    63  	}
    64  	return &Averager[T]{
    65  		period:   *NewPeriod(period),
    66  		maxCount: periodCount,
    67  	}
    68  }
    69  
    70  // Add adds a new value basis for averaging
    71  //   - value is the sample value
    72  //   - t is the sample time, default time.Now()
    73  func (a *Averager[T]) Add(value T, t ...time.Time) {
    74  	if interval := a.getCurrent(a.period.Index(t...)); interval != nil {
    75  		interval.Add(float64(value))
    76  	}
    77  }
    78  
    79  // Average returns the average
    80  //   - t is time, default time.Now()
    81  //   - if sample count aggregated across the slice zero, average is zero
    82  //   - count is number of values making up the average
    83  func (a *Averager[T]) Average(t ...time.Time) (average float64, count uint64) {
    84  
    85  	// invoking getCurrent ensure periods are updated
    86  	//	- if no recent datapoints were provided, the slice may contain old values
    87  	a.getCurrent(a.period.Index(t...))
    88  	a.sliceLock.RLock()
    89  	defer a.sliceLock.RUnlock()
    90  
    91  	// calculate the average
    92  	for _, aPeriod := range a.intervals {
    93  		if aPeriod == nil {
    94  			continue // nil average pointers is allowed
    95  		}
    96  		aPeriod.Aggregate(&count, &average)
    97  	}
    98  	if count > 0 {
    99  		average = average / float64(count)
   100  	}
   101  
   102  	return
   103  }
   104  
   105  // TODO 230726 Delete
   106  // func (a *Averager[T]) Index(t ...time.Time) (index PeriodIndex) {
   107  // 	return a.period.Index(t...)
   108  // }
   109  
   110  // getCurrent ensures an interval for the current period exists and returns it
   111  //   - interval is nil if periodIndex is too far in the past
   112  //   - the slice is updated to include periodIndex if it is after the last entry
   113  func (a *Averager[T]) getCurrent(periodIndex PeriodIndex) (interval *AverageInterval) {
   114  	if interval = a.getLastInterval(); interval != nil && interval.index == periodIndex {
   115  		return // last existing period is the right one return
   116  	}
   117  
   118  	// ensure the slice is up-to-date and
   119  	// return the requested period if it is not out of range
   120  	a.sliceLock.Lock()
   121  	defer a.sliceLock.Unlock()
   122  
   123  	// 1. determine at what period the slice should end
   124  
   125  	// the latest known period
   126  	//	- either the requested value or
   127  	//	- the last existing in slice
   128  	var lastPeriod = periodIndex
   129  	if interval != nil && interval.index > lastPeriod {
   130  		lastPeriod = interval.index
   131  	}
   132  	// firstInterval is the lowest allowed period number considering:
   133  	//	- maxCount and period0
   134  	var firstInterval = a.period.Sub(lastPeriod+1, a.maxCount)
   135  	// length is the number of entries in the slice
   136  	var length = len(a.intervals)
   137  
   138  	// 2. remove any stale slice entries at start of slice
   139  	if length > 0 {
   140  		var firstIndex PeriodIndex
   141  		if firstIndex = a.intervals[0].Index(); firstIndex < firstInterval {
   142  			pslices.TrimLeft(&a.intervals, a.period.Since(firstInterval, firstIndex))
   143  			length = len(a.intervals)
   144  		}
   145  	}
   146  	// numberOfIntervals is the length the slice should have to include periodIndex
   147  	var numberOfIntervals = a.period.Available(periodIndex, a.maxCount)
   148  
   149  	// 3. extend the slice as necessary
   150  	if length < numberOfIntervals {
   151  		pslices.SetLength(&a.intervals, numberOfIntervals)
   152  		length = numberOfIntervals
   153  	}
   154  
   155  	// ensure first interval exists
   156  	if a.intervals[0] == nil {
   157  		a.intervals[0] = NewAverageInterval(firstInterval)
   158  	}
   159  
   160  	// ensure last interval exists
   161  	var lastIndex = length - 1
   162  	if interval = a.intervals[lastIndex]; interval == nil {
   163  		interval = NewAverageInterval(periodIndex)
   164  		a.intervals[lastIndex] = interval
   165  	}
   166  
   167  	if periodIndex < firstInterval {
   168  		interval = nil
   169  		return // periodIndex too far in the past return
   170  	}
   171  
   172  	interval = a.intervals[periodIndex-firstInterval]
   173  
   174  	return
   175  }
   176  
   177  // getLastInterval returns the last period or nil if no period exists
   178  //   - the last period provide the current end of the value series
   179  //   - if no recent datapoints were provided,
   180  //     the last period may be stale
   181  func (a *Averager[T]) getLastInterval() (interval *AverageInterval) {
   182  	a.sliceLock.RLock()
   183  	defer a.sliceLock.RUnlock()
   184  
   185  	if length := len(a.intervals); length > 0 {
   186  		interval = a.intervals[length-1]
   187  	}
   188  
   189  	return
   190  }