Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
81 lines
2.2 KiB
Go
81 lines
2.2 KiB
Go
package api
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// gapFactor is how many steps without data make a stretch "partial".
|
|
const gapFactor = 2
|
|
|
|
// slot is one sample time of the graph and the step of the tier it belongs to.
|
|
type slot struct {
|
|
ts int64
|
|
step int64
|
|
}
|
|
|
|
// gridOf lists every sample time of the plan, oldest first.
|
|
func gridOf(p *graphPlan) []slot {
|
|
var out []slot
|
|
for _, w := range p.tiers {
|
|
step := int64(w.step / time.Second)
|
|
for _, ts := range w.slots() {
|
|
out = append(out, slot{ts: ts, step: step})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// pointsOf lays vals over the grid; a slot without data is a null point.
|
|
func pointsOf(grid []slot, vals map[int64]float64) []Point {
|
|
out := make([]Point, len(grid))
|
|
for i, g := range grid {
|
|
out[i] = Point{TS: g.ts}
|
|
if v, ok := vals[g.ts]; ok {
|
|
x := v
|
|
out[i].V = &x
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// findGaps returns the stretches of [start, end] longer than gapFactor steps in
|
|
// which no slot has data. A stretch runs from the last slot with data (or the
|
|
// start) to the next one (or the end); its step is the larger of its two
|
|
// neighbours', so a join between tiers does not report a false gap.
|
|
func findGaps(grid []slot, has func(ts int64) bool, start, end int64) []TimeSpan {
|
|
if len(grid) == 0 {
|
|
return nil
|
|
}
|
|
var gaps []TimeSpan
|
|
prevTS, prevStep := start, grid[0].step
|
|
report := func(from, to, step int64) {
|
|
if to-from > gapFactor*step {
|
|
gaps = append(gaps, TimeSpan{From: time.Unix(from, 0).UTC(), To: time.Unix(to, 0).UTC()})
|
|
}
|
|
}
|
|
for _, g := range grid {
|
|
if !has(g.ts) {
|
|
continue
|
|
}
|
|
report(prevTS, g.ts, max(prevStep, g.step))
|
|
prevTS, prevStep = g.ts, g.step
|
|
}
|
|
report(prevTS, end, prevStep)
|
|
return gaps
|
|
}
|
|
|
|
// nearestRank is the p-quantile (0 < p <= 1) of vals by the nearest-rank
|
|
// method: the value at rank ceil(p*n) of the sorted list. It is exact, takes
|
|
// an actual sample and needs no interpolation convention. NaN for no values.
|
|
func nearestRank(vals []float64, p float64) float64 {
|
|
if len(vals) == 0 {
|
|
return math.NaN()
|
|
}
|
|
s := append([]float64(nil), vals...)
|
|
sort.Float64s(s)
|
|
rank := int(math.Ceil(p*float64(len(s)) - 1e-9)) // the epsilon keeps 0.95*20 from rounding up to 20
|
|
return s[min(max(rank, 1), len(s))-1]
|
|
}
|