Skip to content

Commit

Permalink
Feat: Math markdown helper (elastic#157)
Browse files Browse the repository at this point in the history
* chore: replace get_mathjs_scope with pivot_object_array

more generic helper function that can be used elsewhere in the code (with tests)

* feat: add math helper to handlebars

and wrap the handlebars instance so the helper is only added once
  • Loading branch information
w33ble authored Sep 11, 2017
1 parent 21f496f commit 8465673
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 17 deletions.
2 changes: 1 addition & 1 deletion common/functions/markdown/markdown.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Markdown from 'markdown-it';
import Handlebars from 'handlebars/dist/handlebars';
import { Handlebars } from '../../lib/handlebars.js';
import Fn from '../fn.js';

const md = new Markdown();
Expand Down
11 changes: 0 additions & 11 deletions common/functions/math/get_mathjs_scope.js

This file was deleted.

5 changes: 3 additions & 2 deletions common/functions/math/math.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Fn from '../fn.js';
import { math } from '../../lib/math.js';
import { getMathjsScope } from './get_mathjs_scope';
import { pivotObjectArray } from '../../lib/pivot_object_array.js';

export default new Fn({
name: 'math',
Expand All @@ -15,7 +15,8 @@ export default new Fn({
},
},
fn: (context, args) => {
const mathContext = context && context.type === 'datatable' ? getMathjsScope(context) : null;
const isDatatable = context && context.type === 'datatable';
const mathContext = isDatatable ? pivotObjectArray(context.rows, context.columns) : null;
const result = math.eval(args._, mathContext);
if (typeof result !== 'number') throw new Error ('Failed to execute math expression. Check your column names');
return result;
Expand Down
44 changes: 44 additions & 0 deletions common/lib/__tests__/pivot_object_array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import expect from 'expect.js';
import { pivotObjectArray } from '../pivot_object_array';

describe('pivotObjectArray', () => {
let rows;

beforeEach(() => {
rows = [
{ make: 'honda', model: 'civic', price: '10000' },
{ make: 'toyota', model: 'corolla', price: '12000' },
{ make: 'tesla', model: 'model 3', price: '35000' },
];
});

it('converts array of objects', () => {
const data = pivotObjectArray(rows);

expect(data).to.be.an('object');
expect(data).to.have.property('make');
expect(data).to.have.property('model');
expect(data).to.have.property('price');

expect(data.make).to.eql(['honda', 'toyota', 'tesla']);
expect(data.model).to.eql(['civic', 'corolla', 'model 3']);
expect(data.price).to.eql(['10000', '12000', '35000']);
});

it('uses passed in column list', () => {
const data = pivotObjectArray(rows, ['price']);

expect(data).to.be.an('object');
expect(data).to.eql({ price: ['10000', '12000', '35000'] });
});

it('adds missing columns with undefined values', () => {
const data = pivotObjectArray(rows, ['price', 'missing']);

expect(data).to.be.an('object');
expect(data).to.eql({
price: ['10000', '12000', '35000'],
missing: [undefined, undefined, undefined],
});
});
});
11 changes: 11 additions & 0 deletions common/lib/handlebars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Hbars from 'handlebars/dist/handlebars';
import { math } from './math.js';
import { pivotObjectArray } from './pivot_object_array.js';

Hbars.registerHelper('math', (rows, expression, precision) => {
if (!Array.isArray(rows)) return 'MATH ERROR: first argument must be an array';
const value = math.eval(expression, pivotObjectArray(rows));
return (precision) ? value.toFixed(precision) : value;
});

export const Handlebars = Hbars;
7 changes: 7 additions & 0 deletions common/lib/pivot_object_array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { map, zipObject } from 'lodash';

export function pivotObjectArray(rows, columns) {
const columnNames = columns || Object.keys(rows[0]);
const columnValues = map(columnNames, (name) => map(rows, name));
return zipObject(columnNames, columnValues);
}
6 changes: 3 additions & 3 deletions server/functions/pointseries/pointseries.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import moment from 'moment';
import { groupBy, zipObject, uniqBy, omit, pickBy, find, uniq, map, mapValues } from 'lodash';
import Fn from '../../../common/functions/fn.js';
import { findInObject } from '../../../common/lib/find_in_object';
import { getMathjsScope } from '../../../common/functions/math/get_mathjs_scope';
import { pivotObjectArray } from '../../../common/lib/pivot_object_array.js';

function isColumnReference(mathExpression) {
const parsedMath = math.parse(mathExpression);
Expand Down Expand Up @@ -77,7 +77,7 @@ export default new Fn({
// The way the function below is written you can add as many arbitrary named args as you want.
},
fn: (context, args) => {
const mathScope = getMathjsScope(context);
const mathScope = pivotObjectArray(context.rows, context.columns);
const dimensionNames = Object.keys(pickBy(args, val => !isMeasure(mathScope, val))).filter(arg => args[arg] != null);
const measureNames = Object.keys(pickBy(args, val => isMeasure(mathScope, val)));
const columns = mapValues(args, arg => {
Expand Down Expand Up @@ -130,7 +130,7 @@ export default new Fn({
// Then compute that 1 value for each measure
Object.values(measureKeys).forEach(rows => {
const subtable = { type: 'datatable', columns: context.columns, rows: rows };
const subScope = getMathjsScope(subtable);
const subScope = pivotObjectArray(subtable.rows, subtable.columns);
const measureValues = measureNames.map(measure => {
try {
return math.eval(args[measure], subScope);
Expand Down

0 comments on commit 8465673

Please sign in to comment.