2024-05-31 17:36:16 +01:00
|
|
|
package stats
|
|
|
|
|
|
|
|
|
|
import (
|
2024-06-02 13:37:29 +01:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2024-05-31 17:36:16 +01:00
|
|
|
"math"
|
|
|
|
|
"slices"
|
|
|
|
|
|
|
|
|
|
"github.com/ncruces/go-sqlite3"
|
|
|
|
|
"github.com/ncruces/go-sqlite3/internal/util"
|
|
|
|
|
"github.com/ncruces/sort/quick"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
median = iota
|
2024-06-06 19:52:49 +01:00
|
|
|
percentile_cont
|
|
|
|
|
percentile_disc
|
2024-05-31 17:36:16 +01:00
|
|
|
)
|
|
|
|
|
|
2024-06-06 19:52:49 +01:00
|
|
|
func newPercentile(kind int) func() sqlite3.AggregateFunction {
|
|
|
|
|
return func() sqlite3.AggregateFunction { return &percentile{kind: kind} }
|
2024-05-31 17:36:16 +01:00
|
|
|
}
|
|
|
|
|
|
2024-06-06 19:52:49 +01:00
|
|
|
type percentile struct {
|
2024-06-02 13:37:29 +01:00
|
|
|
nums []float64
|
|
|
|
|
arg1 []byte
|
2024-06-06 00:09:14 +01:00
|
|
|
kind int
|
2024-05-31 17:36:16 +01:00
|
|
|
}
|
|
|
|
|
|
2024-06-06 19:52:49 +01:00
|
|
|
func (q *percentile) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
2024-05-31 17:36:16 +01:00
|
|
|
if a := arg[0]; a.NumericType() != sqlite3.NULL {
|
2024-06-02 13:37:29 +01:00
|
|
|
q.nums = append(q.nums, a.Float())
|
2024-05-31 17:36:16 +01:00
|
|
|
}
|
|
|
|
|
if q.kind != median {
|
2024-08-27 01:45:44 +01:00
|
|
|
q.arg1 = append(q.arg1[:0], arg[1].RawText()...)
|
2024-05-31 17:36:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-06 19:52:49 +01:00
|
|
|
func (q *percentile) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
|
|
|
|
|
// Implementing inverse allows certain queries that don't really need it to succeed.
|
|
|
|
|
ctx.ResultError(util.ErrorString("percentile: may not be used as a window function"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (q *percentile) Value(ctx sqlite3.Context) {
|
2024-06-02 13:37:29 +01:00
|
|
|
if len(q.nums) == 0 {
|
2024-05-31 17:36:16 +01:00
|
|
|
return
|
|
|
|
|
}
|
2024-06-02 13:37:29 +01:00
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
err error
|
|
|
|
|
float float64
|
|
|
|
|
floats []float64
|
|
|
|
|
)
|
2024-05-31 17:36:16 +01:00
|
|
|
if q.kind == median {
|
2024-06-06 19:52:49 +01:00
|
|
|
float, err = getPercentile(q.nums, 0.5, false)
|
2024-06-02 13:37:29 +01:00
|
|
|
ctx.ResultFloat(float)
|
|
|
|
|
} else if err = json.Unmarshal(q.arg1, &float); err == nil {
|
2024-06-06 19:52:49 +01:00
|
|
|
float, err = getPercentile(q.nums, float, q.kind == percentile_disc)
|
2024-06-02 13:37:29 +01:00
|
|
|
ctx.ResultFloat(float)
|
|
|
|
|
} else if err = json.Unmarshal(q.arg1, &floats); err == nil {
|
2024-06-06 19:52:49 +01:00
|
|
|
err = getPercentiles(q.nums, floats, q.kind == percentile_disc)
|
2024-06-02 13:37:29 +01:00
|
|
|
ctx.ResultJSON(floats)
|
2024-05-31 17:36:16 +01:00
|
|
|
}
|
2024-06-02 13:37:29 +01:00
|
|
|
if err != nil {
|
2024-07-26 13:29:24 +01:00
|
|
|
ctx.ResultError(fmt.Errorf("percentile: %w", err)) // notest
|
2024-05-31 17:36:16 +01:00
|
|
|
}
|
2024-06-02 13:37:29 +01:00
|
|
|
}
|
2024-05-31 17:36:16 +01:00
|
|
|
|
2024-06-06 19:52:49 +01:00
|
|
|
func getPercentile(nums []float64, pos float64, disc bool) (float64, error) {
|
2024-06-02 13:37:29 +01:00
|
|
|
if pos < 0 || pos > 1 {
|
|
|
|
|
return 0, util.ErrorString("invalid pos")
|
|
|
|
|
}
|
2024-05-31 17:36:16 +01:00
|
|
|
|
2024-06-02 13:37:29 +01:00
|
|
|
i, f := math.Modf(pos * float64(len(nums)-1))
|
|
|
|
|
m0 := quick.Select(nums, int(i))
|
|
|
|
|
|
|
|
|
|
if f == 0 || disc {
|
|
|
|
|
return m0, nil
|
2024-05-31 17:36:16 +01:00
|
|
|
}
|
|
|
|
|
|
2024-06-02 13:37:29 +01:00
|
|
|
m1 := slices.Min(nums[int(i)+1:])
|
|
|
|
|
return math.FMA(f, m1, -math.FMA(f, m0, -m0)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-06 19:52:49 +01:00
|
|
|
func getPercentiles(nums []float64, pos []float64, disc bool) error {
|
2024-06-02 13:37:29 +01:00
|
|
|
for i := range pos {
|
2024-06-06 19:52:49 +01:00
|
|
|
v, err := getPercentile(nums, pos[i], disc)
|
2024-06-02 13:37:29 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
pos[i] = v
|
|
|
|
|
}
|
|
|
|
|
return nil
|
2024-05-31 17:36:16 +01:00
|
|
|
}
|