forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replaced MathJS with Tinymath (elastic#321)
* Replaced mathjs with tinymath in math.js. Wrote unit tests for math.js. Upgraded tinymath * Added string and bool columns to testTable for math.js tests. Added unit tests for math.js * Added columns to testTable in math.js test file * Upgraded tinymath to version 0.1.8 * Created function to convert datable to math context for math.js * Replaced pivotObjectArray with datableToMathContext * Wrote unit tests for math.js * Fixed unit test for math.js. Removed references to mathjs package in math.js * Uncommented line to run server tests * Refactored math.js test file * Replaced every instance of Object.value with value from lodash * Create test file for pointseries. Wrote unit tests for pointseries. * Added unit tests for pointseries * Moved 'server/functions/pointseries.js' to 'server/functions/pointseries/index.js'. Updated unit tests. Created lib folder for pointseries helper functions * Removed map function from lodash * Added unit test for pointseries.js * Refactored pointseries and replaced mathJS functions with tinymath functions * Removed common/lib/math.js. Replaced reference to math.js with tinymath in handlebars.js. * Replaced mathjs with tinymath in datacolumn.js * Refactored pointseries.js. Replaced mathjs with tinymath. Removed unused lodash functions * Removed mathjs package and generated docs * Fixed unit tests. Fixed getType function in math.js * Moved getFormObject function to a separate file. Imported getFormObject into datacolumn.js. Rewrote getFormObject to work with parse from tinymath * Rewrote getFormObject to work with parse from tinymath. Wrote unit tests for getFormObject * Added unit test to test runtime of pointseries using demodata * Updated demodata test * Removed console logs * Updated runtime test for pointseries * Removed runtime test for pointseries * Added error handling for empty datatables in math.js. Updated unit tests * Upgraded tinymath to 0.1.9 * Changed error message thrown in getFormObject, updated unit tests * Created test datatables file * Cleaned up tests for math.js. * Wrote tests for datatableToMathContext * Cleaned up unit tests for pointseries * wrote tests for isColumnReference * Renamed getType in pointseries to getExpressionType * Pulled out functions out of pointseries into lib functions and added imports to pointseries * Wrote unit tests for getFieldType * Wrote unit tests for getFieldNames * Wrote unit tests for getExpressionType. Refactored pointseries. Updated unit tests for pointseries helper functions. * Removed mode from dropdown menu in SimpleMathFunction * Cleaned up unit tests for pointseries and helper functions
- Loading branch information
Showing
22 changed files
with
629 additions
and
130 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
const emptyTable = { | ||
type: 'datatable', | ||
columns: [], | ||
rows: [], | ||
}; | ||
|
||
const testTable = { | ||
type: 'datatable', | ||
columns: [ | ||
{ | ||
name: 'name', | ||
type: 'string', | ||
}, | ||
{ | ||
name: 'time', | ||
type: 'date', | ||
}, | ||
{ | ||
name: 'price', | ||
type: 'number', | ||
}, | ||
{ | ||
name: 'quantity', | ||
type: 'number', | ||
}, | ||
{ | ||
name: 'in_stock', | ||
type: 'boolean', | ||
}, | ||
], | ||
rows: [ | ||
{ | ||
name: 'product1', | ||
time: 1517842800950, //05 Feb 2018 15:00:00 GMT | ||
price: 605, | ||
quantity: 100, | ||
in_stock: true, | ||
}, | ||
{ | ||
name: 'product1', | ||
time: 1517929200950, //06 Feb 2018 15:00:00 GMT | ||
price: 583, | ||
quantity: 200, | ||
in_stock: true, | ||
}, | ||
{ | ||
name: 'product1', | ||
time: 1518015600950, //07 Feb 2018 15:00:00 GMT | ||
price: 420, | ||
quantity: 300, | ||
in_stock: true, | ||
}, | ||
{ | ||
name: 'product2', | ||
time: 1517842800950, //05 Feb 2018 15:00:00 GMT | ||
price: 216, | ||
quantity: 350, | ||
in_stock: false, | ||
}, | ||
{ | ||
name: 'product2', | ||
time: 1517929200950, //06 Feb 2018 15:00:00 GMT | ||
price: 200, | ||
quantity: 256, | ||
in_stock: false, | ||
}, | ||
{ | ||
name: 'product2', | ||
time: 1518015600950, //07 Feb 2018 15:00:00 GMT | ||
price: 190, | ||
quantity: 231, | ||
in_stock: false, | ||
}, | ||
{ | ||
name: 'product3', | ||
time: 1517842800950, //05 Feb 2018 15:00:00 GMT | ||
price: 67, | ||
quantity: 240, | ||
in_stock: true, | ||
}, | ||
{ | ||
name: 'product4', | ||
time: 1517842800950, //05 Feb 2018 15:00:00 GMT | ||
price: 311, | ||
quantity: 447, | ||
in_stock: false, | ||
}, | ||
{ | ||
name: 'product5', | ||
time: 1517842800950, //05 Feb 2018 15:00:00 GMT | ||
price: 288, | ||
quantity: 384, | ||
in_stock: true, | ||
}, | ||
], | ||
}; | ||
|
||
export { emptyTable, testTable }; |
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 |
---|---|---|
@@ -1,21 +1,89 @@ | ||
import expect from 'expect.js'; | ||
import { math } from '../math.js'; | ||
import { emptyTable, testTable } from './fixtures/test_tables'; | ||
|
||
describe('math', () => { | ||
const fn = math().fn; | ||
describe('spec', () => { | ||
it('is a function', () => { | ||
expect(math).to.be.a('function'); | ||
}); | ||
}); | ||
|
||
describe('function', () => { | ||
let fn; | ||
beforeEach(() => { | ||
fn = math().fn; | ||
}); | ||
|
||
it('is a function', () => { | ||
expect(fn).to.be.a('function'); | ||
}); | ||
|
||
it('math expressions, no context', () => { | ||
expect(fn(null, { _: '10' })).to.be.equal(10); | ||
expect(fn(null, { _: '10.5345' })).to.be.equal(10.5345); | ||
expect(fn(null, { _: '123 + 456' })).to.be.equal(579); | ||
expect(fn(null, { _: '100 - 46' })).to.be.equal(54); | ||
expect(fn(1, { _: '100 / 5' })).to.be.equal(20); | ||
expect(fn('foo', { _: '100 / 5' })).to.be.equal(20); | ||
expect(fn(true, { _: '100 / 5' })).to.be.equal(20); | ||
expect(fn(testTable, { _: '100 * 5' })).to.be.equal(500); | ||
expect(fn(emptyTable, { _: '100 * 5' })).to.be.equal(500); | ||
}); | ||
|
||
it('math expressions, context as number', () => { | ||
expect(fn(23.23, { _: 'floor(value)' })).to.be.equal(23); | ||
expect(fn(-103, { _: 'abs(value)' })).to.be.equal(103); | ||
}); | ||
|
||
it('math expressions, context as datatable', () => { | ||
expect(fn(testTable, { _: 'count(price)' })).to.be.equal(9); | ||
expect(fn(testTable, { _: 'sum(quantity)' })).to.be.equal(2508); | ||
expect(fn(testTable, { _: 'mean(price)' })).to.be.equal(320); | ||
expect(fn(testTable, { _: 'min(price)' })).to.be.equal(67); | ||
expect(fn(testTable, { _: 'median(quantity)' })).to.be.equal(256); | ||
expect(fn(testTable, { _: 'max(price)' })).to.be.equal(605); | ||
}); | ||
}); | ||
|
||
describe('invalid expression', () => { | ||
it('throws when expression evaluates to an array', () => { | ||
expect(fn) | ||
.withArgs(testTable, { _: 'multiply(price, 2)' }) | ||
.to.throwException(e => { | ||
expect(e.message).to.be( | ||
'Expressions must return a single number. Try wrapping your expression in mean() or sum()' | ||
); | ||
}); | ||
}); | ||
it('throws when using an unknown context variable', () => { | ||
expect(fn) | ||
.withArgs(testTable, { _: 'sum(foo)' }) | ||
.to.throwException(e => { | ||
expect(e.message).to.be('Unknown variable: foo'); | ||
}); | ||
}); | ||
it('throws when using non-numeric data', () => { | ||
expect(fn) | ||
.withArgs(testTable, { _: 'mean(name)' }) | ||
.to.throwException(e => { | ||
expect(e.message).to.be('Unknown variable: name'); | ||
}); | ||
expect(fn) | ||
.withArgs(testTable, { _: 'max(in_stock)' }) | ||
.to.throwException(e => { | ||
expect(e.message).to.be('Unknown variable: in_stock'); | ||
}); | ||
}); | ||
it('throws when missing expression', () => { | ||
expect(fn) | ||
.withArgs(testTable, { _: '' }) | ||
.to.throwException(e => { | ||
expect(e.message).to.be('Empty expression'); | ||
}); | ||
}); | ||
it('throws when passing a context variable from an empty datatable', () => { | ||
expect(fn) | ||
.withArgs(emptyTable, { _: 'mean(foo)' }) | ||
.to.throwException(e => { | ||
expect(e.message).to.be('Empty datatable'); | ||
}); | ||
}); | ||
}); | ||
}); |
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 |
---|---|---|
@@ -1,31 +1,43 @@ | ||
import { map } from 'lodash'; | ||
import mathjs from 'mathjs'; | ||
import { pivotObjectArray } from '../lib/pivot_object_array.js'; | ||
import { evaluate } from 'tinymath'; | ||
import { datatableToMathContext } from '../lib/datatable_to_math_context.js'; | ||
|
||
export const math = () => ({ | ||
name: 'math', | ||
type: 'number', | ||
help: | ||
'Interpret a mathJS expression, with a number or datatable as context. Datatable columns are available by their column name. ' + | ||
'Interpret a math expression, with a number or datatable as context. Datatable columns are available by their column name. ' + | ||
'If you pass in a number it is available as "value" (without the quotes)', | ||
context: { | ||
types: ['number', 'datatable'], | ||
}, | ||
args: { | ||
_: { | ||
types: ['string'], | ||
help: | ||
'An evaluated MathJS expression. (See http://mathjs.org/docs/expressions/parsing.html#eval)', | ||
help: 'An evaluated tinymath expression. (See https://github.com/elastic/tinymath)', | ||
}, | ||
}, | ||
fn: (context, args) => { | ||
if (args._.trim() === '') { | ||
throw new Error('Empty expression'); | ||
} | ||
const isDatatable = context && context.type === 'datatable'; | ||
const mathContext = isDatatable | ||
? pivotObjectArray(context.rows, map(context.columns, 'name')) | ||
: { value: context }; | ||
const result = mathjs.eval(args._, mathContext); | ||
if (typeof result !== 'number') | ||
throw new Error('Failed to execute math expression. Check your column names'); | ||
return result; | ||
const mathContext = isDatatable ? datatableToMathContext(context) : { value: context }; | ||
try { | ||
const result = evaluate(args._, mathContext); | ||
if (Array.isArray(result)) { | ||
throw new Error( | ||
'Expressions must return a single number. Try wrapping your expression in mean() or sum()' | ||
); | ||
} | ||
if (typeof result !== 'number') | ||
throw new Error('Failed to execute math expression. Check your column names'); | ||
return result; | ||
} catch (e) { | ||
if (context.rows.length === 0) { | ||
throw new Error('Empty datatable'); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
}, | ||
}); |
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,14 @@ | ||
import expect from 'expect.js'; | ||
import { datatableToMathContext } from '../datatable_to_math_context'; | ||
import { emptyTable, testTable } from '../../functions/__tests__/fixtures/test_tables'; | ||
describe('datatableToMathContext', () => { | ||
it('empty table', () => { | ||
expect(datatableToMathContext(emptyTable)).to.be.eql({}); | ||
}); | ||
it('filters out non-numeric columns and pivots datatable', () => { | ||
expect(datatableToMathContext(testTable)).to.be.eql({ | ||
price: [605, 583, 420, 216, 200, 190, 67, 311, 288], | ||
quantity: [100, 200, 300, 350, 256, 231, 240, 447, 384], | ||
}); | ||
}); | ||
}); |
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,17 @@ | ||
import expect from 'expect.js'; | ||
import { getFieldType } from '../get_field_type'; | ||
import { emptyTable, testTable } from '../../functions/__tests__/fixtures/test_tables'; | ||
|
||
describe('getFieldType', () => { | ||
it('returns type of a field in a datatable', () => { | ||
expect(getFieldType(testTable.columns, 'name')).to.be('string'); | ||
expect(getFieldType(testTable.columns, 'time')).to.be('date'); | ||
expect(getFieldType(testTable.columns, 'price')).to.be('number'); | ||
expect(getFieldType(testTable.columns, 'quantity')).to.be('number'); | ||
expect(getFieldType(testTable.columns, 'in_stock')).to.be('boolean'); | ||
}); | ||
it(`returns 'null' if field does not exist in datatable`, () => { | ||
expect(getFieldType(testTable.columns, 'foo')).to.be('null'); | ||
expect(getFieldType(emptyTable.columns, 'foo')).to.be('null'); | ||
}); | ||
}); |
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,7 @@ | ||
import { pivotObjectArray } from '../lib/pivot_object_array.js'; | ||
|
||
// filters columns with type number and passes filtered columns to pivotObjectArray | ||
export function datatableToMathContext(datatable) { | ||
const filteredColumns = datatable.columns.filter(col => col.type === 'number'); | ||
return pivotObjectArray(datatable.rows, filteredColumns.map(col => col.name)); | ||
} |
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,7 @@ | ||
// put in common | ||
|
||
export function getFieldType(columns, field) { | ||
if (!field) return 'null'; | ||
const column = columns.find(column => column.name === field); | ||
return column ? column.type : 'null'; | ||
} |
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 was deleted.
Oops, something went wrong.
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
29 changes: 29 additions & 0 deletions
29
public/expression_types/arg_types/datacolumn/__tests__/get_form_object.js
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,29 @@ | ||
import expect from 'expect.js'; | ||
import { getFormObject } from '../get_form_object'; | ||
|
||
describe('getFormObject', () => { | ||
describe('valid input', () => { | ||
it('string', () => { | ||
expect(getFormObject('field')).to.be.eql({ fn: '', column: 'field' }); | ||
}); | ||
it('simple expression', () => { | ||
expect(getFormObject('mean(field)')).to.be.eql({ fn: 'mean', column: 'field' }); | ||
}); | ||
}); | ||
describe('invalid input', () => { | ||
it('number', () => { | ||
expect(getFormObject) | ||
.withArgs('2') | ||
.to.throwException(e => { | ||
expect(e.message).to.be('Cannot render scalar values or complex math expressions'); | ||
}); | ||
}); | ||
it('complex expression', () => { | ||
expect(getFormObject) | ||
.withArgs('mean(field * 3)') | ||
.to.throwException(e => { | ||
expect(e.message).to.be('Cannot render scalar values or complex math expressions'); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.