mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Lens] Add conditional operations in Formula (#142325)
* ✨ Introduce new comparison functions * ✨ Introduce new comparison symbols into grammar * 🔧 Introduce new tinymath functions * ✨ Add comparison fn validation to formula * ♻️ Some type refactoring * ✏️ Fix wrong error message * ✅ Add more formula unit tests * ✅ Add more tests * ✅ Fix tsvb test * 🐛 Fix issue with divide by 0 * ✏️ Update testing command * ✏️ Add some more testing info * ✨ Improved grammar to handle edge cases * ✅ Improve comparison code + unit tests * ✅ Fix test * ✏️ Update documentation with latest functions * 👌 Integrate feedback * 👌 Integrate more feedback * 👌 Update doc * 🐛 Fix bug with function return type check * 🔥 remove duplicate test * [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' * Update x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/formula/util.ts * ✏️ Fixes formula * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
This commit is contained in:
parent
e5cebd80b1
commit
757ab767eb
67 changed files with 1401 additions and 224 deletions
|
@ -66,7 +66,10 @@ parse('1 + random()')
|
|||
|
||||
This package is rebuilt when running `yarn kbn bootstrap`, but can also be build directly
|
||||
using `yarn build` from the `packages/kbn-tinymath` directory.
|
||||
|
||||
### Running tests
|
||||
|
||||
To test `@kbn/tinymath` from Kibana, run `yarn run jest --watch packages/kbn-tinymath` from
|
||||
To test `@kbn/tinymath` from Kibana, run `node scripts/jest --config packages/kbn-tinymath/jest.config.js` from
|
||||
the top level of Kibana.
|
||||
|
||||
To test grammar changes it is required to run a build task before the test suite.
|
||||
|
|
|
@ -96,6 +96,143 @@ clamp(35, 10, [20, 30, 40, 50]) // returns [20, 30, 35, 35]
|
|||
clamp([1, 9], 3, [4, 5]) // returns [clamp([1, 3, 4]), clamp([9, 3, 5])] = [3, 5]
|
||||
```
|
||||
***
|
||||
## _eq(_ _a_, _b_ _)_
|
||||
Performs an equality comparison between two values.
|
||||
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| a | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
| b | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
|
||||
**Returns**: <code>boolean</code> - Returns true if `a` and `b` are equal, false otherwise. Returns an array with the equality comparison of each element if `a` is an array.
|
||||
**Throws**:
|
||||
|
||||
- `'Missing b value'` if `b` is not provided
|
||||
- `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
|
||||
**Example**
|
||||
```js
|
||||
eq(1, 1) // returns true
|
||||
eq(1, 2) // returns false
|
||||
eq([1, 2], 1) // returns [true, false]
|
||||
eq([1, 2], [1, 2]) // returns [true, true]
|
||||
```
|
||||
***
|
||||
## _gt(_ _a_, _b_ _)_
|
||||
Performs a greater than comparison between two values.
|
||||
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| a | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
| b | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
|
||||
**Returns**: <code>boolean</code> - Returns true if `a` is greater than `b`, false otherwise. Returns an array with the greater than comparison of each element if `a` is an array.
|
||||
**Throws**:
|
||||
|
||||
- `'Missing b value'` if `b` is not provided
|
||||
- `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
|
||||
**Example**
|
||||
```js
|
||||
gt(1, 1) // returns false
|
||||
gt(2, 1) // returns true
|
||||
gt([1, 2], 1) // returns [true, false]
|
||||
gt([1, 2], [2, 1]) // returns [false, true]
|
||||
```
|
||||
***
|
||||
## _gte(_ _a_, _b_ _)_
|
||||
Performs a greater than or equal comparison between two values.
|
||||
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| a | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
| b | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
|
||||
**Returns**: <code>boolean</code> - Returns true if `a` is greater than or equal to `b`, false otherwise. Returns an array with the greater than or equal comparison of each element if `a` is an array.
|
||||
**Throws**:
|
||||
|
||||
- `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
|
||||
**Example**
|
||||
```js
|
||||
gte(1, 1) // returns true
|
||||
gte(1, 2) // returns false
|
||||
gte([1, 2], 2) // returns [false, true]
|
||||
gte([1, 2], [1, 1]) // returns [true, true]
|
||||
```
|
||||
***
|
||||
## _ifelse(_ _cond_, _a_, _b_ _)_
|
||||
Evaluates the a conditional argument and returns one of the two values based on that.
|
||||
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| cond | <code>boolean</code> | a boolean value |
|
||||
| a | <code>any</code> \| <code>Array.<any></code> | a value or an array of any values |
|
||||
| b | <code>any</code> \| <code>Array.<any></code> | a value or an array of any values |
|
||||
|
||||
**Returns**: <code>any</code> \| <code>Array.<any></code> - if the value of cond is truthy, return `a`, otherwise return `b`.
|
||||
**Throws**:
|
||||
|
||||
- `'Condition clause is of the wrong type'` if the `cond` provided is not of boolean type
|
||||
- `'Missing a value'` if `a` is not provided
|
||||
- `'Missing b value'` if `b` is not provided
|
||||
|
||||
**Example**
|
||||
```js
|
||||
ifelse(5 > 6, 1, 0) // returns 0
|
||||
ifelse(1 == 1, [1, 2, 3], 5) // returns [1, 2, 3]
|
||||
ifelse(1 < 2, [1, 2, 3], [2, 3, 4]) // returns [1, 2, 3]
|
||||
```
|
||||
***
|
||||
## _lt(_ _a_, _b_ _)_
|
||||
Performs a lower than comparison between two values.
|
||||
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| a | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
| b | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
|
||||
**Returns**: <code>boolean</code> - Returns true if `a` is lower than `b`, false otherwise. Returns an array with the lower than comparison of each element if `a` is an array.
|
||||
**Throws**:
|
||||
|
||||
- `'Missing b value'` if `b` is not provided
|
||||
- `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
|
||||
**Example**
|
||||
```js
|
||||
lt(1, 1) // returns false
|
||||
lt(1, 2) // returns true
|
||||
lt([1, 2], 2) // returns [true, false]
|
||||
lt([1, 2], [1, 2]) // returns [false, false]
|
||||
```
|
||||
***
|
||||
## _lte(_ _a_, _b_ _)_
|
||||
Performs a lower than or equal comparison between two values.
|
||||
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| a | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
| b | <code>number</code> \| <code>Array.<number></code> | a number or an array of numbers |
|
||||
|
||||
**Returns**: <code>boolean</code> - Returns true if `a` is lower than or equal to `b`, false otherwise. Returns an array with the lower than or equal comparison of each element if `a` is an array.
|
||||
**Throws**:
|
||||
|
||||
- `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
|
||||
**Example**
|
||||
```js
|
||||
lte(1, 1) // returns true
|
||||
lte(1, 2) // returns true
|
||||
lte([1, 2], 2) // returns [true, true]
|
||||
lte([1, 2], [1, 1]) // returns [true, false]
|
||||
```
|
||||
***
|
||||
## _cos(_ _a_ _)_
|
||||
Calculates the the cosine of a number. For arrays, the function will be applied index-wise to each element.
|
||||
|
||||
|
|
|
@ -11,6 +11,32 @@
|
|||
max: location.end.offset
|
||||
}
|
||||
}
|
||||
|
||||
const symbolsToFn = {
|
||||
'+': 'add', '-': 'subtract',
|
||||
'*': 'multiply', '/': 'divide',
|
||||
'<': 'lt', '>': 'gt', '==': 'eq',
|
||||
'<=': 'lte', '>=': 'gte',
|
||||
}
|
||||
|
||||
// Shared function for AST operations
|
||||
function parseSymbol(left, rest){
|
||||
const topLevel = rest.reduce((acc, [name, right]) => ({
|
||||
type: 'function',
|
||||
name: symbolsToFn[name],
|
||||
args: [acc, right],
|
||||
}), left);
|
||||
if (typeof topLevel === 'object') {
|
||||
topLevel.location = simpleLocation(location());
|
||||
topLevel.text = text();
|
||||
}
|
||||
return topLevel;
|
||||
}
|
||||
|
||||
// op is always defined, while eq can be null for gt and lt cases
|
||||
function getComparisonSymbol([op, eq]){
|
||||
return symbolsToFn[op+(eq || '')];
|
||||
}
|
||||
}
|
||||
|
||||
start
|
||||
|
@ -70,45 +96,55 @@ Variable
|
|||
|
||||
// expressions
|
||||
|
||||
// An Expression can be of 3 different types:
|
||||
// * a Comparison operation, which can contain recursive MathOperations inside
|
||||
// * a MathOperation, which can contain other MathOperations, but not Comparison types
|
||||
// * an ExpressionGroup, which is a generic Grouping that contains also Comparison operations (i.e. ( 5 > 1))
|
||||
Expression
|
||||
= Comparison
|
||||
/ MathOperation
|
||||
/ ExpressionGroup
|
||||
|
||||
Comparison
|
||||
= _ left:MathOperation op:(('>' / '<')('=')? / '=''=') right:MathOperation _ {
|
||||
return {
|
||||
type: 'function',
|
||||
name: getComparisonSymbol(op),
|
||||
args: [left, right],
|
||||
location: simpleLocation(location()),
|
||||
text: text()
|
||||
};
|
||||
}
|
||||
|
||||
MathOperation
|
||||
= AddSubtract
|
||||
/ MultiplyDivide
|
||||
/ Factor
|
||||
|
||||
AddSubtract
|
||||
= _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)+ _ {
|
||||
const topLevel = rest.reduce((acc, curr) => ({
|
||||
type: 'function',
|
||||
name: curr[0] === '+' ? 'add' : 'subtract',
|
||||
args: [acc, curr[1]],
|
||||
}), left);
|
||||
if (typeof topLevel === 'object') {
|
||||
topLevel.location = simpleLocation(location());
|
||||
topLevel.text = text();
|
||||
}
|
||||
return topLevel;
|
||||
return parseSymbol(left, rest, {'+': 'add', '-': 'subtract'});
|
||||
}
|
||||
/ MultiplyDivide
|
||||
|
||||
MultiplyDivide
|
||||
= _ left:Factor rest:(('*' / '/') Factor)* _ {
|
||||
const topLevel = rest.reduce((acc, curr) => ({
|
||||
type: 'function',
|
||||
name: curr[0] === '*' ? 'multiply' : 'divide',
|
||||
args: [acc, curr[1]],
|
||||
}), left);
|
||||
if (typeof topLevel === 'object') {
|
||||
topLevel.location = simpleLocation(location());
|
||||
topLevel.text = text();
|
||||
}
|
||||
return topLevel;
|
||||
return parseSymbol(left, rest, {'*': 'multiply', '/': 'divide'});
|
||||
}
|
||||
/ Factor
|
||||
|
||||
Factor
|
||||
= Group
|
||||
/ Function
|
||||
/ Literal
|
||||
|
||||
// Because of the new Comparison syntax it is required a new Group type
|
||||
// the previous Group has been renamed into ExpressionGroup while
|
||||
// a new Group type has been defined to exclude the Comparison type from it
|
||||
Group
|
||||
= _ '(' _ expr:MathOperation _ ')' _ {
|
||||
return expr
|
||||
}
|
||||
|
||||
ExpressionGroup
|
||||
= _ '(' _ expr:Expression _ ')' _ {
|
||||
return expr
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
* abs([-1 , -2, 3, -4]) // returns [1, 2, 3, 4]
|
||||
*/
|
||||
|
||||
module.exports = { abs };
|
||||
|
||||
function abs(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => Math.abs(a));
|
||||
}
|
||||
return Math.abs(a);
|
||||
}
|
||||
|
||||
module.exports = { abs };
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* add([1, 2], 3, [4, 5], 6) // returns [(1 + 3 + 4 + 6), (2 + 3 + 5 + 6)] = [14, 16]
|
||||
*/
|
||||
|
||||
module.exports = { add };
|
||||
|
||||
function add(...args) {
|
||||
if (args.length === 1) {
|
||||
if (Array.isArray(args[0])) return args[0].reduce((result, current) => result + current);
|
||||
|
@ -35,3 +33,4 @@ function add(...args) {
|
|||
return result + current;
|
||||
});
|
||||
}
|
||||
module.exports = { add };
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
* cbrt([27, 64, 125]) // returns [3, 4, 5]
|
||||
*/
|
||||
|
||||
module.exports = { cbrt };
|
||||
|
||||
function cbrt(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => Math.cbrt(a));
|
||||
}
|
||||
return Math.cbrt(a);
|
||||
}
|
||||
|
||||
module.exports = { cbrt };
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
* ceil([1.1, 2.2, 3.3]) // returns [2, 3, 4]
|
||||
*/
|
||||
|
||||
module.exports = { ceil };
|
||||
|
||||
function ceil(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => Math.ceil(a));
|
||||
}
|
||||
return Math.ceil(a);
|
||||
}
|
||||
|
||||
module.exports = { ceil };
|
||||
|
|
|
@ -30,8 +30,6 @@ const findClamp = (a, min, max) => {
|
|||
* clamp([1, 9], 3, [4, 5]) // returns [clamp([1, 3, 4]), clamp([9, 3, 5])] = [3, 5]
|
||||
*/
|
||||
|
||||
module.exports = { clamp };
|
||||
|
||||
function clamp(a, min, max) {
|
||||
if (max === null)
|
||||
throw new Error("Missing maximum value. You may want to use the 'min' function instead");
|
||||
|
@ -73,3 +71,5 @@ function clamp(a, min, max) {
|
|||
|
||||
return findClamp(a, min, max);
|
||||
}
|
||||
|
||||
module.exports = { clamp };
|
||||
|
|
39
packages/kbn-tinymath/src/functions/comparison/eq.js
Normal file
39
packages/kbn-tinymath/src/functions/comparison/eq.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Performs an equality comparison between two values.
|
||||
* @param {number|number[]} a a number or an array of numbers
|
||||
* @param {number|number[]} b a number or an array of numbers
|
||||
* @return {boolean} Returns true if `a` and `b` are equal, false otherwise. Returns an array with the equality comparison of each element if `a` is an array.
|
||||
* @throws `'Missing b value'` if `b` is not provided
|
||||
* @throws `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
* @example
|
||||
* eq(1, 1) // returns true
|
||||
* eq(1, 2) // returns false
|
||||
* eq([1, 2], 1) // returns [true, false]
|
||||
* eq([1, 2], [1, 2]) // returns [true, true]
|
||||
*/
|
||||
|
||||
function eq(a, b) {
|
||||
if (b == null) {
|
||||
throw new Error('Missing b value');
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
if (!Array.isArray(b)) {
|
||||
return a.every((v) => v === b);
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
throw new Error('Array length mismatch');
|
||||
}
|
||||
return a.every((v, i) => v === b[i]);
|
||||
}
|
||||
|
||||
return a === b;
|
||||
}
|
||||
module.exports = { eq };
|
39
packages/kbn-tinymath/src/functions/comparison/gt.js
Normal file
39
packages/kbn-tinymath/src/functions/comparison/gt.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Performs a greater than comparison between two values.
|
||||
* @param {number|number[]} a a number or an array of numbers
|
||||
* @param {number|number[]} b a number or an array of numbers
|
||||
* @return {boolean} Returns true if `a` is greater than `b`, false otherwise. Returns an array with the greater than comparison of each element if `a` is an array.
|
||||
* @throws `'Missing b value'` if `b` is not provided
|
||||
* @throws `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
* @example
|
||||
* gt(1, 1) // returns false
|
||||
* gt(2, 1) // returns true
|
||||
* gt([1, 2], 1) // returns [true, false]
|
||||
* gt([1, 2], [2, 1]) // returns [false, true]
|
||||
*/
|
||||
|
||||
function gt(a, b) {
|
||||
if (b == null) {
|
||||
throw new Error('Missing b value');
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
if (!Array.isArray(b)) {
|
||||
return a.every((v) => v > b);
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
throw new Error('Array length mismatch');
|
||||
}
|
||||
return a.every((v, i) => v > b[i]);
|
||||
}
|
||||
|
||||
return a > b;
|
||||
}
|
||||
module.exports = { gt };
|
28
packages/kbn-tinymath/src/functions/comparison/gte.js
Normal file
28
packages/kbn-tinymath/src/functions/comparison/gte.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { eq } = require('./eq');
|
||||
const { gt } = require('./gt');
|
||||
|
||||
/**
|
||||
* Performs a greater than or equal comparison between two values.
|
||||
* @param {number|number[]} a a number or an array of numbers
|
||||
* @param {number|number[]} b a number or an array of numbers
|
||||
* @return {boolean} Returns true if `a` is greater than or equal to `b`, false otherwise. Returns an array with the greater than or equal comparison of each element if `a` is an array.
|
||||
* @throws `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
* @example
|
||||
* gte(1, 1) // returns true
|
||||
* gte(1, 2) // returns false
|
||||
* gte([1, 2], 2) // returns [false, true]
|
||||
* gte([1, 2], [1, 1]) // returns [true, true]
|
||||
*/
|
||||
|
||||
function gte(a, b) {
|
||||
return eq(a, b) || gt(a, b);
|
||||
}
|
||||
module.exports = { gte };
|
38
packages/kbn-tinymath/src/functions/comparison/ifelse.js
Normal file
38
packages/kbn-tinymath/src/functions/comparison/ifelse.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Evaluates the a conditional argument and returns one of the two values based on that.
|
||||
* @param {(boolean)} cond a boolean value
|
||||
* @param {(any|any[])} a a value or an array of any values
|
||||
* @param {(any|any[])} b a value or an array of any values
|
||||
* @return {(any|any[])} if the value of cond is truthy, return `a`, otherwise return `b`.
|
||||
* @throws `'Condition clause is of the wrong type'` if the `cond` provided is not of boolean type
|
||||
* @throws `'Missing a value'` if `a` is not provided
|
||||
* @throws `'Missing b value'` if `b` is not provided
|
||||
* @example
|
||||
* ifelse(5 > 6, 1, 0) // returns 0
|
||||
* ifelse(1 == 1, [1, 2, 3], 5) // returns [1, 2, 3]
|
||||
* ifelse(1 < 2, [1, 2, 3], [2, 3, 4]) // returns [1, 2, 3]
|
||||
*/
|
||||
|
||||
function ifelse(cond, a, b) {
|
||||
if (typeof cond !== 'boolean') {
|
||||
throw Error('Condition clause is of the wrong type');
|
||||
}
|
||||
if (a == null) {
|
||||
throw new Error('Missing a value');
|
||||
}
|
||||
if (b == null) {
|
||||
throw new Error('Missing b value');
|
||||
}
|
||||
return cond ? a : b;
|
||||
}
|
||||
|
||||
ifelse.skipNumberValidation = true;
|
||||
module.exports = { ifelse };
|
16
packages/kbn-tinymath/src/functions/comparison/index.js
Normal file
16
packages/kbn-tinymath/src/functions/comparison/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { eq } = require('./eq');
|
||||
const { lt } = require('./lt');
|
||||
const { gt } = require('./gt');
|
||||
const { lte } = require('./lte');
|
||||
const { gte } = require('./gte');
|
||||
const { ifelse } = require('./ifelse');
|
||||
|
||||
module.exports = { eq, lt, gt, lte, gte, ifelse };
|
39
packages/kbn-tinymath/src/functions/comparison/lt.js
Normal file
39
packages/kbn-tinymath/src/functions/comparison/lt.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Performs a lower than comparison between two values.
|
||||
* @param {number|number[]} a a number or an array of numbers
|
||||
* @param {number|number[]} b a number or an array of numbers
|
||||
* @return {boolean} Returns true if `a` is lower than `b`, false otherwise. Returns an array with the lower than comparison of each element if `a` is an array.
|
||||
* @throws `'Missing b value'` if `b` is not provided
|
||||
* @throws `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
* @example
|
||||
* lt(1, 1) // returns false
|
||||
* lt(1, 2) // returns true
|
||||
* lt([1, 2], 2) // returns [true, false]
|
||||
* lt([1, 2], [1, 2]) // returns [false, false]
|
||||
*/
|
||||
|
||||
function lt(a, b) {
|
||||
if (b == null) {
|
||||
throw new Error('Missing b value');
|
||||
}
|
||||
if (Array.isArray(a)) {
|
||||
if (!Array.isArray(b)) {
|
||||
return a.every((v) => v < b);
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
throw new Error('Array length mismatch');
|
||||
}
|
||||
return a.every((v, i) => v < b[i]);
|
||||
}
|
||||
|
||||
return a < b;
|
||||
}
|
||||
module.exports = { lt };
|
28
packages/kbn-tinymath/src/functions/comparison/lte.js
Normal file
28
packages/kbn-tinymath/src/functions/comparison/lte.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { eq } = require('./eq');
|
||||
const { lt } = require('./lt');
|
||||
|
||||
/**
|
||||
* Performs a lower than or equal comparison between two values.
|
||||
* @param {number|number[]} a a number or an array of numbers
|
||||
* @param {number|number[]} b a number or an array of numbers
|
||||
* @return {boolean} Returns true if `a` is lower than or equal to `b`, false otherwise. Returns an array with the lower than or equal comparison of each element if `a` is an array.
|
||||
* @throws `'Array length mismatch'` if `args` contains arrays of different lengths
|
||||
* @example
|
||||
* lte(1, 1) // returns true
|
||||
* lte(1, 2) // returns true
|
||||
* lte([1, 2], 2) // returns [true, true]
|
||||
* lte([1, 2], [1, 1]) // returns [true, false]
|
||||
*/
|
||||
|
||||
function lte(a, b) {
|
||||
return eq(a, b) || lt(a, b);
|
||||
}
|
||||
module.exports = { lte };
|
|
@ -16,11 +16,10 @@
|
|||
* cos([0, 1.5707963267948966]) // returns [1, 6.123233995736766e-17]
|
||||
*/
|
||||
|
||||
module.exports = { cos };
|
||||
|
||||
function cos(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => Math.cos(a));
|
||||
}
|
||||
return Math.cos(a);
|
||||
}
|
||||
module.exports = { cos };
|
||||
|
|
|
@ -19,10 +19,10 @@ const { size } = require('./size');
|
|||
* count(100) // returns 1
|
||||
*/
|
||||
|
||||
module.exports = { count };
|
||||
|
||||
function count(a) {
|
||||
return size(a);
|
||||
}
|
||||
|
||||
count.skipNumberValidation = true;
|
||||
|
||||
module.exports = { count };
|
||||
|
|
|
@ -18,8 +18,7 @@ const { pow } = require('./pow');
|
|||
* cube([3, 4, 5]) // returns [27, 64, 125]
|
||||
*/
|
||||
|
||||
module.exports = { cube };
|
||||
|
||||
function cube(a) {
|
||||
return pow(a, 3);
|
||||
}
|
||||
module.exports = { cube };
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
* degtorad([0, 90, 180, 360]) // returns [0, 1.5707963267948966, 3.141592653589793, 6.283185307179586]
|
||||
*/
|
||||
|
||||
module.exports = { degtorad };
|
||||
|
||||
function degtorad(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => (a * Math.PI) / 180);
|
||||
}
|
||||
return (a * Math.PI) / 180;
|
||||
}
|
||||
module.exports = { degtorad };
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
* divide([14, 42, 65, 108], [2, 7, 5, 12]) // returns [7, 6, 13, 9]
|
||||
*/
|
||||
|
||||
module.exports = { divide };
|
||||
|
||||
function divide(a, b) {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) throw new Error('Array length mismatch');
|
||||
|
@ -30,8 +28,14 @@ function divide(a, b) {
|
|||
return val / b[i];
|
||||
});
|
||||
}
|
||||
if (Array.isArray(b)) return b.map((b) => a / b);
|
||||
if (Array.isArray(b)) {
|
||||
return b.map((bi) => {
|
||||
if (bi === 0) throw new Error('Cannot divide by 0');
|
||||
return a / bi;
|
||||
});
|
||||
}
|
||||
if (b === 0) throw new Error('Cannot divide by 0');
|
||||
if (Array.isArray(a)) return a.map((a) => a / b);
|
||||
return a / b;
|
||||
}
|
||||
module.exports = { divide };
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
* exp([1, 2, 3]) // returns [e^1, e^2, e^3] = [2.718281828459045, 7.3890560989306495, 20.085536923187668]
|
||||
*/
|
||||
|
||||
module.exports = { exp };
|
||||
|
||||
function exp(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => Math.exp(a));
|
||||
}
|
||||
return Math.exp(a);
|
||||
}
|
||||
module.exports = { exp };
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* first([1, 2, 3]) // returns 1
|
||||
*/
|
||||
|
||||
module.exports = { first };
|
||||
|
||||
function first(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a[0];
|
||||
|
@ -26,3 +24,5 @@ function first(a) {
|
|||
}
|
||||
|
||||
first.skipNumberValidation = true;
|
||||
|
||||
module.exports = { first };
|
||||
|
|
|
@ -24,11 +24,11 @@ const fixer = (a) => {
|
|||
* fix([1.8, 2.9, -3.7, -4.6]) // returns [1, 2, -3, -4]
|
||||
*/
|
||||
|
||||
module.exports = { fix };
|
||||
|
||||
function fix(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => fixer(a));
|
||||
}
|
||||
return fixer(a);
|
||||
}
|
||||
|
||||
module.exports = { fix };
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
* floor([1.7, 2.8, 3.9]) // returns [1, 2, 3]
|
||||
*/
|
||||
|
||||
module.exports = { floor };
|
||||
|
||||
function floor(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => Math.floor(a));
|
||||
}
|
||||
return Math.floor(a);
|
||||
}
|
||||
|
||||
module.exports = { floor };
|
||||
|
|
|
@ -45,6 +45,7 @@ const { subtract } = require('./subtract');
|
|||
const { sum } = require('./sum');
|
||||
const { tan } = require('./tan');
|
||||
const { unique } = require('./unique');
|
||||
const { eq, lt, gt, lte, gte, ifelse } = require('./comparison');
|
||||
|
||||
module.exports = {
|
||||
functions: {
|
||||
|
@ -63,6 +64,7 @@ module.exports = {
|
|||
first,
|
||||
fix,
|
||||
floor,
|
||||
ifelse,
|
||||
last,
|
||||
log,
|
||||
log10,
|
||||
|
@ -87,5 +89,10 @@ module.exports = {
|
|||
sum,
|
||||
tan,
|
||||
unique,
|
||||
eq,
|
||||
lt,
|
||||
gt,
|
||||
lte,
|
||||
gte,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* last([1, 2, 3]) // returns 3
|
||||
*/
|
||||
|
||||
module.exports = { last };
|
||||
|
||||
function last(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a[a.length - 1];
|
||||
|
@ -26,3 +24,4 @@ function last(a) {
|
|||
}
|
||||
|
||||
last.skipNumberValidation = true;
|
||||
module.exports = { last };
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
/**
|
||||
* Transposes a 2D array, i.e. turns the rows into columns and vice versa. Scalar values are also included in the transpose.
|
||||
* @private
|
||||
* @param {any[][]} args an array or an array that contains arrays
|
||||
* @param {number} index index of the first array element in args
|
||||
* @return {any[][]} transpose of args
|
||||
|
|
|
@ -22,8 +22,6 @@ const changeOfBase = (a, b) => Math.log(a) / Math.log(b);
|
|||
* log([2, 4, 8, 16, 32], 2) // returns [1, 2, 3, 4, 5]
|
||||
*/
|
||||
|
||||
module.exports = { log };
|
||||
|
||||
function log(a, b = Math.E) {
|
||||
if (b <= 0) throw new Error('Base out of range');
|
||||
|
||||
|
@ -36,3 +34,4 @@ function log(a, b = Math.E) {
|
|||
if (a < 0) throw new Error('Must be greater than 0');
|
||||
return changeOfBase(a, b);
|
||||
}
|
||||
module.exports = { log };
|
||||
|
|
|
@ -20,8 +20,7 @@ const { log } = require('./log');
|
|||
* log([10, 100, 1000, 10000, 100000]) // returns [1, 2, 3, 4, 5]
|
||||
*/
|
||||
|
||||
module.exports = { log10 };
|
||||
|
||||
function log10(a) {
|
||||
return log(a, 10);
|
||||
}
|
||||
module.exports = { log10 };
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* max([1, 9], 4, [3, 5]) // returns [max([1, 4, 3]), max([9, 4, 5])] = [4, 9]
|
||||
*/
|
||||
|
||||
module.exports = { max };
|
||||
|
||||
function max(...args) {
|
||||
if (args.length === 1) {
|
||||
if (Array.isArray(args[0]))
|
||||
|
@ -36,3 +34,4 @@ function max(...args) {
|
|||
return Math.max(result, current);
|
||||
});
|
||||
}
|
||||
module.exports = { max };
|
||||
|
|
|
@ -19,8 +19,6 @@ const { add } = require('./add');
|
|||
* mean([1, 9], 5, [3, 4]) // returns [mean([1, 5, 3]), mean([9, 5, 4])] = [3, 6]
|
||||
*/
|
||||
|
||||
module.exports = { mean };
|
||||
|
||||
function mean(...args) {
|
||||
if (args.length === 1) {
|
||||
if (Array.isArray(args[0])) return add(args[0]) / args[0].length;
|
||||
|
@ -34,3 +32,4 @@ function mean(...args) {
|
|||
|
||||
return sum / args.length;
|
||||
}
|
||||
module.exports = { mean };
|
||||
|
|
|
@ -33,8 +33,6 @@ const findMedian = (a) => {
|
|||
* median([1, 9], 2, 4, [3, 5]) // returns [median([1, 2, 4, 3]), median([9, 2, 4, 5])] = [2.5, 4.5]
|
||||
*/
|
||||
|
||||
module.exports = { median };
|
||||
|
||||
function median(...args) {
|
||||
if (args.length === 1) {
|
||||
if (Array.isArray(args[0])) return findMedian(args[0]);
|
||||
|
@ -48,3 +46,4 @@ function median(...args) {
|
|||
}
|
||||
return findMedian(args);
|
||||
}
|
||||
module.exports = { median };
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* min([1, 9], 4, [3, 5]) // returns [min([1, 4, 3]), min([9, 4, 5])] = [1, 4]
|
||||
*/
|
||||
|
||||
module.exports = { min };
|
||||
|
||||
function min(...args) {
|
||||
if (args.length === 1) {
|
||||
if (Array.isArray(args[0]))
|
||||
|
@ -36,3 +34,4 @@ function min(...args) {
|
|||
return Math.min(result, current);
|
||||
});
|
||||
}
|
||||
module.exports = { min };
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
* mod([14, 42, 65, 108], [5, 4, 14, 2]) // returns [5, 2, 9, 0]
|
||||
*/
|
||||
|
||||
module.exports = { mod };
|
||||
|
||||
function mod(a, b) {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) throw new Error('Array length mismatch');
|
||||
|
@ -35,3 +33,4 @@ function mod(a, b) {
|
|||
if (Array.isArray(a)) return a.map((a) => a % b);
|
||||
return a % b;
|
||||
}
|
||||
module.exports = { mod };
|
||||
|
|
|
@ -40,8 +40,6 @@ const findMode = (a) => {
|
|||
* mode([1, 9], 1, 4, [3, 5]) // returns [mode([1, 1, 4, 3]), mode([9, 1, 4, 5])] = [[1], [4, 5, 9]]
|
||||
*/
|
||||
|
||||
module.exports = { mode };
|
||||
|
||||
function mode(...args) {
|
||||
if (args.length === 1) {
|
||||
if (Array.isArray(args[0])) return findMode(args[0]);
|
||||
|
@ -55,3 +53,4 @@ function mode(...args) {
|
|||
}
|
||||
return findMode(args);
|
||||
}
|
||||
module.exports = { mode };
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
* multiply([1, 2, 3, 4], [2, 7, 5, 12]) // returns [2, 14, 15, 48]
|
||||
*/
|
||||
|
||||
module.exports = { multiply };
|
||||
|
||||
function multiply(...args) {
|
||||
return args.reduce((result, current) => {
|
||||
if (Array.isArray(result) && Array.isArray(current)) {
|
||||
|
@ -32,3 +30,4 @@ function multiply(...args) {
|
|||
return result * current;
|
||||
});
|
||||
}
|
||||
module.exports = { multiply };
|
||||
|
|
|
@ -14,8 +14,7 @@
|
|||
* pi() // 3.141592653589793
|
||||
*/
|
||||
|
||||
module.exports = { pi };
|
||||
|
||||
function pi() {
|
||||
return Math.PI;
|
||||
}
|
||||
module.exports = { pi };
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* pow([1, 2, 3], 4) // returns [1, 16, 81]
|
||||
*/
|
||||
|
||||
module.exports = { pow };
|
||||
|
||||
function pow(a, b) {
|
||||
if (b == null) throw new Error('Missing exponent');
|
||||
if (Array.isArray(a)) {
|
||||
|
@ -26,3 +24,4 @@ function pow(a, b) {
|
|||
}
|
||||
return Math.pow(a, b);
|
||||
}
|
||||
module.exports = { pow };
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
* radtodeg([0, 1.5707963267948966, 3.141592653589793, 6.283185307179586]) // returns [0, 90, 180, 360]
|
||||
*/
|
||||
|
||||
module.exports = { radtodeg };
|
||||
|
||||
function radtodeg(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => (a * 180) / Math.PI);
|
||||
}
|
||||
return (a * 180) / Math.PI;
|
||||
}
|
||||
module.exports = { radtodeg };
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
* random(-10,10) // returns a random number between -10 (inclusive) and 10 (exclusive)
|
||||
*/
|
||||
|
||||
module.exports = { random };
|
||||
|
||||
function random(a, b) {
|
||||
if (a == null) return Math.random();
|
||||
|
||||
|
@ -33,3 +31,5 @@ function random(a, b) {
|
|||
if (a > b) throw new Error(`Min is greater than max`);
|
||||
return Math.random() * (b - a) + a;
|
||||
}
|
||||
|
||||
module.exports = { random };
|
||||
|
|
|
@ -21,8 +21,7 @@ const { subtract } = require('./subtract');
|
|||
* range([1, 9], 4, [3, 5]) // returns [range([1, 4, 3]), range([9, 4, 5])] = [3, 5]
|
||||
*/
|
||||
|
||||
module.exports = { range };
|
||||
|
||||
function range(...args) {
|
||||
return subtract(max(...args), min(...args));
|
||||
}
|
||||
module.exports = { range };
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const rounder = (a, b) => Math.round(a * Math.pow(10, b)) / Math.pow(10, b);
|
||||
const rounder = (a, b = 0) => Math.round(a * Math.pow(10, b)) / Math.pow(10, b);
|
||||
|
||||
/**
|
||||
* Rounds a number towards the nearest integer by default or decimal place if specified. For arrays, the function will be applied index-wise to each element.
|
||||
|
@ -22,11 +22,10 @@ const rounder = (a, b) => Math.round(a * Math.pow(10, b)) / Math.pow(10, b);
|
|||
* round([2.9234, 5.1234, 3.5234, 4.49234324], 2) // returns [2.92, 5.12, 3.52, 4.49]
|
||||
*/
|
||||
|
||||
module.exports = { round };
|
||||
|
||||
function round(a, b = 0) {
|
||||
function round(a, b) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => rounder(a, b));
|
||||
}
|
||||
return rounder(a, b);
|
||||
}
|
||||
module.exports = { round };
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
* sin([0, 1.5707963267948966]) // returns [0, 1]
|
||||
*/
|
||||
|
||||
module.exports = { sin };
|
||||
|
||||
function sin(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => Math.sin(a));
|
||||
}
|
||||
return Math.sin(a);
|
||||
}
|
||||
module.exports = { sin };
|
||||
|
|
|
@ -17,11 +17,10 @@
|
|||
* size(100) // returns 1
|
||||
*/
|
||||
|
||||
module.exports = { size };
|
||||
|
||||
function size(a) {
|
||||
if (Array.isArray(a)) return a.length;
|
||||
throw new Error('Must pass an array');
|
||||
}
|
||||
|
||||
size.skipNumberValidation = true;
|
||||
module.exports = { size };
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
* sqrt([9, 16, 25]) // returns [3, 4, 5]
|
||||
*/
|
||||
|
||||
module.exports = { sqrt };
|
||||
|
||||
function sqrt(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => {
|
||||
|
@ -30,3 +28,4 @@ function sqrt(a) {
|
|||
if (a < 0) throw new Error('Unable find the square root of a negative number');
|
||||
return Math.sqrt(a);
|
||||
}
|
||||
module.exports = { sqrt };
|
||||
|
|
|
@ -18,8 +18,7 @@ const { pow } = require('./pow');
|
|||
* square([3, 4, 5]) // returns [9, 16, 25]
|
||||
*/
|
||||
|
||||
module.exports = { square };
|
||||
|
||||
function square(a) {
|
||||
return pow(a, 2);
|
||||
}
|
||||
module.exports = { square };
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
* subtract([14, 42, 65, 108], [2, 7, 5, 12]) // returns [12, 35, 52, 96]
|
||||
*/
|
||||
|
||||
module.exports = { subtract };
|
||||
|
||||
function subtract(a, b) {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) throw new Error('Array length mismatch');
|
||||
|
@ -30,3 +28,4 @@ function subtract(a, b) {
|
|||
if (Array.isArray(b)) return b.map((b) => a - b);
|
||||
return a - b;
|
||||
}
|
||||
module.exports = { subtract };
|
||||
|
|
|
@ -20,8 +20,6 @@ const findSum = (total, current) => total + current;
|
|||
* sum([10, 20, 30, 40], 10, [1, 2, 3], 22) // returns sum(10, 20, 30, 40, 10, 1, 2, 3, 22) = 138
|
||||
*/
|
||||
|
||||
module.exports = { sum };
|
||||
|
||||
function sum(...args) {
|
||||
return args.reduce((total, current) => {
|
||||
if (Array.isArray(current)) {
|
||||
|
@ -30,3 +28,4 @@ function sum(...args) {
|
|||
return total + current;
|
||||
}, 0);
|
||||
}
|
||||
module.exports = { sum };
|
||||
|
|
|
@ -16,11 +16,10 @@
|
|||
* tan([0, 1, -1]) // returns [0, 1.5574077246549023, -1.5574077246549023]
|
||||
*/
|
||||
|
||||
module.exports = { tan };
|
||||
|
||||
function tan(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.map((a) => Math.tan(a));
|
||||
}
|
||||
return Math.tan(a);
|
||||
}
|
||||
module.exports = { tan };
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
* unique([1, 2, 3, 4, 2, 2, 2, 3, 4, 2, 4, 5, 2, 1, 4, 2]) // returns 5
|
||||
*/
|
||||
|
||||
module.exports = { unique };
|
||||
|
||||
function unique(a) {
|
||||
if (Array.isArray(a)) {
|
||||
return a.filter((val, i) => a.indexOf(val) === i).length;
|
||||
|
@ -28,3 +26,4 @@ function unique(a) {
|
|||
}
|
||||
|
||||
unique.skipNumberValidation = true;
|
||||
module.exports = { unique };
|
||||
|
|
42
packages/kbn-tinymath/test/functions/comparison/eq.test.js
Normal file
42
packages/kbn-tinymath/test/functions/comparison/eq.test.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { eq } = require('../../../src/functions/comparison/eq');
|
||||
|
||||
describe('Eq', () => {
|
||||
it('numbers', () => {
|
||||
expect(eq(-10, -10)).toBeTruthy();
|
||||
expect(eq(10, 10)).toBeTruthy();
|
||||
expect(eq(0, 0)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('arrays', () => {
|
||||
// Should pass
|
||||
expect(eq([-1], -1)).toBeTruthy();
|
||||
expect(eq([-1], [-1])).toBeTruthy();
|
||||
expect(eq([-1, -1], -1)).toBeTruthy();
|
||||
expect(eq([-1, -1], [-1, -1])).toBeTruthy();
|
||||
|
||||
// Should not pass
|
||||
expect(eq([-1], 0)).toBeFalsy();
|
||||
expect(eq([-1], [0])).toBeFalsy();
|
||||
expect(eq([-1, -1], 0)).toBeFalsy();
|
||||
expect(eq([-1, -1], [0, 0])).toBeFalsy();
|
||||
expect(eq([-1, -1], [-1, 0])).toBeFalsy();
|
||||
});
|
||||
|
||||
it('missing args', () => {
|
||||
expect(() => eq()).toThrow();
|
||||
expect(() => eq(-10)).toThrow();
|
||||
expect(() => eq([])).toThrow();
|
||||
});
|
||||
|
||||
it('empty arrays', () => {
|
||||
expect(eq([], [])).toBeTruthy();
|
||||
});
|
||||
});
|
42
packages/kbn-tinymath/test/functions/comparison/gt.test.js
Normal file
42
packages/kbn-tinymath/test/functions/comparison/gt.test.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { gt } = require('../../../src/functions/comparison/gt');
|
||||
|
||||
describe('Gt', () => {
|
||||
it('missing args', () => {
|
||||
expect(() => gt()).toThrow();
|
||||
expect(() => gt(-10)).toThrow();
|
||||
expect(() => gt([])).toThrow();
|
||||
});
|
||||
|
||||
it('empty arrays', () => {
|
||||
expect(gt([], [])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('numbers', () => {
|
||||
expect(gt(-10, -20)).toBeTruthy();
|
||||
expect(gt(10, 0)).toBeTruthy();
|
||||
expect(gt(0, -1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('arrays', () => {
|
||||
// Should pass
|
||||
expect(gt([-1], -2)).toBeTruthy();
|
||||
expect(gt([-1], [-2])).toBeTruthy();
|
||||
expect(gt([-1, -1], -2)).toBeTruthy();
|
||||
expect(gt([-1, -1], [-2, -2])).toBeTruthy();
|
||||
|
||||
// Should not pass
|
||||
expect(gt([-1], 2)).toBeFalsy();
|
||||
expect(gt([-1], [2])).toBeFalsy();
|
||||
expect(gt([-1, -1], 2)).toBeFalsy();
|
||||
expect(gt([-1, -1], [2, 2])).toBeFalsy();
|
||||
expect(gt([-1, -1], [-2, 2])).toBeFalsy();
|
||||
});
|
||||
});
|
59
packages/kbn-tinymath/test/functions/comparison/gte.test.js
Normal file
59
packages/kbn-tinymath/test/functions/comparison/gte.test.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { gte } = require('../../../src/functions/comparison/gte');
|
||||
|
||||
describe('Gte', () => {
|
||||
it('missing args', () => {
|
||||
expect(() => gte()).toThrow();
|
||||
expect(() => gte(-10)).toThrow();
|
||||
expect(() => gte([])).toThrow();
|
||||
});
|
||||
|
||||
it('empty arrays', () => {
|
||||
expect(gte([], [])).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('eq values', () => {
|
||||
it('numbers', () => {
|
||||
expect(gte(-10, -10)).toBeTruthy();
|
||||
expect(gte(10, 10)).toBeTruthy();
|
||||
expect(gte(0, 0)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('arrays', () => {
|
||||
expect(gte([-1], -1)).toBeTruthy();
|
||||
expect(gte([-1], [-1])).toBeTruthy();
|
||||
expect(gte([-1, -1], -1)).toBeTruthy();
|
||||
expect(gte([-1, -1], [-1, -1])).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('gt values', () => {
|
||||
it('numbers', () => {
|
||||
expect(gte(-10, -20)).toBeTruthy();
|
||||
expect(gte(10, 0)).toBeTruthy();
|
||||
expect(gte(0, -1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('arrays', () => {
|
||||
// Should pass
|
||||
expect(gte([-1], -2)).toBeTruthy();
|
||||
expect(gte([-1], [-2])).toBeTruthy();
|
||||
expect(gte([-1, -1], -2)).toBeTruthy();
|
||||
expect(gte([-1, -1], [-2, -2])).toBeTruthy();
|
||||
|
||||
// Should not pass
|
||||
expect(gte([-1], 2)).toBeFalsy();
|
||||
expect(gte([-1], [2])).toBeFalsy();
|
||||
expect(gte([-1, -1], 2)).toBeFalsy();
|
||||
expect(gte([-1, -1], [2, 2])).toBeFalsy();
|
||||
expect(gte([-1, -1], [-2, 2])).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { ifelse } = require('../../../src/functions/comparison/ifelse');
|
||||
|
||||
describe('Ifelse', () => {
|
||||
it('should basically work', () => {
|
||||
expect(ifelse(true, 1, 0)).toEqual(1);
|
||||
expect(ifelse(false, 1, 0)).toEqual(0);
|
||||
expect(ifelse(1 > 0, 1, 0)).toEqual(1);
|
||||
expect(ifelse(1 < 0, 1, 0)).toEqual(0);
|
||||
});
|
||||
|
||||
it('should throw if cond is not of boolean type', () => {
|
||||
expect(() => ifelse(5, 1, 0)).toThrow('Condition clause is of the wrong type');
|
||||
expect(() => ifelse(null, 1, 0)).toThrow('Condition clause is of the wrong type');
|
||||
expect(() => ifelse(undefined, 1, 0)).toThrow('Condition clause is of the wrong type');
|
||||
expect(() => ifelse(0, 1, 0)).toThrow('Condition clause is of the wrong type');
|
||||
});
|
||||
|
||||
it('missing args', () => {
|
||||
expect(() => ifelse()).toThrow();
|
||||
expect(() => ifelse(-10)).toThrow();
|
||||
expect(() => ifelse([])).toThrow();
|
||||
expect(() => ifelse(true)).toThrow();
|
||||
expect(() => ifelse(true, 1)).toThrow();
|
||||
});
|
||||
});
|
42
packages/kbn-tinymath/test/functions/comparison/lt.test.js
Normal file
42
packages/kbn-tinymath/test/functions/comparison/lt.test.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { lt } = require('../../../src/functions/comparison/lt');
|
||||
|
||||
describe('Lt', () => {
|
||||
it('missing args', () => {
|
||||
expect(() => lt()).toThrow();
|
||||
expect(() => lt(-10)).toThrow();
|
||||
expect(() => lt([])).toThrow();
|
||||
});
|
||||
|
||||
it('empty arrays', () => {
|
||||
expect(lt([], [])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('numbers', () => {
|
||||
expect(lt(-10, -2)).toBeTruthy();
|
||||
expect(lt(10, 20)).toBeTruthy();
|
||||
expect(lt(0, 1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('arrays', () => {
|
||||
// Should pass
|
||||
expect(lt([-1], 0)).toBeTruthy();
|
||||
expect(lt([-1], [0])).toBeTruthy();
|
||||
expect(lt([-1, -1], 0)).toBeTruthy();
|
||||
expect(lt([-1, -1], [0, 0])).toBeTruthy();
|
||||
|
||||
// Should not pass
|
||||
expect(lt([-1], -2)).toBeFalsy();
|
||||
expect(lt([-1], [-2])).toBeFalsy();
|
||||
expect(lt([-1, -1], -2)).toBeFalsy();
|
||||
expect(lt([-1, -1], [-2, -2])).toBeFalsy();
|
||||
expect(lt([-1, -1], [-2, 2])).toBeFalsy();
|
||||
});
|
||||
});
|
59
packages/kbn-tinymath/test/functions/comparison/lte.test.js
Normal file
59
packages/kbn-tinymath/test/functions/comparison/lte.test.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const { lte } = require('../../../src/functions/comparison/lte');
|
||||
|
||||
describe('Lte', () => {
|
||||
it('missing args', () => {
|
||||
expect(() => lte()).toThrow();
|
||||
expect(() => lte(-10)).toThrow();
|
||||
expect(() => lte([])).toThrow();
|
||||
});
|
||||
|
||||
it('empty arrays', () => {
|
||||
expect(lte([], [])).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('eq values', () => {
|
||||
it('numbers', () => {
|
||||
expect(lte(-10, -10)).toBeTruthy();
|
||||
expect(lte(10, 10)).toBeTruthy();
|
||||
expect(lte(0, 0)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('arrays', () => {
|
||||
expect(lte([-1], -1)).toBeTruthy();
|
||||
expect(lte([-1], [-1])).toBeTruthy();
|
||||
expect(lte([-1, -1], -1)).toBeTruthy();
|
||||
expect(lte([-1, -1], [-1, -1])).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('lt values', () => {
|
||||
it('numbers', () => {
|
||||
expect(lte(-10, -2)).toBeTruthy();
|
||||
expect(lte(10, 20)).toBeTruthy();
|
||||
expect(lte(0, 1)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('arrays', () => {
|
||||
// Should pass
|
||||
expect(lte([-1], 0)).toBeTruthy();
|
||||
expect(lte([-1], [0])).toBeTruthy();
|
||||
expect(lte([-1, -1], 0)).toBeTruthy();
|
||||
expect(lte([-1, -1], [0, 0])).toBeTruthy();
|
||||
|
||||
// Should not pass
|
||||
expect(lte([-1], -2)).toBeFalsy();
|
||||
expect(lte([-1], [-2])).toBeFalsy();
|
||||
expect(lte([-1, -1], -2)).toBeFalsy();
|
||||
expect(lte([-1, -1], [-2, -2])).toBeFalsy();
|
||||
expect(lte([-1, -1], [-2, 2])).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -29,4 +29,11 @@ describe('Divide', () => {
|
|||
it('array length mismatch', () => {
|
||||
expect(() => divide([1, 2], [3])).toThrow('Array length mismatch');
|
||||
});
|
||||
|
||||
it('divide by 0', () => {
|
||||
expect(() => divide([1, 2], 0)).toThrow('Cannot divide by 0');
|
||||
expect(() => divide(1, 0)).toThrow('Cannot divide by 0');
|
||||
expect(() => divide([1, 2], [0, 0])).toThrow('Cannot divide by 0');
|
||||
expect(() => divide(1, [1, 0])).toThrow('Cannot divide by 0');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -68,6 +68,91 @@ describe('Parser', () => {
|
|||
location: { min: 0, max: 13 },
|
||||
});
|
||||
});
|
||||
|
||||
describe('Comparison', () => {
|
||||
it('should throw for non valid comparison symbols', () => {
|
||||
const symbols = ['<>', '><', '===', '>>', '<<'];
|
||||
for (const symbol of symbols) {
|
||||
expect(() => parse(`5 ${symbol} 1`)).toThrow();
|
||||
}
|
||||
});
|
||||
describe.each`
|
||||
symbol | fn
|
||||
${'<'} | ${'lt'}
|
||||
${'>'} | ${'gt'}
|
||||
${'=='} | ${'eq'}
|
||||
${'>='} | ${'gte'}
|
||||
${'<='} | ${'lte'}
|
||||
`('Symbol "$symbol" ( $fn )', ({ symbol, fn }) => {
|
||||
it(`should parse comparison symbol: "$symbol"`, () => {
|
||||
expect(parse(`5 ${symbol} 1`)).toEqual({
|
||||
name: fn,
|
||||
type: 'function',
|
||||
args: [5, 1],
|
||||
text: `5 ${symbol} 1`,
|
||||
location: { min: 0, max: 4 + symbol.length },
|
||||
});
|
||||
expect(parse(`a ${symbol} b`)).toEqual({
|
||||
name: fn,
|
||||
type: 'function',
|
||||
args: [variableEqual('a'), variableEqual('b')],
|
||||
text: `a ${symbol} b`,
|
||||
location: { min: 0, max: 4 + symbol.length },
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
expression
|
||||
${`1 + (1 ${symbol} 1)`}
|
||||
${`(1 ${symbol} 1) + 1`}
|
||||
${`((1 ${symbol} 1) + 1)`}
|
||||
${`((1 ${symbol} 1) + (1 ${symbol} 1))`}
|
||||
${`((1 ${symbol} 1) + ( ${symbol} 1))`}
|
||||
${` ${symbol} 1`}
|
||||
${`1 ${symbol} `}
|
||||
${`a + (b ${symbol} c)`}
|
||||
${`(a ${symbol} b) + c`}
|
||||
${`((a ${symbol} b) + c)`}
|
||||
${`((a ${symbol} b) + (c ${symbol} d))`}
|
||||
${`((a ${symbol} b) + ( ${symbol} c))`}
|
||||
${` ${symbol} a`}
|
||||
${`a ${symbol} `}
|
||||
`(
|
||||
'should throw for invalid expression with comparison arguments: $expression',
|
||||
({ expression }) => {
|
||||
expect(() => parse(expression)).toThrow();
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
expression
|
||||
${`1 ${symbol} 1 ${symbol} 1`}
|
||||
${`(1 ${symbol} 1) ${symbol} 1`}
|
||||
${`1 ${symbol} (1 ${symbol} 1)`}
|
||||
${`a ${symbol} b ${symbol} c`}
|
||||
${`(a ${symbol} b) ${symbol} c`}
|
||||
${`a ${symbol} (b ${symbol} c)`}
|
||||
`('should throw for cascading comparison operators: $expression', ({ expression }) => {
|
||||
expect(() => parse(expression)).toThrow();
|
||||
});
|
||||
|
||||
it.each`
|
||||
expression
|
||||
${`1 ${symbol} 1`}
|
||||
${`(1 ${symbol} 1)`}
|
||||
${`((1 ${symbol} 1))`}
|
||||
${`((1 + 1) ${symbol} 1)`}
|
||||
${`1 + 1 ${symbol} 1 * 1`}
|
||||
${`a ${symbol} b`}
|
||||
${`(a ${symbol} b)`}
|
||||
${`((a ${symbol} b))`}
|
||||
${`((a + b) ${symbol} c)`}
|
||||
${`a + b ${symbol} c * d`}
|
||||
`('should parse comparison expressions: $expression', ({ expression }) => {
|
||||
expect(() => parse(expression)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Variables', () => {
|
||||
|
|
|
@ -253,7 +253,7 @@ describe('math(resp, panel, series)', () => {
|
|||
)(await mathAgg(resp, panel, series)((results) => results))([]);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual(
|
||||
'Failed to parse expression. Expected "*", "+", "-", "/", end of input, or whitespace but "(" found.'
|
||||
'Failed to parse expression. Expected "*", "+", "-", "/", "<", "=", ">", end of input, or whitespace but "(" found.'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -48,7 +48,7 @@ import { MemoizedFormulaHelp } from './formula_help';
|
|||
import './formula.scss';
|
||||
import { FormulaIndexPatternColumn } from '../formula';
|
||||
import { insertOrReplaceFormulaColumn } from '../parse';
|
||||
import { filterByVisibleOperation } from '../util';
|
||||
import { filterByVisibleOperation, nonNullable } from '../util';
|
||||
import { getColumnTimeShiftWarnings, getDateHistogramInterval } from '../../../../time_shift_utils';
|
||||
|
||||
function tableHasData(
|
||||
|
@ -363,7 +363,7 @@ export function FormulaEditor({
|
|||
}
|
||||
return newWarnings;
|
||||
})
|
||||
.filter((marker) => marker);
|
||||
.filter(nonNullable);
|
||||
setWarnings(markers.map(({ severity, message }) => ({ severity, message })));
|
||||
monaco.editor.setModelMarkers(editorModel.current, 'LENS', markers);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { Markdown } from '@kbn/kibana-react-plugin/public';
|
||||
import { groupBy } from 'lodash';
|
||||
import type { IndexPattern } from '../../../../../../types';
|
||||
import { tinymathFunctions } from '../util';
|
||||
import { getPossibleFunctions } from './math_completion';
|
||||
|
@ -193,31 +194,40 @@ max(system.network.in.bytes, reducedTimeRange="30m")
|
|||
items: [],
|
||||
});
|
||||
|
||||
const availableFunctions = getPossibleFunctions(indexPattern);
|
||||
const {
|
||||
elasticsearch: esFunction,
|
||||
calculation: calculationFunction,
|
||||
math: mathOperations,
|
||||
comparison: comparisonOperations,
|
||||
} = useMemo(
|
||||
() =>
|
||||
groupBy(getPossibleFunctions(indexPattern), (key) => {
|
||||
if (key in operationDefinitionMap) {
|
||||
return operationDefinitionMap[key].documentation?.section;
|
||||
}
|
||||
if (key in tinymathFunctions) {
|
||||
return tinymathFunctions[key].section;
|
||||
}
|
||||
}),
|
||||
[operationDefinitionMap, indexPattern]
|
||||
);
|
||||
|
||||
// Es aggs
|
||||
helpGroups[2].items.push(
|
||||
...availableFunctions
|
||||
.filter(
|
||||
(key) =>
|
||||
key in operationDefinitionMap &&
|
||||
operationDefinitionMap[key].documentation?.section === 'elasticsearch'
|
||||
)
|
||||
.sort()
|
||||
.map((key) => ({
|
||||
label: key,
|
||||
description: (
|
||||
<>
|
||||
<h3>
|
||||
{key}({operationDefinitionMap[key].documentation?.signature})
|
||||
</h3>
|
||||
...esFunction.sort().map((key) => ({
|
||||
label: key,
|
||||
description: (
|
||||
<>
|
||||
<h3>
|
||||
{key}({operationDefinitionMap[key].documentation?.signature})
|
||||
</h3>
|
||||
|
||||
{operationDefinitionMap[key].documentation?.description ? (
|
||||
<Markdown markdown={operationDefinitionMap[key].documentation!.description} />
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
}))
|
||||
{operationDefinitionMap[key].documentation?.description ? (
|
||||
<Markdown markdown={operationDefinitionMap[key].documentation!.description} />
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
}))
|
||||
);
|
||||
|
||||
helpGroups.push({
|
||||
|
@ -236,31 +246,24 @@ max(system.network.in.bytes, reducedTimeRange="30m")
|
|||
|
||||
// Calculations aggs
|
||||
helpGroups[3].items.push(
|
||||
...availableFunctions
|
||||
.filter(
|
||||
(key) =>
|
||||
key in operationDefinitionMap &&
|
||||
operationDefinitionMap[key].documentation?.section === 'calculation'
|
||||
)
|
||||
.sort()
|
||||
.map((key) => ({
|
||||
label: key,
|
||||
description: (
|
||||
<>
|
||||
<h3>
|
||||
{key}({operationDefinitionMap[key].documentation?.signature})
|
||||
</h3>
|
||||
...calculationFunction.sort().map((key) => ({
|
||||
label: key,
|
||||
description: (
|
||||
<>
|
||||
<h3>
|
||||
{key}({operationDefinitionMap[key].documentation?.signature})
|
||||
</h3>
|
||||
|
||||
{operationDefinitionMap[key].documentation?.description ? (
|
||||
<Markdown markdown={operationDefinitionMap[key].documentation!.description} />
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
checked:
|
||||
selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}`
|
||||
? ('on' as const)
|
||||
: undefined,
|
||||
}))
|
||||
{operationDefinitionMap[key].documentation?.description ? (
|
||||
<Markdown markdown={operationDefinitionMap[key].documentation!.description} />
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
checked:
|
||||
selectedFunction === `${key}: ${operationDefinitionMap[key].displayName}`
|
||||
? ('on' as const)
|
||||
: undefined,
|
||||
}))
|
||||
);
|
||||
|
||||
helpGroups.push({
|
||||
|
@ -274,22 +277,55 @@ max(system.network.in.bytes, reducedTimeRange="30m")
|
|||
items: [],
|
||||
});
|
||||
|
||||
const tinymathFns = useMemo(() => {
|
||||
return getPossibleFunctions(indexPattern)
|
||||
.filter((key) => key in tinymathFunctions)
|
||||
.sort()
|
||||
.map((key) => {
|
||||
const [description, examples] = tinymathFunctions[key].help.split(`\`\`\``);
|
||||
return {
|
||||
label: key,
|
||||
description: description.replace(/\n/g, '\n\n'),
|
||||
examples: examples ? `\`\`\`${examples}\`\`\`` : '',
|
||||
};
|
||||
});
|
||||
}, [indexPattern]);
|
||||
const mathFns = useMemo(() => {
|
||||
return mathOperations.sort().map((key) => {
|
||||
const [description, examples] = tinymathFunctions[key].help.split(`\`\`\``);
|
||||
return {
|
||||
label: key,
|
||||
description: description.replace(/\n/g, '\n\n'),
|
||||
examples: examples ? `\`\`\`${examples}\`\`\`` : '',
|
||||
};
|
||||
});
|
||||
}, [mathOperations]);
|
||||
|
||||
helpGroups[4].items.push(
|
||||
...tinymathFns.map(({ label, description, examples }) => {
|
||||
...mathFns.map(({ label, description, examples }) => {
|
||||
return {
|
||||
label,
|
||||
description: (
|
||||
<>
|
||||
<h3>{getFunctionSignatureLabel(label, operationDefinitionMap)}</h3>
|
||||
|
||||
<Markdown markdown={`${description}${examples}`} />
|
||||
</>
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
helpGroups.push({
|
||||
label: i18n.translate('xpack.lens.formulaDocumentation.comparisonSection', {
|
||||
defaultMessage: 'Comparison',
|
||||
}),
|
||||
description: i18n.translate('xpack.lens.formulaDocumentation.comparisonSectionDescription', {
|
||||
defaultMessage: 'These functions are used to perform value comparison.',
|
||||
}),
|
||||
items: [],
|
||||
});
|
||||
|
||||
const comparisonFns = useMemo(() => {
|
||||
return comparisonOperations.sort().map((key) => {
|
||||
const [description, examples] = tinymathFunctions[key].help.split(`\`\`\``);
|
||||
return {
|
||||
label: key,
|
||||
description: description.replace(/\n/g, '\n\n'),
|
||||
examples: examples ? `\`\`\`${examples}\`\`\`` : '',
|
||||
};
|
||||
});
|
||||
}, [comparisonOperations]);
|
||||
|
||||
helpGroups[5].items.push(
|
||||
...comparisonFns.map(({ label, description, examples }) => {
|
||||
return {
|
||||
label,
|
||||
description: (
|
||||
|
|
|
@ -24,7 +24,7 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
|||
import { parseTimeShift } from '@kbn/data-plugin/common';
|
||||
import type { IndexPattern } from '../../../../../../types';
|
||||
import { memoizedGetAvailableOperationsByMetadata } from '../../../operations';
|
||||
import { tinymathFunctions, groupArgsByType, unquotedStringRegex } from '../util';
|
||||
import { tinymathFunctions, groupArgsByType, unquotedStringRegex, nonNullable } from '../util';
|
||||
import type { GenericOperationDefinition } from '../..';
|
||||
import { getFunctionSignatureLabel, getHelpTextContent } from './formula_help';
|
||||
import { hasFunctionFieldArgument } from '../validation';
|
||||
|
@ -78,7 +78,7 @@ export function getInfoAtZeroIndexedPosition(
|
|||
if (ast.type === 'function') {
|
||||
const [match] = ast.args
|
||||
.map((arg) => getInfoAtZeroIndexedPosition(arg, zeroIndexedPosition, ast))
|
||||
.filter((a) => a);
|
||||
.filter(nonNullable);
|
||||
if (match) {
|
||||
return match;
|
||||
} else if (ast.location) {
|
||||
|
@ -297,7 +297,7 @@ function getArgumentSuggestions(
|
|||
const fields = validOperation.operations
|
||||
.filter((op) => op.operationType === operation.type)
|
||||
.map((op) => ('field' in op ? op.field : undefined))
|
||||
.filter((field) => field);
|
||||
.filter(nonNullable);
|
||||
const fieldArg = ast.args[0];
|
||||
const location = typeof fieldArg !== 'string' && (fieldArg as TinymathVariable).location;
|
||||
let range: monaco.IRange | undefined;
|
||||
|
|
|
@ -1491,6 +1491,23 @@ invalid: "
|
|||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('returns no error if the formula contains comparison operator within the ifelse operation', () => {
|
||||
const formulas = [
|
||||
...['lt', 'gt', 'lte', 'gte', 'eq'].map((op) => `${op}(5, 1)`),
|
||||
...['<', '>', '==', '>=', '<='].map((symbol) => `5 ${symbol} 1`),
|
||||
];
|
||||
for (const formula of formulas) {
|
||||
expect(
|
||||
formulaOperation.getErrorMessage!(
|
||||
getNewLayerWithFormula(`ifelse(${formula}, 1, 5)`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns no error if a math operation is passed to fullReference operations', () => {
|
||||
const formulas = [
|
||||
'derivative(7+1)',
|
||||
|
@ -1534,6 +1551,8 @@ invalid: "
|
|||
{ formula: 'last_value(dest)' },
|
||||
{ formula: 'terms(dest)' },
|
||||
{ formula: 'moving_average(last_value(dest), window=7)', errorFormula: 'last_value(dest)' },
|
||||
...['lt', 'gt', 'lte', 'gte', 'eq'].map((op) => ({ formula: `${op}(5, 1)` })),
|
||||
...['<', '>', '==', '>=', '<='].map((symbol) => ({ formula: `5 ${symbol} 1` })),
|
||||
];
|
||||
for (const { formula, errorFormula } of formulas) {
|
||||
expect(
|
||||
|
@ -1546,7 +1565,7 @@ invalid: "
|
|||
).toEqual([
|
||||
`The return value type of the operation ${
|
||||
errorFormula ?? formula
|
||||
} is not supported in Formula.`,
|
||||
} is not supported in Formula`,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
@ -1557,8 +1576,14 @@ invalid: "
|
|||
// * field passed
|
||||
// * missing argument
|
||||
const errors = [
|
||||
(operation: string) =>
|
||||
`The first argument for ${operation} should be a operation name. Found ()`,
|
||||
(operation: string) => {
|
||||
const required = tinymathFunctions[operation].positionalArguments.filter(
|
||||
({ optional }) => !optional
|
||||
);
|
||||
return `The operation ${operation} in the Formula is missing ${
|
||||
required.length
|
||||
} arguments: ${required.map(({ name }) => name).join(', ')}`;
|
||||
},
|
||||
(operation: string) => `The operation ${operation} has too many arguments`,
|
||||
(operation: string) => `The operation ${operation} does not accept any field as argument`,
|
||||
(operation: string) => {
|
||||
|
@ -1573,9 +1598,11 @@ invalid: "
|
|||
.join(', ')}`;
|
||||
},
|
||||
];
|
||||
|
||||
const mathFns = Object.keys(tinymathFunctions);
|
||||
// we'll try to map all of these here in this test
|
||||
for (const fn of Object.keys(tinymathFunctions)) {
|
||||
it(`returns an error for the math functions available: ${fn}`, () => {
|
||||
for (const fn of mathFns) {
|
||||
it(`[${fn}] returns an error for the math functions available`, () => {
|
||||
const nArgs = tinymathFunctions[fn].positionalArguments;
|
||||
// start with the first 3 types
|
||||
const formulas = [
|
||||
|
@ -1585,14 +1612,22 @@ invalid: "
|
|||
`${fn}(${Array(nArgs.length).fill('bytes').join(', ')})`,
|
||||
];
|
||||
// add the fourth check only for those functions with more than 1 arg required
|
||||
// and check that this first argument is of type number
|
||||
const enableFourthCheck =
|
||||
nArgs.filter(
|
||||
({ optional, alternativeWhenMissing }) => !optional && !alternativeWhenMissing
|
||||
).length > 1;
|
||||
).length > 1 && nArgs[0]?.type === 'number';
|
||||
if (enableFourthCheck) {
|
||||
formulas.push(`${fn}(1)`);
|
||||
}
|
||||
formulas.forEach((formula, i) => {
|
||||
const finalFormulas = formulas.map((text) => {
|
||||
if (tinymathFunctions[fn].outputType !== 'boolean') {
|
||||
return text;
|
||||
}
|
||||
// for comparison functions wrap the existing formula within the ifelse function
|
||||
return `ifelse(${text}, 1, 0)`;
|
||||
});
|
||||
finalFormulas.forEach((formula, i) => {
|
||||
expect(
|
||||
formulaOperation.getErrorMessage!(
|
||||
getNewLayerWithFormula(formula),
|
||||
|
@ -1600,16 +1635,87 @@ invalid: "
|
|||
indexPattern,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([errors[i](fn)]);
|
||||
).toContain(errors[i](fn));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// comparison tests
|
||||
for (const fn of mathFns.filter((name) => tinymathFunctions[name].section === 'comparison')) {
|
||||
if (tinymathFunctions[fn].outputType === 'boolean') {
|
||||
it(`[${fn}] returns an error about unsupported return type and when partial arguments are passed`, () => {
|
||||
const formulas = [`${fn}()`, `${fn}(1)`];
|
||||
formulas.forEach((formula, nArg) => {
|
||||
const expectedCount = tinymathFunctions[fn].positionalArguments.length - nArg;
|
||||
const expectedArgs = ['left', 'right'].slice(nArg).join(', ');
|
||||
expect(
|
||||
formulaOperation.getErrorMessage!(
|
||||
getNewLayerWithFormula(formula),
|
||||
'col1',
|
||||
indexPattern,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
`The return value type of the operation ${formula} is not supported in Formula`,
|
||||
`The operation ${fn} in the Formula is missing ${expectedCount} arguments: ${expectedArgs}`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const indexReverseMap = {
|
||||
cond: [0],
|
||||
left: [1],
|
||||
right: [2],
|
||||
all: [0, 1, 2],
|
||||
};
|
||||
it.each`
|
||||
cond | left | right | expectedFail
|
||||
${'1'} | ${'2'} | ${'3'} | ${'cond'}
|
||||
${'1 > 1'} | ${'2 > 2'} | ${'3'} | ${'left'}
|
||||
${'1 > 1'} | ${'2'} | ${'3 > 3'} | ${'right'}
|
||||
${'1'} | ${'2 > 2'} | ${'3 > 3'} | ${'all'}
|
||||
${'count()'} | ${'average(bytes)'} | ${'average(bytes)'} | ${'cond'}
|
||||
${'count() > 1'} | ${'average(bytes) > 2'} | ${'average(bytes)'} | ${'left'}
|
||||
${'count() > 1'} | ${'average(bytes)'} | ${'average(bytes) > 3'} | ${'right'}
|
||||
${'count()'} | ${'average(bytes) > 2'} | ${'average(bytes) > 3'} | ${'all'}
|
||||
`(
|
||||
`[${fn}] returns an error if $expectedFail argument is/are of the wrong type: ${fn}($cond, $left, $right)`,
|
||||
({
|
||||
cond,
|
||||
left,
|
||||
right,
|
||||
expectedFail,
|
||||
}: {
|
||||
cond: string;
|
||||
left: string;
|
||||
right: string;
|
||||
expectedFail: keyof typeof indexReverseMap;
|
||||
}) => {
|
||||
const argsSorted = [cond, left, right];
|
||||
expect(
|
||||
formulaOperation.getErrorMessage!(
|
||||
getNewLayerWithFormula(`${fn}(${cond}, ${left}, ${right})`),
|
||||
'col1',
|
||||
indexPattern,
|
||||
operationDefinitionMap
|
||||
)
|
||||
).toEqual(
|
||||
indexReverseMap[expectedFail].map((i) => {
|
||||
const arg = tinymathFunctions[fn].positionalArguments[i];
|
||||
const passedValue = />/.test(argsSorted[i]) ? 'boolean' : 'number';
|
||||
return `The ${arg.name} argument for the operation ${fn} in the Formula is of the wrong type: ${passedValue} instead of ${arg.type}`;
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
it('returns an error suggesting to use an alternative function', () => {
|
||||
const formulas = [`clamp(1)`, 'clamp(1, 5)'];
|
||||
const errorsWithSuggestions = [
|
||||
'The operation clamp in the Formula is missing the min argument: use the pick_max operation instead.',
|
||||
'The operation clamp in the Formula is missing the max argument: use the pick_min operation instead.',
|
||||
'The operation clamp in the Formula is missing the min argument: use the pick_max operation instead',
|
||||
'The operation clamp in the Formula is missing the max argument: use the pick_min operation instead',
|
||||
];
|
||||
formulas.forEach((formula, i) => {
|
||||
expect(
|
||||
|
@ -1648,7 +1754,7 @@ invalid: "
|
|||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
`The Formula filter of type "lucene" is not compatible with the inner filter of type "kql" from the ${operation} operation.`,
|
||||
`The Formula filter of type "lucene" is not compatible with the inner filter of type "kql" from the ${operation} operation`,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
@ -1668,8 +1774,8 @@ invalid: "
|
|||
operationDefinitionMap
|
||||
)
|
||||
).toEqual([
|
||||
`The Formula filter of type "lucene" is not compatible with the inner filter of type "kql" from the count operation.`,
|
||||
`The Formula filter of type "lucene" is not compatible with the inner filter of type "kql" from the sum operation.`,
|
||||
`The Formula filter of type "lucene" is not compatible with the inner filter of type "kql" from the count operation`,
|
||||
`The Formula filter of type "lucene" is not compatible with the inner filter of type "kql" from the sum operation`,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import { runASTValidation, tryToParse } from './validation';
|
|||
import { WrappedFormulaEditor } from './editor';
|
||||
import { insertOrReplaceFormulaColumn } from './parse';
|
||||
import { generateFormula } from './generate';
|
||||
import { filterByVisibleOperation } from './util';
|
||||
import { filterByVisibleOperation, nonNullable } from './util';
|
||||
import { getManagedColumnsFrom } from '../../layer_helpers';
|
||||
import { getFilter, isColumnFormatted } from '../helpers';
|
||||
|
||||
|
@ -77,7 +77,8 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
const errors = runASTValidation(root, layer, indexPattern, visibleOperationsMap, column);
|
||||
|
||||
if (errors.length) {
|
||||
return errors.map(({ message }) => message);
|
||||
// remove duplicates
|
||||
return Array.from(new Set(errors.map(({ message }) => message)));
|
||||
}
|
||||
|
||||
const managedColumns = getManagedColumnsFrom(columnId, layer.columns);
|
||||
|
@ -90,7 +91,7 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
|
|||
}
|
||||
return [];
|
||||
})
|
||||
.filter((marker) => marker);
|
||||
.filter(nonNullable);
|
||||
const hasBuckets = layer.columnOrder.some((colId) => layer.columns[colId].isBucketed);
|
||||
const hasOtherMetrics = layer.columnOrder.some((colId) => {
|
||||
const col = layer.columns[colId];
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
getOperationParams,
|
||||
groupArgsByType,
|
||||
mergeWithGlobalFilters,
|
||||
nonNullable,
|
||||
} from './util';
|
||||
import { FormulaIndexPatternColumn, isFormulaIndexPatternColumn } from './formula';
|
||||
import { getColumnOrder } from '../../layer_helpers';
|
||||
|
@ -89,9 +90,9 @@ function extractColumns(
|
|||
const nodeOperation = operations[node.name];
|
||||
if (!nodeOperation) {
|
||||
// it's a regular math node
|
||||
const consumedArgs = node.args
|
||||
.map(parseNode)
|
||||
.filter((n) => typeof n !== 'undefined' && n !== null) as Array<number | TinymathVariable>;
|
||||
const consumedArgs = node.args.map(parseNode).filter(nonNullable) as Array<
|
||||
number | TinymathVariable
|
||||
>;
|
||||
return {
|
||||
...node,
|
||||
args: consumedArgs,
|
||||
|
|
|
@ -114,12 +114,16 @@ function getTypeI18n(type: string) {
|
|||
if (type === 'string') {
|
||||
return i18n.translate('xpack.lens.formula.string', { defaultMessage: 'string' });
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return i18n.translate('xpack.lens.formula.boolean', { defaultMessage: 'boolean' });
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export const tinymathFunctions: Record<
|
||||
string,
|
||||
{
|
||||
section: 'math' | 'comparison';
|
||||
positionalArguments: Array<{
|
||||
name: string;
|
||||
optional?: boolean;
|
||||
|
@ -129,9 +133,13 @@ export const tinymathFunctions: Record<
|
|||
}>;
|
||||
// Help is in Markdown format
|
||||
help: string;
|
||||
// When omitted defaults to "number".
|
||||
// Used for comparison functions return type
|
||||
outputType?: string;
|
||||
}
|
||||
> = {
|
||||
add: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
|
@ -158,6 +166,7 @@ Example: Offset count by a static value
|
|||
}),
|
||||
},
|
||||
subtract: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
|
@ -179,6 +188,7 @@ Example: Calculate the range of a field
|
|||
}),
|
||||
},
|
||||
multiply: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
|
@ -203,6 +213,7 @@ Example: Calculate price after constant tax rate
|
|||
}),
|
||||
},
|
||||
divide: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
|
@ -226,6 +237,7 @@ Example: \`divide(sum(bytes), 2)\`
|
|||
}),
|
||||
},
|
||||
abs: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -241,6 +253,7 @@ Example: Calculate average distance to sea level \`abs(average(altitude))\`
|
|||
}),
|
||||
},
|
||||
cbrt: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -257,6 +270,7 @@ Example: Calculate side length from volume
|
|||
}),
|
||||
},
|
||||
ceil: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -273,6 +287,7 @@ Example: Round up price to the next dollar
|
|||
}),
|
||||
},
|
||||
clamp: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -305,6 +320,7 @@ clamp(
|
|||
}),
|
||||
},
|
||||
cube: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -321,6 +337,7 @@ Example: Calculate volume from side length
|
|||
}),
|
||||
},
|
||||
exp: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -338,6 +355,7 @@ Example: Calculate the natural exponential function
|
|||
}),
|
||||
},
|
||||
fix: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -354,6 +372,7 @@ Example: Rounding towards zero
|
|||
}),
|
||||
},
|
||||
floor: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -370,6 +389,7 @@ Example: Round down a price
|
|||
}),
|
||||
},
|
||||
log: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -395,6 +415,7 @@ log(sum(bytes), 2)
|
|||
}),
|
||||
},
|
||||
mod: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -415,6 +436,7 @@ Example: Calculate last three digits of a value
|
|||
}),
|
||||
},
|
||||
pow: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -435,6 +457,7 @@ Example: Calculate volume based on side length
|
|||
}),
|
||||
},
|
||||
round: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -460,6 +483,7 @@ round(sum(bytes), 2)
|
|||
}),
|
||||
},
|
||||
sqrt: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -476,6 +500,7 @@ Example: Calculate side length based on area
|
|||
}),
|
||||
},
|
||||
square: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -492,6 +517,7 @@ Example: Calculate area based on side length
|
|||
}),
|
||||
},
|
||||
pick_max: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
|
@ -512,6 +538,7 @@ Example: Find the maximum between two fields averages
|
|||
}),
|
||||
},
|
||||
pick_min: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
|
@ -532,6 +559,7 @@ Example: Find the minimum between two fields averages
|
|||
}),
|
||||
},
|
||||
defaults: {
|
||||
section: 'math',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.value', { defaultMessage: 'value' }),
|
||||
|
@ -548,11 +576,170 @@ Returns a default numeric value when value is null.
|
|||
|
||||
Example: Return -1 when a field has no data
|
||||
\`defaults(average(bytes), -1)\`
|
||||
`,
|
||||
}),
|
||||
},
|
||||
lt: {
|
||||
section: 'comparison',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
],
|
||||
outputType: getTypeI18n('boolean'),
|
||||
help: i18n.translate('xpack.lens.formula.ltFunction.markdown', {
|
||||
defaultMessage: `
|
||||
Performs a lower than comparison between two values.
|
||||
To be used as condition for \`ifelse\` comparison function.
|
||||
Also works with \`<\` symbol.
|
||||
|
||||
Example: Returns true if the average of bytes is lower than the average amount of memory
|
||||
\`average(bytes) <= average(memory)\`
|
||||
|
||||
Example: \`lt(average(bytes), 1000)\`
|
||||
`,
|
||||
}),
|
||||
},
|
||||
gt: {
|
||||
section: 'comparison',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
],
|
||||
outputType: getTypeI18n('boolean'),
|
||||
help: i18n.translate('xpack.lens.formula.gtFunction.markdown', {
|
||||
defaultMessage: `
|
||||
Performs a greater than comparison between two values.
|
||||
To be used as condition for \`ifelse\` comparison function.
|
||||
Also works with \`>\` symbol.
|
||||
|
||||
Example: Returns true if the average of bytes is greater than the average amount of memory
|
||||
\`average(bytes) > average(memory)\`
|
||||
|
||||
Example: \`gt(average(bytes), 1000)\`
|
||||
`,
|
||||
}),
|
||||
},
|
||||
eq: {
|
||||
section: 'comparison',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
],
|
||||
outputType: getTypeI18n('boolean'),
|
||||
help: i18n.translate('xpack.lens.formula.eqFunction.markdown', {
|
||||
defaultMessage: `
|
||||
Performs an equality comparison between two values.
|
||||
To be used as condition for \`ifelse\` comparison function.
|
||||
Also works with \`==\` symbol.
|
||||
|
||||
Example: Returns true if the average of bytes is exactly the same amount of average memory
|
||||
\`average(bytes) == average(memory)\`
|
||||
|
||||
Example: \`eq(sum(bytes), 1000000)\`
|
||||
`,
|
||||
}),
|
||||
},
|
||||
lte: {
|
||||
section: 'comparison',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
],
|
||||
outputType: getTypeI18n('boolean'),
|
||||
help: i18n.translate('xpack.lens.formula.lteFunction.markdown', {
|
||||
defaultMessage: `
|
||||
Performs a lower than or equal comparison between two values.
|
||||
To be used as condition for \`ifelse\` comparison function.
|
||||
Also works with \`<=\` symbol.
|
||||
|
||||
Example: Returns true if the average of bytes is lower than or equal to the average amount of memory
|
||||
\`average(bytes) <= average(memory)\`
|
||||
|
||||
Example: \`lte(average(bytes), 1000)\`
|
||||
`,
|
||||
}),
|
||||
},
|
||||
gte: {
|
||||
section: 'comparison',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
],
|
||||
outputType: getTypeI18n('boolean'),
|
||||
help: i18n.translate('xpack.lens.formula.gteFunction.markdown', {
|
||||
defaultMessage: `
|
||||
Performs a greater than comparison between two values.
|
||||
To be used as condition for \`ifelse\` comparison function.
|
||||
Also works with \`>=\` symbol.
|
||||
|
||||
Example: Returns true if the average of bytes is greater than or equal to the average amount of memory
|
||||
\`average(bytes) >= average(memory)\`
|
||||
|
||||
Example: \`gte(average(bytes), 1000)\`
|
||||
`,
|
||||
}),
|
||||
},
|
||||
ifelse: {
|
||||
section: 'comparison',
|
||||
positionalArguments: [
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.condition', { defaultMessage: 'condition' }),
|
||||
type: getTypeI18n('boolean'),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.left', { defaultMessage: 'left' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.lens.formula.right', { defaultMessage: 'right' }),
|
||||
type: getTypeI18n('number'),
|
||||
},
|
||||
],
|
||||
help: i18n.translate('xpack.lens.formula.ifElseFunction.markdown', {
|
||||
defaultMessage: `
|
||||
Returns a value depending on whether the element of condition is true or false.
|
||||
|
||||
Example: Average revenue per customer but in some cases customer id is not provided which counts as additional customer
|
||||
\`sum(total)/(unique_count(customer_id) + ifelse( count() > count(kql='customer_id:*'), 1, 0))\`
|
||||
`,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export function nonNullable<T>(v: T): v is NonNullable<T> {
|
||||
return v != null;
|
||||
}
|
||||
|
||||
export function isMathNode(node: TinymathAST | string) {
|
||||
return isObject(node) && node.type === 'function' && tinymathFunctions[node.name];
|
||||
}
|
||||
|
@ -562,7 +749,7 @@ export function findMathNodes(root: TinymathAST | string): TinymathFunction[] {
|
|||
if (!isObject(node) || node.type !== 'function' || !isMathNode(node)) {
|
||||
return [];
|
||||
}
|
||||
return [node, ...node.args.flatMap(flattenMathNodes)].filter(Boolean);
|
||||
return [node, ...node.args.flatMap(flattenMathNodes)].filter(nonNullable);
|
||||
}
|
||||
|
||||
return flattenMathNodes(root);
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
getValueOrName,
|
||||
groupArgsByType,
|
||||
isMathNode,
|
||||
nonNullable,
|
||||
tinymathFunctions,
|
||||
} from './util';
|
||||
|
||||
|
@ -45,6 +46,10 @@ interface ValidationErrors {
|
|||
message: string;
|
||||
type: { operation: string; params: string };
|
||||
};
|
||||
wrongTypeArgument: {
|
||||
message: string;
|
||||
type: { operation: string; name: string; type: string; expectedType: string };
|
||||
};
|
||||
wrongFirstArgument: {
|
||||
message: string;
|
||||
type: { operation: string; type: string; argument: string | number };
|
||||
|
@ -109,6 +114,26 @@ export interface ErrorWrapper {
|
|||
severity?: 'error' | 'warning';
|
||||
}
|
||||
|
||||
function getNodeLocation(node: TinymathFunction): TinymathLocation[] {
|
||||
return [node.location].filter(nonNullable);
|
||||
}
|
||||
|
||||
function getArgumentType(arg: TinymathAST, operations: Record<string, GenericOperationDefinition>) {
|
||||
if (!isObject(arg)) {
|
||||
return typeof arg;
|
||||
}
|
||||
if (arg.type === 'function') {
|
||||
if (tinymathFunctions[arg.name]) {
|
||||
return tinymathFunctions[arg.name].outputType ?? 'number';
|
||||
}
|
||||
// Assume it's a number for now
|
||||
if (operations[arg.name]) {
|
||||
return 'number';
|
||||
}
|
||||
}
|
||||
// leave for now other argument types
|
||||
}
|
||||
|
||||
export function isParsingError(message: string) {
|
||||
return message.includes('Failed to parse expression');
|
||||
}
|
||||
|
@ -118,7 +143,7 @@ function findFunctionNodes(root: TinymathAST | string): TinymathFunction[] {
|
|||
if (!isObject(node) || node.type !== 'function') {
|
||||
return [];
|
||||
}
|
||||
return [node, ...node.args.flatMap(flattenFunctionNodes)].filter(Boolean);
|
||||
return [node, ...node.args.flatMap(flattenFunctionNodes)].filter(nonNullable);
|
||||
}
|
||||
|
||||
return flattenFunctionNodes(root);
|
||||
|
@ -132,14 +157,15 @@ export function hasInvalidOperations(
|
|||
return {
|
||||
// avoid duplicates
|
||||
names: Array.from(new Set(nodes.map(({ name }) => name))),
|
||||
locations: nodes.map(({ location }) => location).filter((a) => a) as TinymathLocation[],
|
||||
locations: nodes.map(({ location }) => location).filter(nonNullable),
|
||||
};
|
||||
}
|
||||
|
||||
export const getRawQueryValidationError = (text: string, operations: Record<string, unknown>) => {
|
||||
// try to extract the query context here
|
||||
const singleLine = text.split('\n').join('');
|
||||
const allArgs = singleLine.split(',').filter((args) => /(kql|lucene)/.test(args));
|
||||
const languagesRegexp = /(kql|lucene)/;
|
||||
const allArgs = singleLine.split(',').filter((args) => languagesRegexp.test(args));
|
||||
// check for the presence of a valid ES operation
|
||||
const containsOneValidOperation = Object.keys(operations).some((operation) =>
|
||||
singleLine.includes(operation)
|
||||
|
@ -153,7 +179,7 @@ export const getRawQueryValidationError = (text: string, operations: Record<stri
|
|||
// For instance: count(kql=...) + count(lucene=...) - count(kql=...)
|
||||
// therefore before partition them, split them by "count" keywork and filter only string with a length
|
||||
const flattenArgs = allArgs.flatMap((arg) =>
|
||||
arg.split('count').filter((subArg) => /(kql|lucene)/.test(subArg))
|
||||
arg.split('count').filter((subArg) => languagesRegexp.test(subArg))
|
||||
);
|
||||
const [kqlQueries, luceneQueries] = partition(flattenArgs, (arg) => /kql/.test(arg));
|
||||
const errors = [];
|
||||
|
@ -260,6 +286,18 @@ function getMessageFromId<K extends ErrorTypes>({
|
|||
values: { operation: out.operation, params: out.params },
|
||||
});
|
||||
break;
|
||||
case 'wrongTypeArgument':
|
||||
message = i18n.translate('xpack.lens.indexPattern.formulaExpressionWrongTypeArgument', {
|
||||
defaultMessage:
|
||||
'The {name} argument for the operation {operation} in the Formula is of the wrong type: {type} instead of {expectedType}',
|
||||
values: {
|
||||
operation: out.operation,
|
||||
name: out.name,
|
||||
type: out.type,
|
||||
expectedType: out.expectedType,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'duplicateArgument':
|
||||
message = i18n.translate('xpack.lens.indexPattern.formulaOperationDuplicateParams', {
|
||||
defaultMessage:
|
||||
|
@ -332,21 +370,20 @@ function getMessageFromId<K extends ErrorTypes>({
|
|||
break;
|
||||
case 'wrongReturnedType':
|
||||
message = i18n.translate('xpack.lens.indexPattern.formulaOperationWrongReturnedType', {
|
||||
defaultMessage:
|
||||
'The return value type of the operation {text} is not supported in Formula.',
|
||||
defaultMessage: 'The return value type of the operation {text} is not supported in Formula',
|
||||
values: { text: out.text },
|
||||
});
|
||||
break;
|
||||
case 'filtersTypeConflict':
|
||||
message = i18n.translate('xpack.lens.indexPattern.formulaOperationFiltersTypeConflicts', {
|
||||
defaultMessage:
|
||||
'The Formula filter of type "{outerType}" is not compatible with the inner filter of type "{innerType}" from the {operation} operation.',
|
||||
'The Formula filter of type "{outerType}" is not compatible with the inner filter of type "{innerType}" from the {operation} operation',
|
||||
values: { operation: out.operation, outerType: out.outerType, innerType: out.innerType },
|
||||
});
|
||||
break;
|
||||
case 'useAlternativeFunction':
|
||||
message = i18n.translate('xpack.lens.indexPattern.formulaUseAlternative', {
|
||||
defaultMessage: `The operation {operation} in the Formula is missing the {params} argument: use the {alternativeFn} operation instead.`,
|
||||
defaultMessage: `The operation {operation} in the Formula is missing the {params} argument: use the {alternativeFn} operation instead`,
|
||||
values: { operation: out.operation, params: out.params, alternativeFn: out.alternativeFn },
|
||||
});
|
||||
break;
|
||||
|
@ -404,6 +441,7 @@ export function runASTValidation(
|
|||
) {
|
||||
return [
|
||||
...checkMissingVariableOrFunctions(ast, layer, indexPattern, operations),
|
||||
...checkTopNodeReturnType(ast),
|
||||
...runFullASTValidation(ast, layer, indexPattern, operations, currentColumn),
|
||||
];
|
||||
}
|
||||
|
@ -548,7 +586,7 @@ function validateFiltersArguments(
|
|||
innerType,
|
||||
outerType,
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -574,7 +612,7 @@ function validateNameArguments(
|
|||
operation: node.name,
|
||||
params: missingParams.map(({ name }) => name).join(', '),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -587,7 +625,7 @@ function validateNameArguments(
|
|||
operation: node.name,
|
||||
params: wrongTypeParams.map(({ name }) => name).join(', '),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -600,7 +638,7 @@ function validateNameArguments(
|
|||
operation: node.name,
|
||||
params: duplicateParams.join(', '),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -614,13 +652,34 @@ function validateNameArguments(
|
|||
getMessageFromId({
|
||||
messageId: 'tooManyQueries',
|
||||
values: {},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
const DEFAULT_RETURN_TYPE = 'number';
|
||||
function checkTopNodeReturnType(ast: TinymathAST): ErrorWrapper[] {
|
||||
if (
|
||||
isObject(ast) &&
|
||||
ast.type === 'function' &&
|
||||
ast.text &&
|
||||
(tinymathFunctions[ast.name]?.outputType || DEFAULT_RETURN_TYPE) !== DEFAULT_RETURN_TYPE
|
||||
) {
|
||||
return [
|
||||
getMessageFromId({
|
||||
messageId: 'wrongReturnedType',
|
||||
values: {
|
||||
text: ast.text,
|
||||
},
|
||||
locations: getNodeLocation(ast),
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function runFullASTValidation(
|
||||
ast: TinymathAST,
|
||||
layer: FormBasedLayer,
|
||||
|
@ -645,7 +704,7 @@ function runFullASTValidation(
|
|||
const [firstArg] = node?.args || [];
|
||||
|
||||
if (!nodeOperation) {
|
||||
errors.push(...validateMathNodes(node, missingVariablesSet));
|
||||
errors.push(...validateMathNodes(node, missingVariablesSet, operations));
|
||||
// carry on with the validation for all the functions within the math operation
|
||||
if (functions?.length) {
|
||||
return errors.concat(functions.flatMap((fn) => validateNode(fn)));
|
||||
|
@ -664,7 +723,7 @@ function runFullASTValidation(
|
|||
}),
|
||||
argument: `math operation`,
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
@ -683,7 +742,7 @@ function runFullASTValidation(
|
|||
defaultMessage: 'no field',
|
||||
}),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -716,7 +775,7 @@ function runFullASTValidation(
|
|||
values: {
|
||||
operation: node.name,
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
@ -742,7 +801,8 @@ function runFullASTValidation(
|
|||
// In general this should be handled down the Esaggs route rather than here
|
||||
const isFirstArgumentNotValid = Boolean(
|
||||
!isArgumentValidType(firstArg, 'function') ||
|
||||
(isMathNode(firstArg) && validateMathNodes(firstArg, missingVariablesSet).length)
|
||||
(isMathNode(firstArg) &&
|
||||
validateMathNodes(firstArg, missingVariablesSet, operations).length)
|
||||
);
|
||||
// First field has a special handling
|
||||
if (isFirstArgumentNotValid) {
|
||||
|
@ -760,7 +820,7 @@ function runFullASTValidation(
|
|||
defaultMessage: 'no operation',
|
||||
}),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -786,7 +846,7 @@ function runFullASTValidation(
|
|||
values: {
|
||||
operation: node.name,
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
@ -946,22 +1006,27 @@ export function isArgumentValidType(arg: TinymathAST | string, type: TinymathNod
|
|||
return isObject(arg) && arg.type === type;
|
||||
}
|
||||
|
||||
export function validateMathNodes(root: TinymathAST, missingVariableSet: Set<string>) {
|
||||
export function validateMathNodes(
|
||||
root: TinymathAST,
|
||||
missingVariableSet: Set<string>,
|
||||
operations: Record<string, GenericOperationDefinition>
|
||||
) {
|
||||
const mathNodes = findMathNodes(root);
|
||||
const errors: ErrorWrapper[] = [];
|
||||
mathNodes.forEach((node: TinymathFunction) => {
|
||||
const { positionalArguments } = tinymathFunctions[node.name];
|
||||
const mandatoryArguments = positionalArguments.filter(({ optional }) => !optional);
|
||||
if (!node.args.length) {
|
||||
// we can stop here
|
||||
return errors.push(
|
||||
getMessageFromId({
|
||||
messageId: 'wrongFirstArgument',
|
||||
messageId: 'missingMathArgument',
|
||||
values: {
|
||||
operation: node.name,
|
||||
type: 'operation',
|
||||
argument: `()`,
|
||||
count: mandatoryArguments.length,
|
||||
params: mandatoryArguments.map(({ name }) => name).join(', '),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -973,12 +1038,12 @@ export function validateMathNodes(root: TinymathAST, missingVariableSet: Set<str
|
|||
values: {
|
||||
operation: node.name,
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// no need to iterate all the arguments, one field is anough to trigger the error
|
||||
// no need to iterate all the arguments, one field is enough to trigger the error
|
||||
const hasFieldAsArgument = positionalArguments.some((requirements, index) => {
|
||||
const arg = node.args[index];
|
||||
if (arg != null && typeof arg !== 'number') {
|
||||
|
@ -992,12 +1057,11 @@ export function validateMathNodes(root: TinymathAST, missingVariableSet: Set<str
|
|||
values: {
|
||||
operation: node.name,
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const mandatoryArguments = positionalArguments.filter(({ optional }) => !optional);
|
||||
// if there is only 1 mandatory arg, this is already handled by the wrongFirstArgument check
|
||||
if (mandatoryArguments.length > 1 && node.args.length < mandatoryArguments.length) {
|
||||
const missingArgs = mandatoryArguments.filter((_, i) => node.args[i] == null);
|
||||
|
@ -1020,7 +1084,7 @@ export function validateMathNodes(root: TinymathAST, missingVariableSet: Set<str
|
|||
count: mandatoryArguments.length - node.args.length,
|
||||
params: missingArgsWithoutAlternative.map(({ name }) => name).join(', '),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -1035,11 +1099,37 @@ export function validateMathNodes(root: TinymathAST, missingVariableSet: Set<str
|
|||
params: firstArg.name,
|
||||
alternativeFn: firstArg.alternativeWhenMissing,
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const wrongTypeArgumentIndexes = positionalArguments
|
||||
.map(({ type }, index) => {
|
||||
const arg = node.args[index];
|
||||
if (arg != null) {
|
||||
const argType = getArgumentType(arg, operations);
|
||||
if (argType && argType !== type) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(nonNullable);
|
||||
for (const wrongTypeArgumentIndex of wrongTypeArgumentIndexes) {
|
||||
const arg = node.args[wrongTypeArgumentIndex];
|
||||
errors.push(
|
||||
getMessageFromId({
|
||||
messageId: 'wrongTypeArgument',
|
||||
values: {
|
||||
operation: node.name,
|
||||
name: positionalArguments[wrongTypeArgumentIndex].name,
|
||||
type: getArgumentType(arg, operations) || 'number',
|
||||
expectedType: positionalArguments[wrongTypeArgumentIndex].type || '',
|
||||
},
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
@ -1073,7 +1163,7 @@ function validateFieldArguments(
|
|||
supported: 1,
|
||||
text: (fields as TinymathVariable[]).map(({ text }) => text).join(', '),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -1085,7 +1175,7 @@ function validateFieldArguments(
|
|||
values: {
|
||||
text: node.text ?? `${node.name}(${getValueOrName(firstArg)})`,
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -1101,7 +1191,7 @@ function validateFieldArguments(
|
|||
defaultMessage: 'field',
|
||||
}),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -1133,7 +1223,7 @@ function validateFunctionArguments(
|
|||
defaultMessage: 'metric',
|
||||
}),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
|
@ -1146,7 +1236,7 @@ function validateFunctionArguments(
|
|||
supported: requiredFunctions,
|
||||
text: (esOperations as TinymathFunction[]).map(({ text }) => text).join(', '),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -1164,7 +1254,7 @@ function validateFunctionArguments(
|
|||
type,
|
||||
text: (mathOperations as TinymathFunction[]).map(({ text }) => text).join(', '),
|
||||
},
|
||||
locations: node.location ? [node.location] : [],
|
||||
locations: getNodeLocation(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue