mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SSE: Add is_nan() and other like functions and allow '_' in func names (#43555)
* SSE: Add is_nan() and allow '_' in func names is_null() infn(), is_inf(), is_number()
This commit is contained in:
parent
43c81ddd23
commit
f17fb76b5e
@ -101,13 +101,31 @@ While most functions exist in the own expression operations, the math operation
|
||||
|
||||
abs returns the absolute value of its argument which can be a number or a series. For example `abs(-1)` or `abs($A)`.
|
||||
|
||||
##### is_inf
|
||||
|
||||
is_inf takes a number or a series and returns `1` for `Inf` values (negative or positive) and `0` for other values. For example `is_inf($A)`.
|
||||
|
||||
> **Note:** If you need to specifically check for negative infinity for example, you can do a comparison like `$A == infn()`.
|
||||
|
||||
##### is_nan
|
||||
|
||||
is_nan takes a number or a series and returns `1` for `NaN` values and `0` for other values. For example `is_nan($A)`. This function exists because `NaN` is not equal to `NaN`.
|
||||
|
||||
##### is_null
|
||||
|
||||
is_nan takes a number or a series and returns `1` for `null` values and `0` for other values. For example `is_null($A)`.
|
||||
|
||||
##### is_number
|
||||
|
||||
is_number takes a number or a series and returns `1` for all real number values and `0` for other values (which are `null`, `Inf+`, `Inf-`, and `NaN`). For example `is_number($A)`.
|
||||
|
||||
##### log
|
||||
|
||||
Log returns the natural logarithm of of its argument which can be a number or a series. If the value is less than 0, NaN is returned. For example `log(-1)` or `log($A)`.
|
||||
|
||||
##### inf, nan, and null
|
||||
##### inf, infn, nan, and null
|
||||
|
||||
The inf, nan, and null functions all return a single value of the name. They primarily exist for testing. Example: `null()`. (Note: inf always returns positive infinity, should probably change this to take an argument so it can return negative infinity).
|
||||
The inf, infn, nan, and null functions all return a single value of the name. They primarily exist for testing. Example: `null()`.
|
||||
|
||||
### Reduce
|
||||
|
||||
|
@ -21,14 +21,38 @@ var builtins = map[string]parse.Func{
|
||||
Return: parse.TypeScalar,
|
||||
F: nan,
|
||||
},
|
||||
"is_nan": {
|
||||
Args: []parse.ReturnType{parse.TypeVariantSet},
|
||||
VariantReturn: true,
|
||||
F: isNaN,
|
||||
},
|
||||
"inf": {
|
||||
Return: parse.TypeScalar,
|
||||
F: inf,
|
||||
},
|
||||
"infn": {
|
||||
Return: parse.TypeScalar,
|
||||
F: infn,
|
||||
},
|
||||
"is_inf": {
|
||||
Args: []parse.ReturnType{parse.TypeVariantSet},
|
||||
VariantReturn: true,
|
||||
F: isInf,
|
||||
},
|
||||
"null": {
|
||||
Return: parse.TypeScalar,
|
||||
F: null,
|
||||
},
|
||||
"is_null": {
|
||||
Args: []parse.ReturnType{parse.TypeVariantSet},
|
||||
VariantReturn: true,
|
||||
F: isNull,
|
||||
},
|
||||
"is_number": {
|
||||
Args: []parse.ReturnType{parse.TypeVariantSet},
|
||||
VariantReturn: true,
|
||||
F: isNumber,
|
||||
},
|
||||
}
|
||||
|
||||
// abs returns the absolute value for each result in NumberSet, SeriesSet, or Scalar
|
||||
@ -57,6 +81,43 @@ func log(e *State, varSet Results) (Results, error) {
|
||||
return newRes, nil
|
||||
}
|
||||
|
||||
// isNaN returns 1 if the value for each result in NumberSet, SeriesSet, or Scalar is NaN, else 0.
|
||||
func isNaN(e *State, varSet Results) (Results, error) {
|
||||
newRes := Results{}
|
||||
for _, res := range varSet.Values {
|
||||
newVal, err := perFloat(e, res, func(f float64) float64 {
|
||||
if math.IsNaN(f) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
if err != nil {
|
||||
return newRes, err
|
||||
}
|
||||
newRes.Values = append(newRes.Values, newVal)
|
||||
}
|
||||
return newRes, nil
|
||||
}
|
||||
|
||||
// isInf returns 1 if the value for each result in NumberSet, SeriesSet, or Scalar is a
|
||||
// positive or negative Inf, else 0.
|
||||
func isInf(e *State, varSet Results) (Results, error) {
|
||||
newRes := Results{}
|
||||
for _, res := range varSet.Values {
|
||||
newVal, err := perFloat(e, res, func(f float64) float64 {
|
||||
if math.IsInf(f, 0) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
if err != nil {
|
||||
return newRes, err
|
||||
}
|
||||
newRes.Values = append(newRes.Values, newVal)
|
||||
}
|
||||
return newRes, nil
|
||||
}
|
||||
|
||||
// nan returns a scalar nan value
|
||||
func nan(e *State) Results {
|
||||
aNaN := math.NaN()
|
||||
@ -69,11 +130,59 @@ func inf(e *State) Results {
|
||||
return NewScalarResults(e.RefID, &aInf)
|
||||
}
|
||||
|
||||
// infn returns a scalar negative infinity value
|
||||
func infn(e *State) Results {
|
||||
aInf := math.Inf(-1)
|
||||
return NewScalarResults(e.RefID, &aInf)
|
||||
}
|
||||
|
||||
// null returns a null scalar value
|
||||
func null(e *State) Results {
|
||||
return NewScalarResults(e.RefID, nil)
|
||||
}
|
||||
|
||||
// isNull returns 1 if the value for each result in NumberSet, SeriesSet, or Scalar is null, else 0.
|
||||
func isNull(e *State, varSet Results) (Results, error) {
|
||||
newRes := Results{}
|
||||
for _, res := range varSet.Values {
|
||||
newVal, err := perNullableFloat(e, res, func(f *float64) *float64 {
|
||||
nF := float64(0)
|
||||
if f == nil {
|
||||
nF = 1
|
||||
}
|
||||
return &nF
|
||||
})
|
||||
if err != nil {
|
||||
return newRes, err
|
||||
}
|
||||
newRes.Values = append(newRes.Values, newVal)
|
||||
}
|
||||
return newRes, nil
|
||||
}
|
||||
|
||||
// isNumber returns 1 if the value for each result in NumberSet, SeriesSet, or Scalar is a real number, else 0.
|
||||
// Therefore 0 is returned if the value Inf+, Inf-, NaN, or Null.
|
||||
func isNumber(e *State, varSet Results) (Results, error) {
|
||||
newRes := Results{}
|
||||
for _, res := range varSet.Values {
|
||||
newVal, err := perNullableFloat(e, res, func(f *float64) *float64 {
|
||||
nF := float64(1)
|
||||
if f == nil || math.IsInf(*f, 0) || math.IsNaN(*f) {
|
||||
nF = 0
|
||||
}
|
||||
return &nF
|
||||
})
|
||||
if err != nil {
|
||||
return newRes, err
|
||||
}
|
||||
newRes.Values = append(newRes.Values, newVal)
|
||||
}
|
||||
return newRes, nil
|
||||
}
|
||||
|
||||
// perFloat passes the non-null value of a Scalar/Number or each value point of a Series to floatF.
|
||||
// The return Value type will be the same type provided to function, (e.g. a Series input returns a series).
|
||||
// If input values are null the function is not called and NaN is returned for each value.
|
||||
func perFloat(e *State, val Value, floatF func(x float64) float64) (Value, error) {
|
||||
var newVal Value
|
||||
switch val.Type() {
|
||||
@ -113,3 +222,34 @@ func perFloat(e *State, val Value, floatF func(x float64) float64) (Value, error
|
||||
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
// perNullableFloat is like perFloat, but takes and returns float pointers instead of floats.
|
||||
// This is for instead for functions that need specific null handling.
|
||||
// The input float pointer should not be modified in the floatF func.
|
||||
func perNullableFloat(e *State, val Value, floatF func(x *float64) *float64) (Value, error) {
|
||||
var newVal Value
|
||||
switch val.Type() {
|
||||
case parse.TypeNumberSet:
|
||||
n := NewNumber(e.RefID, val.GetLabels())
|
||||
f := val.(Number).GetFloat64Value()
|
||||
n.SetValue(floatF(f))
|
||||
newVal = n
|
||||
case parse.TypeScalar:
|
||||
f := val.(Scalar).GetFloat64Value()
|
||||
newVal = NewScalar(e.RefID, floatF(f))
|
||||
case parse.TypeSeriesSet:
|
||||
resSeries := val.(Series)
|
||||
newSeries := NewSeries(e.RefID, resSeries.GetLabels(), resSeries.Len())
|
||||
for i := 0; i < resSeries.Len(); i++ {
|
||||
t, f := resSeries.GetPoint(i)
|
||||
if err := newSeries.SetPoint(i, t, floatF(f)); err != nil {
|
||||
return newSeries, err
|
||||
}
|
||||
}
|
||||
newVal = newSeries
|
||||
default:
|
||||
// TODO: Should we deal with TypeString, TypeVariantSet?
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
package mathexp
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFunc(t *testing.T) {
|
||||
func TestAbsFunc(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
expr string
|
||||
vars Vars
|
||||
newErrIs assert.ErrorAssertionFunc
|
||||
execErrIs assert.ErrorAssertionFunc
|
||||
resultIs assert.ComparisonAssertionFunc
|
||||
newErrIs require.ErrorAssertionFunc
|
||||
execErrIs require.ErrorAssertionFunc
|
||||
resultIs require.ComparisonAssertionFunc
|
||||
results Results
|
||||
}{
|
||||
{
|
||||
@ -27,18 +28,18 @@ func TestFunc(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
newErrIs: assert.NoError,
|
||||
execErrIs: assert.NoError,
|
||||
resultIs: assert.Equal,
|
||||
newErrIs: require.NoError,
|
||||
execErrIs: require.NoError,
|
||||
resultIs: require.Equal,
|
||||
results: Results{[]Value{makeNumber("", nil, float64Pointer(7))}},
|
||||
},
|
||||
{
|
||||
name: "abs on scalar",
|
||||
expr: "abs(-1)",
|
||||
vars: Vars{},
|
||||
newErrIs: assert.NoError,
|
||||
execErrIs: assert.NoError,
|
||||
resultIs: assert.Equal,
|
||||
newErrIs: require.NoError,
|
||||
execErrIs: require.NoError,
|
||||
resultIs: require.Equal,
|
||||
results: Results{[]Value{NewScalar("", float64Pointer(1.0))}},
|
||||
},
|
||||
{
|
||||
@ -55,9 +56,9 @@ func TestFunc(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
newErrIs: assert.NoError,
|
||||
execErrIs: assert.NoError,
|
||||
resultIs: assert.Equal,
|
||||
newErrIs: require.NoError,
|
||||
execErrIs: require.NoError,
|
||||
resultIs: require.Equal,
|
||||
results: Results{
|
||||
[]Value{
|
||||
makeSeries("", nil, tp{
|
||||
@ -72,7 +73,7 @@ func TestFunc(t *testing.T) {
|
||||
name: "abs on string - should error",
|
||||
expr: `abs("hi")`,
|
||||
vars: Vars{},
|
||||
newErrIs: assert.Error,
|
||||
newErrIs: require.Error,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@ -87,3 +88,74 @@ func TestFunc(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNumberFunc(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
expr string
|
||||
vars Vars
|
||||
results Results
|
||||
}{
|
||||
{
|
||||
name: "is_number on number type with real number value",
|
||||
expr: "is_number($A)",
|
||||
vars: Vars{
|
||||
"A": Results{
|
||||
[]Value{
|
||||
makeNumber("", nil, float64Pointer(6)),
|
||||
},
|
||||
},
|
||||
},
|
||||
results: Results{[]Value{makeNumber("", nil, float64Pointer(1))}},
|
||||
},
|
||||
{
|
||||
name: "is_number on number type with null value",
|
||||
expr: "is_number($A)",
|
||||
vars: Vars{
|
||||
"A": Results{
|
||||
[]Value{
|
||||
makeNumber("", nil, nil),
|
||||
},
|
||||
},
|
||||
},
|
||||
results: Results{[]Value{makeNumber("", nil, float64Pointer(0))}},
|
||||
},
|
||||
{
|
||||
name: "is_number on on series",
|
||||
expr: "is_number($A)",
|
||||
vars: Vars{
|
||||
"A": Results{
|
||||
[]Value{
|
||||
makeSeries("", nil,
|
||||
tp{time.Unix(5, 0), float64Pointer(5)},
|
||||
tp{time.Unix(10, 0), nil},
|
||||
tp{time.Unix(15, 0), float64Pointer(math.NaN())},
|
||||
tp{time.Unix(20, 0), float64Pointer(math.Inf(-1))},
|
||||
tp{time.Unix(25, 0), float64Pointer(math.Inf(0))}),
|
||||
},
|
||||
},
|
||||
},
|
||||
results: Results{
|
||||
[]Value{
|
||||
makeSeries("", nil,
|
||||
tp{time.Unix(5, 0), float64Pointer(1)},
|
||||
tp{time.Unix(10, 0), float64Pointer(0)},
|
||||
tp{time.Unix(15, 0), float64Pointer(0)},
|
||||
tp{time.Unix(20, 0), float64Pointer(0)},
|
||||
tp{time.Unix(25, 0), float64Pointer(0)}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e, err := New(tt.expr)
|
||||
require.NoError(t, err)
|
||||
if e != nil {
|
||||
res, err := e.Execute("", tt.vars)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.results, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ func lexSymbol(l *lexer) stateFn {
|
||||
func lexFunc(l *lexer) stateFn {
|
||||
for {
|
||||
switch r := l.next(); {
|
||||
case unicode.IsLetter(r):
|
||||
case unicode.IsLetter(r) || r == '_':
|
||||
// absorb
|
||||
default:
|
||||
l.backup()
|
||||
|
@ -12,7 +12,7 @@ interface Props {
|
||||
const mathPlaceholder =
|
||||
'Math operations on one more queries, you reference the query by ${refId} ie. $A, $B, $C etc\n' +
|
||||
'Example: $A + $B\n' +
|
||||
'Available functions: abs(), log(), nan(), inf(), null()';
|
||||
'Available functions: abs(), log(), is_number(), is_inf(), is_nan(), is_null()';
|
||||
|
||||
export const Math: FC<Props> = ({ labelWidth, onChange, query }) => {
|
||||
const onExpressionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
|
Loading…
Reference in New Issue
Block a user