Skip to content

Commit

Permalink
[Lens] Add conditional operations in Formula (#142325)
Browse files Browse the repository at this point in the history
* ✨ 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 <[email protected]>
Co-authored-by: Joe Reuter <[email protected]>
  • Loading branch information
3 people authored Oct 12, 2022
1 parent e5cebd8 commit 757ab76
Show file tree
Hide file tree
Showing 67 changed files with 1,403 additions and 226 deletions.
5 changes: 4 additions & 1 deletion packages/kbn-tinymath/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
137 changes: 137 additions & 0 deletions packages/kbn-tinymath/docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.&lt;number&gt;</code> | a number or an array of numbers |
| b | <code>number</code> \| <code>Array.&lt;number&gt;</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.&lt;number&gt;</code> | a number or an array of numbers |
| b | <code>number</code> \| <code>Array.&lt;number&gt;</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.&lt;number&gt;</code> | a number or an array of numbers |
| b | <code>number</code> \| <code>Array.&lt;number&gt;</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.&lt;any&gt;</code> | a value or an array of any values |
| b | <code>any</code> \| <code>Array.&lt;any&gt;</code> | a value or an array of any values |

**Returns**: <code>any</code> \| <code>Array.&lt;any&gt;</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.&lt;number&gt;</code> | a number or an array of numbers |
| b | <code>number</code> \| <code>Array.&lt;number&gt;</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.&lt;number&gt;</code> | a number or an array of numbers |
| b | <code>number</code> \| <code>Array.&lt;number&gt;</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.

Expand Down
80 changes: 58 additions & 22 deletions packages/kbn-tinymath/grammar/grammar.peggy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions packages/kbn-tinymath/src/functions/abs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
3 changes: 1 addition & 2 deletions packages/kbn-tinymath/src/functions/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -35,3 +33,4 @@ function add(...args) {
return result + current;
});
}
module.exports = { add };
4 changes: 2 additions & 2 deletions packages/kbn-tinymath/src/functions/cbrt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
4 changes: 2 additions & 2 deletions packages/kbn-tinymath/src/functions/ceil.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
4 changes: 2 additions & 2 deletions packages/kbn-tinymath/src/functions/clamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -73,3 +71,5 @@ function clamp(a, min, max) {

return findClamp(a, min, max);
}

module.exports = { clamp };
39 changes: 39 additions & 0 deletions packages/kbn-tinymath/src/functions/comparison/eq.js
Original file line number Diff line number Diff line change
@@ -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 };
Loading

0 comments on commit 757ab76

Please sign in to comment.