mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-01 03:12:04 +08:00
chore: port functions support
This commit is contained in:
parent
c08d1bccaf
commit
2e9c66abdd
@ -207,7 +207,7 @@ type SecondaryAggregation struct {
|
|||||||
|
|
||||||
type Function struct {
|
type Function struct {
|
||||||
// name of the function
|
// name of the function
|
||||||
Name string `json:"name"`
|
Name FunctionName `json:"name"`
|
||||||
|
|
||||||
// args is the arguments to the function
|
// args is the arguments to the function
|
||||||
Args []struct {
|
Args []struct {
|
||||||
|
@ -1,27 +1,489 @@
|
|||||||
package querybuildertypesv5
|
package querybuildertypesv5
|
||||||
|
|
||||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
import (
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
type FunctionName struct {
|
type FunctionName struct {
|
||||||
valuer.String
|
valuer.String
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
FunctionNameCutOffMin = FunctionName{valuer.NewString("cutOffMin")}
|
FunctionNameCutOffMin = FunctionName{valuer.NewString("cutoff_min")}
|
||||||
FunctionNameCutOffMax = FunctionName{valuer.NewString("cutOffMax")}
|
FunctionNameCutOffMax = FunctionName{valuer.NewString("cutoff_max")}
|
||||||
FunctionNameClampMin = FunctionName{valuer.NewString("clampMin")}
|
FunctionNameClampMin = FunctionName{valuer.NewString("clamp_min")}
|
||||||
FunctionNameClampMax = FunctionName{valuer.NewString("clampMax")}
|
FunctionNameClampMax = FunctionName{valuer.NewString("clamp_max")}
|
||||||
FunctionNameAbsolute = FunctionName{valuer.NewString("absolute")}
|
FunctionNameAbsolute = FunctionName{valuer.NewString("absolute")}
|
||||||
FunctionNameRunningDiff = FunctionName{valuer.NewString("runningDiff")}
|
FunctionNameRunningDiff = FunctionName{valuer.NewString("running_diff")}
|
||||||
FunctionNameLog2 = FunctionName{valuer.NewString("log2")}
|
FunctionNameLog2 = FunctionName{valuer.NewString("log2")}
|
||||||
FunctionNameLog10 = FunctionName{valuer.NewString("log10")}
|
FunctionNameLog10 = FunctionName{valuer.NewString("log10")}
|
||||||
FunctionNameCumSum = FunctionName{valuer.NewString("cumSum")}
|
FunctionNameCumulativeSum = FunctionName{valuer.NewString("cumulative_sum")}
|
||||||
FunctionNameEWMA3 = FunctionName{valuer.NewString("ewma3")}
|
FunctionNameEWMA3 = FunctionName{valuer.NewString("ewma3")}
|
||||||
FunctionNameEWMA5 = FunctionName{valuer.NewString("ewma5")}
|
FunctionNameEWMA5 = FunctionName{valuer.NewString("ewma5")}
|
||||||
FunctionNameEWMA7 = FunctionName{valuer.NewString("ewma7")}
|
FunctionNameEWMA7 = FunctionName{valuer.NewString("ewma7")}
|
||||||
FunctionNameMedian3 = FunctionName{valuer.NewString("median3")}
|
FunctionNameMedian3 = FunctionName{valuer.NewString("median3")}
|
||||||
FunctionNameMedian5 = FunctionName{valuer.NewString("median5")}
|
FunctionNameMedian5 = FunctionName{valuer.NewString("median5")}
|
||||||
FunctionNameMedian7 = FunctionName{valuer.NewString("median7")}
|
FunctionNameMedian7 = FunctionName{valuer.NewString("median7")}
|
||||||
FunctionNameTimeShift = FunctionName{valuer.NewString("timeShift")}
|
FunctionNameTimeShift = FunctionName{valuer.NewString("time_shift")}
|
||||||
FunctionNameAnomaly = FunctionName{valuer.NewString("anomaly")}
|
FunctionNameAnomaly = FunctionName{valuer.NewString("anomaly")}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ApplyFunction applies the given function to the result data
|
||||||
|
func ApplyFunction(fn Function, result *Result) *Result {
|
||||||
|
// Extract the function name and arguments
|
||||||
|
name := fn.Name
|
||||||
|
args := fn.Args
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case FunctionNameCutOffMin, FunctionNameCutOffMax, FunctionNameClampMin, FunctionNameClampMax:
|
||||||
|
if len(args) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
threshold, err := parseFloat64Arg(args[0].Value)
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case FunctionNameCutOffMin:
|
||||||
|
return funcCutOffMin(result, threshold)
|
||||||
|
case FunctionNameCutOffMax:
|
||||||
|
return funcCutOffMax(result, threshold)
|
||||||
|
case FunctionNameClampMin:
|
||||||
|
return funcClampMin(result, threshold)
|
||||||
|
case FunctionNameClampMax:
|
||||||
|
return funcClampMax(result, threshold)
|
||||||
|
}
|
||||||
|
case FunctionNameAbsolute:
|
||||||
|
return funcAbsolute(result)
|
||||||
|
case FunctionNameRunningDiff:
|
||||||
|
return funcRunningDiff(result)
|
||||||
|
case FunctionNameLog2:
|
||||||
|
return funcLog2(result)
|
||||||
|
case FunctionNameLog10:
|
||||||
|
return funcLog10(result)
|
||||||
|
case FunctionNameCumulativeSum:
|
||||||
|
return funcCumulativeSum(result)
|
||||||
|
case FunctionNameEWMA3, FunctionNameEWMA5, FunctionNameEWMA7:
|
||||||
|
alpha := getEWMAAlpha(name, args)
|
||||||
|
return funcEWMA(result, alpha)
|
||||||
|
case FunctionNameMedian3:
|
||||||
|
return funcMedian3(result)
|
||||||
|
case FunctionNameMedian5:
|
||||||
|
return funcMedian5(result)
|
||||||
|
case FunctionNameMedian7:
|
||||||
|
return funcMedian7(result)
|
||||||
|
case FunctionNameTimeShift:
|
||||||
|
if len(args) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
shift, err := parseFloat64Arg(args[0].Value)
|
||||||
|
if err != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return funcTimeShift(result, shift)
|
||||||
|
case FunctionNameAnomaly:
|
||||||
|
// Placeholder for anomaly detection - would need more sophisticated implementation
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFloat64Arg parses a string argument to float64
|
||||||
|
func parseFloat64Arg(value string) (float64, error) {
|
||||||
|
return strconv.ParseFloat(value, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEWMAAlpha calculates the alpha value for EWMA functions
|
||||||
|
func getEWMAAlpha(name FunctionName, args []struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}) float64 {
|
||||||
|
// Try to get alpha from arguments first
|
||||||
|
if len(args) > 0 {
|
||||||
|
if alpha, err := parseFloat64Arg(args[0].Value); err == nil {
|
||||||
|
return alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default alpha values: alpha = 2 / (n + 1) where n is the window size
|
||||||
|
switch name {
|
||||||
|
case FunctionNameEWMA3:
|
||||||
|
return 0.5 // 2 / (3 + 1)
|
||||||
|
case FunctionNameEWMA5:
|
||||||
|
return 1.0 / 3.0 // 2 / (5 + 1)
|
||||||
|
case FunctionNameEWMA7:
|
||||||
|
return 0.25 // 2 / (7 + 1)
|
||||||
|
}
|
||||||
|
return 0.5 // default
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcCutOffMin cuts off values below the threshold and replaces them with NaN
|
||||||
|
func funcCutOffMin(result *Result, threshold float64) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
if point.Value < threshold {
|
||||||
|
point.Value = math.NaN()
|
||||||
|
}
|
||||||
|
series.Values[idx] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcCutOffMax cuts off values above the threshold and replaces them with NaN
|
||||||
|
func funcCutOffMax(result *Result, threshold float64) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
if point.Value > threshold {
|
||||||
|
point.Value = math.NaN()
|
||||||
|
}
|
||||||
|
series.Values[idx] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcClampMin cuts off values below the threshold and replaces them with the threshold
|
||||||
|
func funcClampMin(result *Result, threshold float64) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
if point.Value < threshold {
|
||||||
|
point.Value = threshold
|
||||||
|
}
|
||||||
|
series.Values[idx] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcClampMax cuts off values above the threshold and replaces them with the threshold
|
||||||
|
func funcClampMax(result *Result, threshold float64) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
if point.Value > threshold {
|
||||||
|
point.Value = threshold
|
||||||
|
}
|
||||||
|
series.Values[idx] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcAbsolute returns the absolute value of each point
|
||||||
|
func funcAbsolute(result *Result) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
point.Value = math.Abs(point.Value)
|
||||||
|
series.Values[idx] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcRunningDiff returns the running difference of each point
|
||||||
|
func funcRunningDiff(result *Result) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
// iterate over the points in reverse order
|
||||||
|
for idx := len(series.Values) - 1; idx >= 0; idx-- {
|
||||||
|
if idx > 0 {
|
||||||
|
series.Values[idx].Value = series.Values[idx].Value - series.Values[idx-1].Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove the first point
|
||||||
|
if len(series.Values) > 0 {
|
||||||
|
series.Values = series.Values[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcLog2 returns the log2 of each point
|
||||||
|
func funcLog2(result *Result) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
point.Value = math.Log2(point.Value)
|
||||||
|
series.Values[idx] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcLog10 returns the log10 of each point
|
||||||
|
func funcLog10(result *Result) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
point.Value = math.Log10(point.Value)
|
||||||
|
series.Values[idx] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcCumulativeSum returns the cumulative sum for each point in a series
|
||||||
|
func funcCumulativeSum(result *Result) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
var sum float64
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
if !math.IsNaN(point.Value) {
|
||||||
|
sum += point.Value
|
||||||
|
}
|
||||||
|
point.Value = sum
|
||||||
|
series.Values[idx] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcEWMA calculates the Exponentially Weighted Moving Average
|
||||||
|
func funcEWMA(result *Result, alpha float64) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
var ewma float64
|
||||||
|
var initialized bool
|
||||||
|
|
||||||
|
for i, point := range series.Values {
|
||||||
|
if !initialized {
|
||||||
|
if !math.IsNaN(point.Value) {
|
||||||
|
// Initialize EWMA with the first non-NaN value
|
||||||
|
ewma = point.Value
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
// Continue until the EWMA is initialized
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !math.IsNaN(point.Value) {
|
||||||
|
// Update EWMA with the current value
|
||||||
|
ewma = alpha*point.Value + (1-alpha)*ewma
|
||||||
|
}
|
||||||
|
// Set the EWMA value for the current point
|
||||||
|
series.Values[i].Value = ewma
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcMedian3 returns the median of 3 points for each point in a series
|
||||||
|
func funcMedian3(result *Result) *Result {
|
||||||
|
return funcMedianN(result, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcMedian5 returns the median of 5 points for each point in a series
|
||||||
|
func funcMedian5(result *Result) *Result {
|
||||||
|
return funcMedianN(result, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcMedian7 returns the median of 7 points for each point in a series
|
||||||
|
func funcMedian7(result *Result) *Result {
|
||||||
|
return funcMedianN(result, 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcMedianN returns the median of N points for each point in a series
|
||||||
|
func funcMedianN(result *Result, n int) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
halfWindow := n / 2
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
medianValues := make([]*TimeSeriesValue, 0)
|
||||||
|
|
||||||
|
for i := halfWindow; i < len(series.Values)-halfWindow; i++ {
|
||||||
|
values := make([]float64, 0, n)
|
||||||
|
|
||||||
|
// Add non-NaN values to the slice
|
||||||
|
for j := -halfWindow; j <= halfWindow; j++ {
|
||||||
|
if !math.IsNaN(series.Values[i+j].Value) {
|
||||||
|
values = append(values, series.Values[i+j].Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new point with median value
|
||||||
|
newPoint := &TimeSeriesValue{
|
||||||
|
Timestamp: series.Values[i].Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the case where there are not enough values to calculate a median
|
||||||
|
if len(values) == 0 {
|
||||||
|
newPoint.Value = math.NaN()
|
||||||
|
} else {
|
||||||
|
newPoint.Value = median(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
medianValues = append(medianValues, newPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the series values with median values
|
||||||
|
// Keep the original edge points unchanged
|
||||||
|
for i := halfWindow; i < len(series.Values)-halfWindow; i++ {
|
||||||
|
series.Values[i] = medianValues[i-halfWindow]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// median calculates the median of a slice of float64 values
|
||||||
|
func median(values []float64) float64 {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return math.NaN()
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Float64s(values)
|
||||||
|
medianIndex := len(values) / 2
|
||||||
|
if len(values)%2 == 0 {
|
||||||
|
return (values[medianIndex-1] + values[medianIndex]) / 2
|
||||||
|
}
|
||||||
|
return values[medianIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcTimeShift shifts all timestamps by the given amount (in seconds)
|
||||||
|
func funcTimeShift(result *Result, shift float64) *Result {
|
||||||
|
if result.Type != RequestTypeTimeSeries {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
shiftMs := int64(shift * 1000) // Convert seconds to milliseconds
|
||||||
|
|
||||||
|
for _, aggregation := range timeSeriesData.Aggregations {
|
||||||
|
for _, series := range aggregation.Series {
|
||||||
|
for idx, point := range series.Values {
|
||||||
|
series.Values[idx].Timestamp = point.Timestamp + shiftMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyFunctions applies a list of functions sequentially to the result
|
||||||
|
func ApplyFunctions(functions []Function, result *Result) *Result {
|
||||||
|
for _, fn := range functions {
|
||||||
|
result = ApplyFunction(fn, result)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -0,0 +1,712 @@
|
|||||||
|
package querybuildertypesv5
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to create test time series data
|
||||||
|
func createTestTimeSeriesData(values []float64) *Result {
|
||||||
|
timeSeriesValues := make([]*TimeSeriesValue, len(values))
|
||||||
|
for i, val := range values {
|
||||||
|
timeSeriesValues[i] = &TimeSeriesValue{
|
||||||
|
Timestamp: int64(i + 1),
|
||||||
|
Value: val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
series := &TimeSeries{
|
||||||
|
Values: timeSeriesValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregation := &AggregationBucket{
|
||||||
|
Index: 0,
|
||||||
|
Alias: "test",
|
||||||
|
Series: []*TimeSeries{series},
|
||||||
|
}
|
||||||
|
|
||||||
|
timeSeriesData := &TimeSeriesData{
|
||||||
|
QueryName: "test",
|
||||||
|
Aggregations: []*AggregationBucket{aggregation},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Result{
|
||||||
|
Type: RequestTypeTimeSeries,
|
||||||
|
Value: timeSeriesData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract values from result for comparison
|
||||||
|
func extractValues(result *Result) []float64 {
|
||||||
|
timeSeriesData, ok := result.Value.(*TimeSeriesData)
|
||||||
|
if !ok || len(timeSeriesData.Aggregations) == 0 || len(timeSeriesData.Aggregations[0].Series) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
series := timeSeriesData.Aggregations[0].Series[0]
|
||||||
|
values := make([]float64, len(series.Values))
|
||||||
|
for i, point := range series.Values {
|
||||||
|
values[i] = point.Value
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncCutOffMin(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
threshold float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcCutOffMin",
|
||||||
|
values: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
threshold: 0.3,
|
||||||
|
want: []float64{0.5, 0.4, 0.3, math.NaN(), math.NaN()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test funcCutOffMin with threshold 0",
|
||||||
|
values: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
threshold: 0,
|
||||||
|
want: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcCutOffMin(result, tt.threshold)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcCutOffMin() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if math.IsNaN(tt.want[i]) {
|
||||||
|
if !math.IsNaN(got[i]) {
|
||||||
|
t.Errorf("funcCutOffMin() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcCutOffMin() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncCutOffMax(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
threshold float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcCutOffMax",
|
||||||
|
values: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
threshold: 0.3,
|
||||||
|
want: []float64{math.NaN(), math.NaN(), 0.3, 0.2, 0.1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test funcCutOffMax with threshold 0",
|
||||||
|
values: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
threshold: 0,
|
||||||
|
want: []float64{math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN()},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcCutOffMax(result, tt.threshold)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcCutOffMax() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if math.IsNaN(tt.want[i]) {
|
||||||
|
if !math.IsNaN(got[i]) {
|
||||||
|
t.Errorf("funcCutOffMax() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcCutOffMax() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCutOffMinCumSum(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
threshold float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcCutOffMin followed by funcCumulativeSum",
|
||||||
|
values: []float64{0.5, 0.2, 0.1, 0.4, 0.3},
|
||||||
|
threshold: 0.3,
|
||||||
|
want: []float64{0.5, 0.5, 0.5, 0.9, 1.2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcCutOffMin(result, tt.threshold)
|
||||||
|
newResult = funcCumulativeSum(newResult)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("CutOffMin+CumSum got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if math.IsNaN(tt.want[i]) {
|
||||||
|
if !math.IsNaN(got[i]) {
|
||||||
|
t.Errorf("CutOffMin+CumSum at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("CutOffMin+CumSum at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncMedian3(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Values",
|
||||||
|
values: []float64{5, 3, 8, 2, 7},
|
||||||
|
want: []float64{5, 5, 3, 7, 7}, // edge values unchanged, middle values are median of 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NaNHandling",
|
||||||
|
values: []float64{math.NaN(), 3, math.NaN(), 7, 9},
|
||||||
|
want: []float64{math.NaN(), 3, 5, 8, 9}, // median of available values
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UniformValues",
|
||||||
|
values: []float64{7, 7, 7, 7, 7},
|
||||||
|
want: []float64{7, 7, 7, 7, 7},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleValueSeries",
|
||||||
|
values: []float64{9},
|
||||||
|
want: []float64{9},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptySeries",
|
||||||
|
values: []float64{},
|
||||||
|
want: []float64{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
got := funcMedian3(result)
|
||||||
|
gotValues := extractValues(got)
|
||||||
|
|
||||||
|
if len(gotValues) != len(tt.want) {
|
||||||
|
t.Errorf("funcMedian3() got length %d, want length %d", len(gotValues), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range gotValues {
|
||||||
|
if math.IsNaN(tt.want[i]) {
|
||||||
|
if !math.IsNaN(gotValues[i]) {
|
||||||
|
t.Errorf("funcMedian3() at index %d = %v, want %v", i, gotValues[i], tt.want[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if gotValues[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcMedian3() at index %d = %v, want %v", i, gotValues[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncMedian5(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Values",
|
||||||
|
values: []float64{5, 3, 8, 2, 7, 9, 1, 4, 6, 10},
|
||||||
|
want: []float64{5, 3, 5, 7, 7, 4, 6, 6, 6, 10}, // edge values unchanged
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NaNHandling",
|
||||||
|
values: []float64{math.NaN(), 3, math.NaN(), 7, 9, 1, 4, 6, 10, 2},
|
||||||
|
want: []float64{math.NaN(), 3, 7, 5, 5.5, 6, 6, 4, 10, 2}, // median of available values
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UniformValues",
|
||||||
|
values: []float64{7, 7, 7, 7, 7},
|
||||||
|
want: []float64{7, 7, 7, 7, 7},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleValueSeries",
|
||||||
|
values: []float64{9},
|
||||||
|
want: []float64{9},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EmptySeries",
|
||||||
|
values: []float64{},
|
||||||
|
want: []float64{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
got := funcMedian5(result)
|
||||||
|
gotValues := extractValues(got)
|
||||||
|
|
||||||
|
if len(gotValues) != len(tt.want) {
|
||||||
|
t.Errorf("funcMedian5() got length %d, want length %d", len(gotValues), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range gotValues {
|
||||||
|
if math.IsNaN(tt.want[i]) {
|
||||||
|
if !math.IsNaN(gotValues[i]) {
|
||||||
|
t.Errorf("funcMedian5() at index %d = %v, want %v", i, gotValues[i], tt.want[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if gotValues[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcMedian5() at index %d = %v, want %v", i, gotValues[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncRunningDiff(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcRunningDiff",
|
||||||
|
values: []float64{1, 2, 3},
|
||||||
|
want: []float64{1, 1}, // diff removes first element
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test funcRunningDiff with start number as 8",
|
||||||
|
values: []float64{8, 8, 8},
|
||||||
|
want: []float64{0, 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
got := funcRunningDiff(result)
|
||||||
|
gotValues := extractValues(got)
|
||||||
|
|
||||||
|
if len(gotValues) != len(tt.want) {
|
||||||
|
t.Errorf("funcRunningDiff() got length %d, want length %d", len(gotValues), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range gotValues {
|
||||||
|
if gotValues[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcRunningDiff() at index %d = %v, want %v", i, gotValues[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncClampMin(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
threshold float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcClampMin",
|
||||||
|
values: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
threshold: 0.3,
|
||||||
|
want: []float64{0.5, 0.4, 0.3, 0.3, 0.3},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test funcClampMin with threshold 0",
|
||||||
|
values: []float64{-0.5, -0.4, 0.3, 0.2, 0.1},
|
||||||
|
threshold: 0,
|
||||||
|
want: []float64{0, 0, 0.3, 0.2, 0.1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcClampMin(result, tt.threshold)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcClampMin() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcClampMin() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncClampMax(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
threshold float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcClampMax",
|
||||||
|
values: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
threshold: 0.3,
|
||||||
|
want: []float64{0.3, 0.3, 0.3, 0.2, 0.1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test funcClampMax with threshold 1.0",
|
||||||
|
values: []float64{2.5, 0.4, 1.3, 0.2, 0.1},
|
||||||
|
threshold: 1.0,
|
||||||
|
want: []float64{1.0, 0.4, 1.0, 0.2, 0.1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcClampMax(result, tt.threshold)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcClampMax() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcClampMax() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncAbsolute(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcAbsolute",
|
||||||
|
values: []float64{-0.5, 0.4, -0.3, 0.2, -0.1},
|
||||||
|
want: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test funcAbsolute with all positive",
|
||||||
|
values: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
want: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcAbsolute(result)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcAbsolute() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcAbsolute() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncLog2(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcLog2",
|
||||||
|
values: []float64{1, 2, 4, 8, 16},
|
||||||
|
want: []float64{0, 1, 2, 3, 4},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcLog2(result)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcLog2() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if math.Abs(got[i]-tt.want[i]) > 1e-10 {
|
||||||
|
t.Errorf("funcLog2() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncLog10(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcLog10",
|
||||||
|
values: []float64{1, 10, 100, 1000},
|
||||||
|
want: []float64{0, 1, 2, 3},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcLog10(result)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcLog10() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if math.Abs(got[i]-tt.want[i]) > 1e-10 {
|
||||||
|
t.Errorf("funcLog10() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncCumSum(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcCumSum",
|
||||||
|
values: []float64{1, 2, 3, 4, 5},
|
||||||
|
want: []float64{1, 3, 6, 10, 15},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test funcCumSum with NaN",
|
||||||
|
values: []float64{1, math.NaN(), 3, 4, 5},
|
||||||
|
want: []float64{1, 1, 4, 8, 13}, // NaN is ignored
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcCumulativeSum(result)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcCumSum() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcCumSum() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuncTimeShift(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
shift float64
|
||||||
|
want []int64 // expected timestamps
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test funcTimeShift positive",
|
||||||
|
values: []float64{1, 2, 3},
|
||||||
|
shift: 5.0, // 5 seconds
|
||||||
|
want: []int64{6000, 7000, 8000}, // original timestamps (1,2,3) + 5000ms
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test funcTimeShift negative",
|
||||||
|
values: []float64{1, 2, 3},
|
||||||
|
shift: -2.0, // -2 seconds
|
||||||
|
want: []int64{-1000, 0, 1000}, // original timestamps (1,2,3) - 2000ms
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := funcTimeShift(result, tt.shift)
|
||||||
|
|
||||||
|
timeSeriesData, ok := newResult.Value.(*TimeSeriesData)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("funcTimeShift() failed to get time series data")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
series := timeSeriesData.Aggregations[0].Series[0]
|
||||||
|
got := make([]int64, len(series.Values))
|
||||||
|
for i, point := range series.Values {
|
||||||
|
got[i] = point.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("funcTimeShift() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("funcTimeShift() at index %d timestamp = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyFunction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
function Function
|
||||||
|
values []float64
|
||||||
|
want []float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cutOffMin function",
|
||||||
|
function: Function{
|
||||||
|
Name: FunctionNameCutOffMin,
|
||||||
|
Args: []struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}{
|
||||||
|
{Value: "0.3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
values: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
want: []float64{0.5, 0.4, 0.3, math.NaN(), math.NaN()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "absolute function",
|
||||||
|
function: Function{
|
||||||
|
Name: FunctionNameAbsolute,
|
||||||
|
},
|
||||||
|
values: []float64{-0.5, 0.4, -0.3, 0.2, -0.1},
|
||||||
|
want: []float64{0.5, 0.4, 0.3, 0.2, 0.1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := createTestTimeSeriesData(tt.values)
|
||||||
|
newResult := ApplyFunction(tt.function, result)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(tt.want) {
|
||||||
|
t.Errorf("ApplyFunction() got length %d, want length %d", len(got), len(tt.want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if math.IsNaN(tt.want[i]) {
|
||||||
|
if !math.IsNaN(got[i]) {
|
||||||
|
t.Errorf("ApplyFunction() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if got[i] != tt.want[i] {
|
||||||
|
t.Errorf("ApplyFunction() at index %d = %v, want %v", i, got[i], tt.want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyFunctions(t *testing.T) {
|
||||||
|
functions := []Function{
|
||||||
|
{
|
||||||
|
Name: FunctionNameCutOffMin,
|
||||||
|
Args: []struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}{
|
||||||
|
{Value: "0.3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: FunctionNameCumulativeSum,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
values := []float64{0.5, 0.2, 0.1, 0.4, 0.3}
|
||||||
|
want := []float64{0.5, 0.5, 0.5, 0.9, 1.2}
|
||||||
|
|
||||||
|
result := createTestTimeSeriesData(values)
|
||||||
|
newResult := ApplyFunctions(functions, result)
|
||||||
|
got := extractValues(newResult)
|
||||||
|
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Errorf("ApplyFunctions() got length %d, want length %d", len(got), len(want))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Errorf("ApplyFunctions() at index %d = %v, want %v", i, got[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -249,7 +249,7 @@ func TestQueryRangeRequest_UnmarshalJSON(t *testing.T) {
|
|||||||
Name: "error_rate",
|
Name: "error_rate",
|
||||||
Expression: "A / B * 100",
|
Expression: "A / B * 100",
|
||||||
Functions: []Function{{
|
Functions: []Function{{
|
||||||
Name: "absolute",
|
Name: FunctionNameAbsolute,
|
||||||
Args: []struct {
|
Args: []struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user