This repository has been archived by the owner on Dec 10, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(core): move mexp from plugins to core (#1371)
* refactor(core): move mexp from plugins to core * remove test nest
- Loading branch information
Showing
9 changed files
with
198 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import mexp from 'math-expression-evaluator'; | ||
|
||
const REPLACE_OPERATORS: [RegExp, string][] = [ | ||
[new RegExp(/==/g), 'Eq'], | ||
[new RegExp(/>=/g), 'Gte'], | ||
[new RegExp(/<=/g), 'Lte'], | ||
[new RegExp(/>/g), 'Gt'], | ||
[new RegExp(/</g), 'Lt'], | ||
]; | ||
|
||
const TOKENS = [ | ||
{ | ||
type: 3, | ||
token: 'x', | ||
show: 'x', | ||
value: 'x', | ||
}, | ||
{ | ||
type: 2, | ||
token: '&', | ||
show: '&', | ||
value: (a: number, b: number): number => a & b, | ||
}, | ||
{ | ||
type: 2, | ||
token: '|', | ||
show: '|', | ||
value: (a: number, b: number): number => a | b, | ||
}, | ||
{ | ||
type: 2, | ||
token: 'and', | ||
show: 'and', | ||
value: (a: number, b: number): number => a && b, | ||
}, | ||
{ | ||
type: 2, | ||
token: 'xor', | ||
show: 'xor', | ||
value: (a: number, b: number): number => a ^ b, | ||
}, | ||
{ | ||
type: 2, | ||
token: 'or', | ||
show: 'or', | ||
value: (a: number, b: number): number => Number(a || b), | ||
}, | ||
{ | ||
type: 2, | ||
token: 'Eq', | ||
show: 'Eq', | ||
value: (a: number, b: number): number => Number(a === b), | ||
}, | ||
{ | ||
type: 2, | ||
token: 'Lt', | ||
show: 'Lt', | ||
value: (a: number, b: number): number => Number(a < b), | ||
}, | ||
{ | ||
type: 2, | ||
token: 'Lte', | ||
show: 'Lte', | ||
value: (a: number, b: number): number => Number(a <= b), | ||
}, | ||
{ | ||
type: 2, | ||
token: 'Gt', | ||
show: 'Gt', | ||
value: (a: number, b: number): number => Number(a > b), | ||
}, | ||
{ | ||
type: 2, | ||
token: 'Gte', | ||
show: 'Gte', | ||
value: (a: number, b: number): number => Number(a >= b), | ||
}, | ||
]; | ||
|
||
export function evalExpression(expression: string, value: number): number { | ||
let parsedExpression = expression; | ||
// replace `<` with `Lt` (and others) to avoid clashes with builtin function operators | ||
// that are not needed in Superset. | ||
REPLACE_OPERATORS.forEach(([key, value]) => { | ||
parsedExpression = parsedExpression.replace(key, value); | ||
}); | ||
const subExpressions = String(parsedExpression).split('='); | ||
parsedExpression = subExpressions[1] ?? subExpressions[0]; | ||
// we can ignore the type requirement on `TOKENS`, as value is always `number` | ||
// and doesn't need to consider `number | underfined`. | ||
// @ts-ignore | ||
return Number(mexp.eval(parsedExpression, TOKENS, { x: value })); | ||
} | ||
|
||
export function isValidExpression(expression: string): boolean { | ||
try { | ||
evalExpression(expression, 0); | ||
} catch (err) { | ||
return false; | ||
} | ||
return true; | ||
} |
79 changes: 79 additions & 0 deletions
79
packages/superset-ui-core/test/math-expression/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { evalExpression, isValidExpression } from '@superset-ui/core/src/math-expression'; | ||
|
||
test('evalExpression evaluates constants correctly', () => { | ||
expect(evalExpression('0', 10)).toEqual(0); | ||
expect(evalExpression('0.123456', 0)).toEqual(0.123456); | ||
expect(evalExpression('789', 100)).toEqual(789); | ||
}); | ||
|
||
test('evalExpression evaluates infinities correctly', () => { | ||
const formula = 'x/0'; | ||
expect(evalExpression(formula, 1)).toEqual(Infinity); | ||
expect(evalExpression(formula, -1)).toEqual(-Infinity); | ||
}); | ||
|
||
test('evalExpression evaluates powers correctly', () => { | ||
const formula = '2^(x/2)*100'; | ||
expect(evalExpression(formula, 0)).toEqual(100); | ||
expect(evalExpression(formula, 1)).toEqual(141.4213562373095); | ||
expect(evalExpression(formula, 2)).toEqual(200); | ||
}); | ||
|
||
test('evalExpression ignores whitespace and variables on left hand side of equals sign', () => { | ||
expect(evalExpression('y=x+1', 1)).toEqual(2); | ||
expect(evalExpression('y = x - 1', 1)).toEqual(0); | ||
}); | ||
|
||
test('evalExpression evaluates custom operators correctly', () => { | ||
const equalsExpression = 'x == 10'; | ||
expect(evalExpression(equalsExpression, 5)).toEqual(0); | ||
expect(evalExpression(equalsExpression, 10)).toEqual(1); | ||
expect(evalExpression(equalsExpression, 10.1)).toEqual(0); | ||
|
||
const closedRange = '(x > 0) and (x < 10)'; | ||
expect(evalExpression(closedRange, 0)).toEqual(0); | ||
expect(evalExpression(closedRange, 5)).toEqual(1); | ||
expect(evalExpression(closedRange, 10)).toEqual(0); | ||
|
||
const openRange = '(x >= 0) and (x <= 10)'; | ||
expect(evalExpression(openRange, -0.1)).toEqual(0); | ||
expect(evalExpression(openRange, 0)).toEqual(1); | ||
expect(evalExpression(openRange, 5)).toEqual(1); | ||
expect(evalExpression(openRange, 10)).toEqual(1); | ||
expect(evalExpression(openRange, 10.1)).toEqual(0); | ||
|
||
const orRange = '(x < 0) or (x > 10)'; | ||
expect(evalExpression(orRange, -0.1)).toEqual(1); | ||
expect(evalExpression(orRange, 0)).toEqual(0); | ||
expect(evalExpression(orRange, 5)).toEqual(0); | ||
expect(evalExpression(orRange, 10)).toEqual(0); | ||
expect(evalExpression(orRange, 10.1)).toEqual(1); | ||
|
||
// other less used operators | ||
expect(evalExpression('5 & x', 3)).toEqual(1); | ||
expect(evalExpression('5 | x', 3)).toEqual(7); | ||
expect(evalExpression('5 xor x', 2)).toEqual(7); | ||
|
||
// complex combinations | ||
const complexExpression = '20.51*(x<1577836800000)+20.2((x<15805152000000)&(x>=1577836800000))'; | ||
expect(evalExpression(complexExpression, 0)).toEqual(20.51); | ||
expect(evalExpression(complexExpression, 1000)).toEqual(20.51); | ||
expect(evalExpression(complexExpression, 1577836800000)).toEqual(20.2); | ||
expect(evalExpression(complexExpression, 15805151999999)).toEqual(20.2); | ||
expect(evalExpression(complexExpression, 15805152000000)).toEqual(0); | ||
expect(evalExpression(complexExpression, 15805159000000)).toEqual(0); | ||
}); | ||
|
||
test('isValidExpression correctly identifies invalid formulas', () => { | ||
expect(isValidExpression('foobar')).toEqual(false); | ||
expect(isValidExpression('x+')).toEqual(false); | ||
expect(isValidExpression('z+1')).toEqual(false); | ||
}); | ||
|
||
test('isValidExpression correctly identifies valid formulas', () => { | ||
expect(isValidExpression('x')).toEqual(true); | ||
expect(isValidExpression('x+1')).toEqual(true); | ||
expect(isValidExpression('y=x-1')).toEqual(true); | ||
expect(isValidExpression('y = x - 1')).toEqual(true); | ||
expect(isValidExpression('y = (x < 100 and x > 0) * 100')).toEqual(true); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
eae9acd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs: