diff --git a/README.md b/README.md
index da48d5d..1207d82 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Inspired by [dplyr](https://dplyr.tidyverse.org/) and [Arquero](https://github.c
### Run locally
1. Clone this [repo](https://github.com/Justin-ZS/bow)
1. Run `npm i`
-1. Run `npm run server`
+1. Run `npm start`
1. Do whatever you like
### Develop RoadMap
diff --git a/index.html b/index.html
index 1e9b4ed..438c2c3 100644
--- a/index.html
+++ b/index.html
@@ -80,7 +80,7 @@
Example
age: [12, 12, 12, 14, 14],
})
.groupBy('name', 'age')
- .summarize({ sumOfValue: 'sum(value)' })
+ .summarize({ sumOfValue: bow.Op.sum('value') })
.orderBy((a, b) => b.age - a.age)
.addColumns({
added: ({ age, sumOfValue }) => age + sumOfValue
diff --git a/src/expression/parse.ts b/src/expression/parse.ts
index dd4ddcd..cd2f556 100644
--- a/src/expression/parse.ts
+++ b/src/expression/parse.ts
@@ -1,17 +1,35 @@
import { parse } from 'acorn';
import * as ESTree from 'estree';
+import transformAST from './transform';
const PARSE_OPTS: acorn.Options = {
ecmaVersion: 'latest',
sourceType: 'script',
};
+export const FUNC_PREFIX = '_is_Fn_\u001d';
+
+const replacer = (key, value) => {
+ if (typeof value === 'function') return `${FUNC_PREFIX}${value}`;
+ return value;
+};
+
+const transformers = {
+ 'Literal': (node) => {
+ if (node.value.startsWith(FUNC_PREFIX)) {
+ const funcAST = parse(JSON.parse(node.raw).slice(FUNC_PREFIX.length), PARSE_OPTS);
+ return funcAST;
+ }
+ return node;
+ }
+};
// Expression -> ES AST
export const parseES = (expr: string | Function): ESTree.Node => {
if (!expr) throw Error("ParseES: Empty Expression");
try {
- const ast: any = parse(`expr=${expr}`, PARSE_OPTS);
- return ast.body[0].expression.right;
+ const ast: any = parse(`expr=${JSON.stringify(expr, replacer)}`, PARSE_OPTS);
+ const content = ast.body[0].expression.right;
+ return transformAST(content, null, transformers);
} catch(err) {
throw Error(`ParseES: ${err}`);
}
diff --git a/src/expression/transform/index.ts b/src/expression/transform/index.ts
index 712fbc0..5705123 100644
--- a/src/expression/transform/index.ts
+++ b/src/expression/transform/index.ts
@@ -2,19 +2,20 @@ import * as ESTree from 'estree';
import baseTransformers, { RecursiveTransformer } from './base';
import customTransformers from './custom';
-const getTransformer = (node: ESTree.Node) => {
+const getTransformer = (node: ESTree.Node, custom) => {
const type = node.type;
- return customTransformers[type] ?? baseTransformers[type] ?? (x => x);
+ return custom[type] ?? baseTransformers[type] ?? (x => x);
};
const transformAST = (
root: ESTree.Node,
state: T = null,
+ customTs = customTransformers,
) => (function t(
node: ESTree.Node,
st: T,
) {
- const transformer = getTransformer(node) as RecursiveTransformer;
+ const transformer = getTransformer(node, customTs) as RecursiveTransformer;
return transformer(node, st, t);
})(root, state);
diff --git a/src/index.ts b/src/index.ts
index 27b3b2a..8a6e4dd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,9 @@
-import { Table } from './modules';
+import { Table, Op } from './modules';
import { TableEx } from './extensions';
export {
Table,
TableEx,
+ Op,
};
diff --git a/src/modules/Aggregator.ts b/src/modules/Aggregator.ts
index 9362950..b018fc9 100644
--- a/src/modules/Aggregator.ts
+++ b/src/modules/Aggregator.ts
@@ -1,40 +1,31 @@
-import { AggregateType, FieldDescription, DataType, AggregateDescription } from 'Typings';
+import { AggregateType, DataType } from 'Typings';
export abstract class Aggregator {
- readonly field: FieldDescription;
- readonly name: string;
-
type: AggregateType;
dataType = DataType.Null;
- constructor(targetField: FieldDescription, name?: string) {
- this.field = targetField;
- this.name = name;
- }
-
- abstract addUp(value: unknown): Aggregator;
- abstract get value(): unknown;
+ abstract addUp(getter: () => unknown): void;
+ abstract value: number;
- isAnonymous() {
- return name === undefined;
- }
clone(): Aggregator {
const Ctor = this.constructor as any;
- return new Ctor(this.field, this, name);
+ return new Ctor();
}
}
class SumAggregator extends Aggregator {
type = AggregateType.Sum;
dataType = DataType.Number;
+
private sum: number = null;
- addUp(value: number) {
+ addUp(getter) {
+ const value = getter();
if (value == null) return this;
this.sum += +value; // @TODO: handle type coerce
- return this;
}
+
get value() {
return this.sum;
}
@@ -42,9 +33,11 @@ class SumAggregator extends Aggregator {
class MinAggregator extends Aggregator {
type = AggregateType.Min;
dataType = DataType.Number;
+
private min: number = null;
- addUp(value: number) {
+ addUp(getter) {
+ const value = getter();
if (value == null) return this;
if (this.min == null) {
@@ -52,8 +45,6 @@ class MinAggregator extends Aggregator {
} else {
this.min = Math.min(this.min, +value); // @TODO: handle type coerce
}
-
- return this;
}
get value() {
return this.min;
@@ -62,9 +53,11 @@ class MinAggregator extends Aggregator {
class MaxAggregator extends Aggregator {
type = AggregateType.Max;
dataType = DataType.Number;
+
private max: number = null;
- addUp(value: number) {
+ addUp(getter) {
+ const value = getter();
if (value == null) return this;
if (this.max == null) {
@@ -72,8 +65,6 @@ class MaxAggregator extends Aggregator {
} else {
this.max = Math.max(this.max, +value); // @TODO: handle type coerce
}
-
- return this;
}
get value() {
return this.max;
@@ -82,6 +73,7 @@ class MaxAggregator extends Aggregator {
class CountAggregator extends Aggregator {
type = AggregateType.Count;
dataType = DataType.Number;
+
private count = 0;
addUp() {
@@ -95,14 +87,14 @@ class CountAggregator extends Aggregator {
class AvgAggregator extends Aggregator {
type = AggregateType.Avg;
dataType = DataType.Number;
- private sumAgg = new SumAggregator(this.field, this.name);
- private cntAgg = new CountAggregator(this.field, this.name);
- addUp(value: number) {
+ private sumAgg = new SumAggregator();
+ private cntAgg = new CountAggregator();
+
+ addUp(getter) {
+ const value = getter();
this.sumAgg.addUp(value);
this.cntAgg.addUp();
-
- return this;
}
get value() {
const sum = this.sumAgg.value;
@@ -115,19 +107,10 @@ class AvgAggregator extends Aggregator {
}
}
-export const getAggregatorByDescription = (aggDesc: AggregateDescription): Aggregator => {
- switch (aggDesc.type) {
- case AggregateType.Sum:
- return new SumAggregator(aggDesc.field, aggDesc.name);
- case AggregateType.Min:
- return new MinAggregator(aggDesc.field, aggDesc.name);
- case AggregateType.Max:
- return new MaxAggregator(aggDesc.field, aggDesc.name);
- case AggregateType.Count:
- return new CountAggregator(aggDesc.field, aggDesc.name);
- case AggregateType.Avg:
- return new AvgAggregator(aggDesc.field, aggDesc.name);
- default:
- throw Error(`Not Supported Aggregate Type: ${aggDesc.type}`);
- }
+export const aggMapping = {
+ [AggregateType.Sum]: SumAggregator,
+ [AggregateType.Avg]: AvgAggregator,
+ [AggregateType.Count]: CountAggregator,
+ [AggregateType.Max]: MaxAggregator,
+ [AggregateType.Min]: MinAggregator,
};
\ No newline at end of file
diff --git a/src/modules/aggregate.ts b/src/modules/aggregate.ts
index f0d9285..cf01d83 100644
--- a/src/modules/aggregate.ts
+++ b/src/modules/aggregate.ts
@@ -1,23 +1,34 @@
-import { ITable, AggregateDescription } from 'Typings';
-import { range } from 'PureUtils';
+import { ITable, FieldDescription } from 'Typings';
+import { range, mapRecord } from 'PureUtils';
import { TableEx } from 'Extensions';
-import { extractGroupedColumns } from './/group';
-import { Aggregator, getAggregatorByDescription } from './Aggregator';
+import { extractGroupedColumns } from './group';
+import { Aggregator, aggMapping } from './Aggregator';
+
+export type AggregatorDescription = {
+ name?: string,
+ agg: Aggregator,
+ getterFn: (rowIdx, table) => () => unknown,
+}
const aggregateGroupedTable = (
- aggs: Aggregator[],
+ descs: AggregatorDescription[],
table: ITable,
) => {
const { size, keys, map, names } = table.groups;
const groups = range(0, size);
- const aggss = aggs.map(agg => groups.map(() => agg.clone()));
+ const descss: AggregatorDescription[][] = descs
+ .map(desc => groups
+ .map(() => ({
+ ...desc,
+ agg: desc.agg.clone(),
+ })));
table.traverse((rowIdx) => {
- aggss.forEach(aggs => {
- const agg = aggs[keys[rowIdx]];
- agg.addUp(table.getCell(agg.field.name, rowIdx));
+ descss.forEach(descs => {
+ const desc = descs[keys[rowIdx]];
+ desc.agg.addUp(desc.getterFn(rowIdx, table));
});
});
@@ -28,26 +39,26 @@ const aggregateGroupedTable = (
data[names[idx]] = column;
});
// aggregated values data
- aggs.forEach((agg, idx) => {
- data[agg.name] = aggss[idx].map(agg => agg.value);
+ descs.forEach((desc, idx) => {
+ data[desc.name] = descss[idx].map(({ agg }) => agg.value);
});
return TableEx.fromColumns(data);
};
const aggregateFlatTable = (
- aggs: Aggregator[],
+ descs: AggregatorDescription[],
table: ITable,
) => {
table.traverse((rowIdx) => {
- aggs.forEach(agg => {
- agg.addUp(table.getCell(agg.field.name, rowIdx));
+ descs.forEach(desc => {
+ desc.agg.addUp(desc.getterFn(rowIdx, table));
});
});
- const data = aggs
- .reduce((acc, agg) => {
- acc[agg.name] = [agg.value];
+ const data = descs
+ .reduce((acc, { name, agg }) => {
+ acc[name] = [agg.value];
return acc;
}, {});
@@ -55,13 +66,23 @@ const aggregateFlatTable = (
};
export const getAggregatedTable = (
- aggDescs: AggregateDescription[],
+ aggDescs: AggregatorDescription[],
table: ITable,
) => {
- const aggs = aggDescs.map(getAggregatorByDescription);
-
if (table.isGrouped) {
- return aggregateGroupedTable(aggs, table);
+ return aggregateGroupedTable(aggDescs, table);
}
- return aggregateFlatTable(aggs, table);
-};
\ No newline at end of file
+ return aggregateFlatTable(aggDescs, table);
+};
+
+const fields2GetterFn = (fields: FieldDescription[]) =>
+ (rowIdx: number, table: ITable) =>
+ () => (fields.length === 1
+ ? table.getCell(fields[0].name, rowIdx)
+ : fields.map(({ name }) => table.getCell(name, rowIdx)));
+
+export const aggOps = mapRecord((Agg) =>
+ (...fields: FieldDescription[]): AggregatorDescription => ({
+ agg: new Agg(),
+ getterFn: fields2GetterFn(fields),
+ }), aggMapping);
\ No newline at end of file
diff --git a/src/modules/index.ts b/src/modules/index.ts
index f7804c8..256ff39 100644
--- a/src/modules/index.ts
+++ b/src/modules/index.ts
@@ -1,6 +1,9 @@
import Table from './table';
+
export * from './column';
+import * as Op from './operators';
export {
Table,
+ Op
};
\ No newline at end of file
diff --git a/src/modules/operators/Operator.ts b/src/modules/operators/Operator.ts
deleted file mode 100644
index 9ec22ca..0000000
--- a/src/modules/operators/Operator.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Operator as IOperator } from 'Typings';
-
-export class Operator implements IOperator {
- type: IOperator['type'];
- fields: IOperator['fields'];
-
- static create(...args: ConstructorParameters) {
- return new Operator(...args);
- }
- constructor(type: IOperator['type'], ...fields: IOperator['fields']) {
- this.type = type;
- this.fields = fields;
- }
- toString() {
- return JSON.stringify(this);
- }
-}
-
-export default Operator;
\ No newline at end of file
diff --git a/src/modules/operators/aggregate.ts b/src/modules/operators/aggregate.ts
index cb2a947..9c025f8 100644
--- a/src/modules/operators/aggregate.ts
+++ b/src/modules/operators/aggregate.ts
@@ -1,15 +1,15 @@
import { AggregateType } from 'Typings';
-import Op from './Operator';
+import { makeOperator } from 'CommonUtils';
-export const sum = (field: string) => Op.create(AggregateType.Sum, field);
+export const sum = (field: string) => makeOperator(AggregateType.Sum, field);
-export const average = (field: string) => Op.create(AggregateType.Avg, field);
+export const average = (field: string) => makeOperator(AggregateType.Avg, field);
export const avg = average;
export const mean = average;
-export const max = (field: string) => Op.create(AggregateType.Max, field);
+export const max = (field: string) => makeOperator(AggregateType.Max, field);
-export const min = (field: string) => Op.create(AggregateType.Min, field);
+export const min = (field: string) => makeOperator(AggregateType.Min, field);
-export const rowCount = () => Op.create(AggregateType.Count);
-export const n = () => rowCount;
\ No newline at end of file
+export const rowCount = () => makeOperator(AggregateType.Count);
+export const n = rowCount;
\ No newline at end of file
diff --git a/src/modules/operators/index.ts b/src/modules/operators/index.ts
index 4bc1b0b..cccb0fc 100644
--- a/src/modules/operators/index.ts
+++ b/src/modules/operators/index.ts
@@ -1,2 +1 @@
-export * from './aggregate';
-export * from './Operator';
\ No newline at end of file
+export * from './aggregate';
\ No newline at end of file
diff --git a/src/modules/table.ts b/src/modules/table.ts
index 0d1784b..47fb861 100644
--- a/src/modules/table.ts
+++ b/src/modules/table.ts
@@ -1,14 +1,15 @@
import {
- ITable, TableData, GroupDescription, FieldDescription,
- TableDescription, AggregateDescription, AggregateType,
+ ITable, TableData, GroupDescription,
+ FieldDescription, TableDescription,
} from 'Typings';
-import { pick, omit } from 'PureUtils';
+import { pick, omit, list2Record } from 'PureUtils';
import { makeFieldDesc } from 'CommonUtils';
+import exprResolver from '../expression';
import { getGroupDesc } from './group';
import { getIndexSet } from './filter';
import { getOrderedIndexes } from './order';
-import { getAggregatedTable } from './aggregate';
+import { getAggregatedTable, aggOps } from './aggregate';
import { getCalculatedTable } from './calculate';
export default class Table implements ITable {
@@ -211,23 +212,11 @@ export default class Table implements ITable {
}
// TODO: use expression in parameters
public summarize(aggOpts: any) {
- const isSumAgg = str => {
- const reg = /sum\((.*)\)/i;
- const result = reg.exec(str);
-
- if (!result) return false;
- if (result[1] === '') return false;
- return true;
- };
- const getFieldName = str => /sum\((.*)\)/i.exec(str)[1];
-
- const aggDescs: AggregateDescription[] = Object.entries(aggOpts)
- .filter(([_, str]) => isSumAgg(str))
- .map(([name, str]) => ({
- name,
- type: AggregateType.Sum,
- field: this.getFieldDescriptionByName(getFieldName(str)),
- }));
+ const expr = exprResolver(aggOpts);
+ const t = list2Record(this.fields, 'name');
+
+ const aggDescs = Object.entries(expr)
+ .map(([name, fn]) => ({ name, ...(fn as any)(t, aggOps) }));
return getAggregatedTable(aggDescs, this);
}
// #region alias
diff --git a/src/utils/pureUtils.ts b/src/utils/pureUtils.ts
index a5618e8..b758850 100644
--- a/src/utils/pureUtils.ts
+++ b/src/utils/pureUtils.ts
@@ -1,6 +1,7 @@
// Never import any lib here!
// All utils should be pure function
+const isRecord = (x: unknown) => x && typeof x === 'object' && !Array.isArray(x);
const isPropertyInRecord = (prop: string, obj: Record) => Object.prototype.hasOwnProperty.call(obj, prop);
// (['a', 'd'], {a: 1, b: 2, c: 3, d: 4}) -> {a: 1, d: 4}
@@ -87,4 +88,18 @@ export const isFn = >(
}
return false;
};
-};
\ No newline at end of file
+};
+// [1, 2, 3] -> { 1: 1, 2: 2, 3: 3 }
+// [{ key: 'A' }, { key: 'B' }] -> { A: { key: 'A' }, B: { key: 'B' } }
+export const list2Record = (xs: any[], keyName = 'key') => xs
+ .reduce((record, x, idx) => {
+ if (!isRecord(x)) record[idx] = x;
+ else record[x[keyName] ?? idx] = x;
+ return record;
+ }, {});
+
+export const lens = (getter = i => i, setter) => ({
+ view: getter,
+ set: setter,
+ over: (fn, obj) => setter(fn(getter(obj)), obj),
+});
\ No newline at end of file
diff --git a/src/utils/typeUtils.ts b/src/utils/typeUtils.ts
index af10aea..e2a1f6a 100644
--- a/src/utils/typeUtils.ts
+++ b/src/utils/typeUtils.ts
@@ -1,4 +1,4 @@
-import { GroupDescription, FieldDescription, DataType } from 'Typings';
+import { GroupDescription, FieldDescription, DataType, Operator } from 'Typings';
export const makeGroupDesc = (
names: string[],
@@ -12,3 +12,8 @@ export const makeFieldDesc = (
idx: number,
type: DataType,
): FieldDescription => ({ name, idx, type });
+
+export const makeOperator = (type: Operator['type'], ...fields: Operator['fields']): Operator => ({
+ type,
+ fields,
+});