From 53380d14fa0cbad76eb662b77a4880a4de6a3546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 12 Mar 2019 22:00:14 -0400 Subject: [PATCH 01/54] refactor syntax tree into various trees and lexicons --- src/core/syntax-tree/index.ts | 18 +- src/core/syntax-tree/lexer.ts | 8 +- src/core/syntax-tree/lexicon.ts | 224 +----------------- .../components/ControlledTable/index.tsx | 4 +- src/dash-table/components/FilterFactory.tsx | 93 ++------ src/dash-table/conditional/index.ts | 6 +- src/dash-table/derived/cell/dropdowns.ts | 6 +- src/dash-table/derived/data/virtual.ts | 4 +- src/dash-table/derived/style/index.ts | 6 +- src/dash-table/syntax-tree/index.ts | 70 ++++++ src/dash-table/syntax-tree/lexeme/and.ts | 24 ++ .../syntax-tree/lexeme/binaryOperator.ts | 48 ++++ src/dash-table/syntax-tree/lexeme/block.ts | 28 +++ .../syntax-tree/lexeme/expression.ts | 32 +++ src/dash-table/syntax-tree/lexeme/index.ts | 20 ++ src/dash-table/syntax-tree/lexeme/operand.ts | 18 ++ src/dash-table/syntax-tree/lexeme/or.ts | 24 ++ src/dash-table/syntax-tree/lexeme/unaryNot.ts | 23 ++ .../syntax-tree/lexeme/unaryOperator.ts | 50 ++++ src/dash-table/syntax-tree/lexicon/column.ts | 15 ++ .../syntax-tree/lexicon/columnMulti.ts | 9 + src/dash-table/syntax-tree/lexicon/query.ts | 23 ++ .../cypress/tests/unit/syntactic_tree_test.ts | 134 +++++------ 23 files changed, 498 insertions(+), 389 deletions(-) create mode 100644 src/dash-table/syntax-tree/index.ts create mode 100644 src/dash-table/syntax-tree/lexeme/and.ts create mode 100644 src/dash-table/syntax-tree/lexeme/binaryOperator.ts create mode 100644 src/dash-table/syntax-tree/lexeme/block.ts create mode 100644 src/dash-table/syntax-tree/lexeme/expression.ts create mode 100644 src/dash-table/syntax-tree/lexeme/index.ts create mode 100644 src/dash-table/syntax-tree/lexeme/operand.ts create mode 100644 src/dash-table/syntax-tree/lexeme/or.ts create mode 100644 src/dash-table/syntax-tree/lexeme/unaryNot.ts create mode 100644 src/dash-table/syntax-tree/lexeme/unaryOperator.ts create mode 100644 src/dash-table/syntax-tree/lexicon/column.ts create mode 100644 src/dash-table/syntax-tree/lexicon/columnMulti.ts create mode 100644 src/dash-table/syntax-tree/lexicon/query.ts diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index 292d351d0..419a59d16 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -1,20 +1,26 @@ import Logger from 'core/Logger'; -import lexer from 'core/syntax-tree/lexer'; +import lexer, { ILexerResult } from 'core/syntax-tree/lexer'; import syntaxer, { ISyntaxerResult } from 'core/syntax-tree/syntaxer'; +import { ILexeme } from './lexicon'; export default class SyntaxTree { - private result: ISyntaxerResult; + protected lexerResult: ILexerResult; + protected syntaxerResult: ISyntaxerResult; get isValid() { - return this.result.valid; + return this.syntaxerResult.valid; } private get tree() { - return this.result.tree; + return this.syntaxerResult.tree; } - constructor(private readonly query: string) { - this.result = syntaxer(lexer(this.query)); + constructor( + private readonly lexicon: ILexeme[], + private readonly query: string + ) { + this.lexerResult = lexer(this.lexicon, this.query); + this.syntaxerResult = syntaxer(this.lexerResult); } evaluate = (target: any) => { diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts index 6daeca311..40dad3707 100644 --- a/src/core/syntax-tree/lexer.ts +++ b/src/core/syntax-tree/lexer.ts @@ -1,4 +1,4 @@ -import Lexicon, { ILexeme } from 'core/syntax-tree/lexicon'; +import { ILexeme } from 'core/syntax-tree/lexicon'; export interface ILexerResult { lexemes: ILexemeResult[]; @@ -11,20 +11,20 @@ export interface ILexemeResult { value?: string; } -export default function lexer(query: string): ILexerResult { +export default function lexer(lexicon: ILexeme[], query: string): ILexerResult { let lexeme: ILexeme | null = null; let result: ILexemeResult[] = []; while (query.length) { query = query.replace(/^\s+/, ''); - let lexemes: ILexeme[] = Lexicon.filter(_lexeme => + let lexemes: ILexeme[] = lexicon.filter(_lexeme => lexeme && _lexeme.when && _lexeme.when.indexOf(lexeme.name) !== -1); if (!lexemes.length) { - lexemes = Lexicon; + lexemes = lexicon; } lexeme = lexemes.find(_lexeme => _lexeme.regexp.test(query)) || null; diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 3be0a5912..627c132f6 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -1,4 +1,3 @@ -import Logger from 'core/Logger'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; export enum LexemeType { @@ -22,225 +21,4 @@ export interface ILexeme { regexp: RegExp; syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any; when?: string[]; -} - -const isPrime = (c: number) => { - if (c === 2) { return true; } - if (c < 2 || c % 2 === 0) { return false; } - for (let n = 3; n * n <= c; n += 2) { if (c % n === 0) { return false; } } - return true; -}; - -const operand = { - resolve: (target: any, tree: ISyntaxTree) => { - if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.")+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { - return target[ - tree.value.slice(1, tree.value.length - 1) - ]; - } else if (/^(\w|[:.\-+])+$/.test(tree.value)) { - return target[tree.value]; - } - }, - regexp: /^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/ -}; - -const expression = { - resolve: (target: any, tree: ISyntaxTree) => { - if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { - return tree.value.slice(1, tree.value.length - 1); - } else if (/^(num|str)\(.*\)$/.test(tree.value)) { - const res = tree.value.match(/^(\w+)\((.*)\)$/); - if (res) { - const [, op, value] = res; - - switch (op) { - case 'num': - return parseFloat(value); - case 'str': - default: - return value; - } - } else { - throw Error(); - } - } else { - return target[tree.value]; - } - }, - regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/ -}; - -const lexicon: ILexeme[] = [ - { - evaluate: (target, tree) => { - Logger.trace('evalute -> &&', target, tree); - - const t = tree as any; - const lv = t.left.lexeme.evaluate(target, t.left); - const rv = t.right.lexeme.evaluate(target, t.right); - return lv && rv; - }, - name: LexemeType.And, - priority: 2, - regexp: /^(and\s|&&)/i, - syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { - return Object.assign({ - left: lexs.slice(0, pivotIndex), - right: lexs.slice(pivotIndex + 1) - }, pivot); - } - }, - { - evaluate: (target, tree) => { - Logger.trace('evalute -> ||', target, tree); - - const t = tree as any; - - return t.left.lexeme.evaluate(target, t.left) || - t.right.lexeme.evaluate(target, t.right); - }, - name: LexemeType.Or, - priority: 3, - regexp: /^(or\s|\|\|)/i, - syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { - return Object.assign({ - left: lexs.slice(0, pivotIndex), - right: lexs.slice(pivotIndex + 1) - }, pivot); - } - }, - { - name: LexemeType.BlockClose, - nesting: -1, - regexp: /^\)/ - }, - { - evaluate: (target, tree) => { - Logger.trace('evaluate -> ()', target, tree); - - const t = tree as any; - - return t.block.lexeme.evaluate(target, t.block); - }, - name: LexemeType.BlockOpen, - nesting: 1, - priority: 1, - regexp: /^\(/, - syntaxer: (lexs: any[]) => { - return Object.assign({ - block: lexs.slice(1, lexs.length - 1) - }, lexs[0]); - }, - when: [LexemeType.UnaryNot] - }, - { - ...operand, - name: LexemeType.Operand - }, - { - evaluate: (target, tree) => { - Logger.trace('evaluate -> binary', target, tree); - - const t = tree as any; - - const opValue = t.left.lexeme.resolve(target, t.left); - const expValue = t.right.lexeme.resolve(target, t.right); - Logger.trace(`opValue: ${opValue}, expValue: ${expValue}`); - - switch (tree.value.toLowerCase()) { - case 'eq': - case '=': - return opValue === expValue; - case 'gt': - case '>': - return opValue > expValue; - case 'ge': - case '>=': - return opValue >= expValue; - case 'lt': - case '<': - return opValue < expValue; - case 'le': - case '<=': - return opValue <= expValue; - case 'ne': - case '!=': - return opValue !== expValue; - default: - throw new Error(); - } - }, - name: LexemeType.BinaryOperator, - priority: 0, - regexp: /^(>=|<=|>|<|!=|=|ge|le|gt|lt|eq|ne)/i, - syntaxer: (lexs: any[]) => { - let [left, lexeme, right] = lexs; - - return Object.assign({ left, right }, lexeme); - }, - when: [LexemeType.Operand] - }, - { - evaluate: (target, tree) => { - Logger.trace('evaluate -> unary', target, tree); - - const t = tree as any; - const opValue = t.block.lexeme.resolve(target, t.block); - - switch (tree.value.toLowerCase()) { - case 'is even': - return typeof opValue === 'number' && opValue % 2 === 0; - case 'is nil': - return opValue === undefined || opValue === null; - case 'is bool': - return typeof opValue === 'boolean'; - case 'is odd': - return typeof opValue === 'number' && opValue % 2 === 1; - case 'is num': - return typeof opValue === 'number'; - case 'is object': - return opValue !== null && typeof opValue === 'object'; - case 'is str': - return typeof opValue === 'string'; - case 'is prime': - return typeof opValue === 'number' && isPrime(opValue); - default: - throw new Error(); - } - }, - name: LexemeType.UnaryOperator, - priority: 0, - regexp: /^((is nil)|(is odd)|(is even)|(is bool)|(is num)|(is object)|(is str)|(is prime))/i, - syntaxer: (lexs: any[]) => { - let [block, lexeme] = lexs; - - return Object.assign({ block }, lexeme); - }, - when: [LexemeType.Operand] - }, - { - evaluate: (target, tree) => { - Logger.trace('evaluate -> unary not', target, tree); - - const t = tree as any; - - return !t.block.lexeme.evaluate(target, t.block); - }, - name: LexemeType.UnaryNot, - priority: 1.5, - regexp: /^!/, - syntaxer: (lexs: any[]) => { - return Object.assign({ - block: lexs.slice(1, lexs.length) - }, lexs[0]); - }, - when: [LexemeType.UnaryNot] - }, - { - ...expression, - name: LexemeType.Expression, - when: [LexemeType.BinaryOperator] - } -]; - -export default lexicon; \ No newline at end of file +} \ No newline at end of file diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 763eda7c3..80619a915 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -29,6 +29,8 @@ import { derivedTableStyle } from 'dash-table/derived/style'; import { IStyle } from 'dash-table/derived/style/props'; import TableTooltip from './fragments/TableTooltip'; +import queryLexicon from 'dash-table/syntax-tree/lexicon/query'; + const sortNumerical = R.sort((a, b) => a - b); const DEFAULT_STYLE = { @@ -50,7 +52,7 @@ export default class ControlledTable extends PureComponent this.updateStylesheet(); } - getLexerResult = memoizeOne(lexer); + getLexerResult = memoizeOne(lexer.bind(undefined, queryLexicon)); get lexerResult() { const { filtering_settings } = this.props; diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index d87fe691a..f5e9f660e 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -2,16 +2,15 @@ import * as R from 'ramda'; import React from 'react'; import Logger from 'core/Logger'; +import { arrayMap } from 'core/math/arrayZipMap'; +import { LexemeType } from 'core/syntax-tree/lexicon'; import ColumnFilter from 'dash-table/components/Filter/Column'; import { ColumnId, Filtering, FilteringType, IVisibleColumn, VisibleColumns } from 'dash-table/components/Table/props'; -import lexer, { ILexerResult, ILexemeResult } from 'core/syntax-tree/lexer'; -import { LexemeType } from 'core/syntax-tree/lexicon'; -import syntaxer, { ISyntaxerResult, ISyntaxTree } from 'core/syntax-tree/syntaxer'; import derivedFilterStyles from 'dash-table/derived/filter/wrapperStyles'; import { derivedRelevantFilterStyles } from 'dash-table/derived/style'; -import { arrayMap } from 'core/math/arrayZipMap'; -import { Style, Cells, BasicFilters } from 'dash-table/derived/style/props'; +import { BasicFilters, Cells, Style } from 'dash-table/derived/style/props'; +import { MultiColumnsSyntaxTree, SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; type SetFilter = (filter: string) => void; @@ -73,85 +72,26 @@ export default class FilterFactory { ); } - private respectsBasicSyntax(lexemes: ILexemeResult[], allowMultiple: boolean = true) { - const allowedLexemeTypes = [ - LexemeType.BinaryOperator, - LexemeType.Expression, - LexemeType.Operand, - LexemeType.UnaryOperator - ]; - - if (allowMultiple) { - allowedLexemeTypes.push(LexemeType.And); - } - - const allAllowed = R.all( - item => R.contains(item.lexeme.name, allowedLexemeTypes), - lexemes - ); - - if (!allAllowed) { - return false; - } - - const fields = R.map( - item => item.value, - R.filter( - i => i.lexeme.name === LexemeType.Operand, - lexemes - ) - ); - - const uniqueFields = R.uniq(fields); - - if (fields.length !== uniqueFields.length) { - return false; - } - - return true; - } - - private isBasicFilter( - lexerResult: ILexerResult, - syntaxerResult: ISyntaxerResult, - allowMultiple: boolean = true - ) { - return lexerResult.valid && - syntaxerResult.valid && - this.respectsBasicSyntax(lexerResult.lexemes, allowMultiple); - } - private updateOps(query: string) { - const lexerResult = lexer(query); - const syntaxerResult = syntaxer(lexerResult); + const ast = new MultiColumnsSyntaxTree(query); - if (!this.isBasicFilter(lexerResult, syntaxerResult)) { + if (!ast.isValid) { return; } - const { tree } = syntaxerResult; - if (!tree) { + const statements = ast.statements; + if (!statements) { this.ops.clear(); return; } - const toCheck: (ISyntaxTree | undefined)[] = [tree]; - while (toCheck.length) { - const item = toCheck.pop(); - if (!item) { - continue; + R.forEach(s => { + if (s.lexeme.name === LexemeType.UnaryOperator && s.block) { + this.ops.set(s.block.value, s.value); + } else if (s.lexeme.name === LexemeType.BinaryOperator && s.left && s.right) { + this.ops.set(s.left.value, `${s.value} ${s.right.value}`); } - - if (item.lexeme.name === LexemeType.UnaryOperator && item.block) { - this.ops.set(item.block.value, item.value); - } else if (item.lexeme.name === LexemeType.BinaryOperator && item.left && item.right) { - this.ops.set(item.left.value, `${item.value} ${item.right.value}`); - } else { - toCheck.push(item.left); - toCheck.push(item.block); - toCheck.push(item.right); - } - } + }, statements); } private isFragmentValidOrNull(columnId: ColumnId) { @@ -163,10 +103,9 @@ export default class FilterFactory { private isFragmentValid(columnId: ColumnId) { const op = this.ops.get(columnId.toString()); - const lexerResult = lexer(`"${columnId}" ${op}`); - const syntaxerResult = syntaxer(lexerResult); + const ast = new SingleColumnSyntaxTree(`"${columnId}" ${op}`); - return syntaxerResult.valid && this.isBasicFilter(lexerResult, syntaxerResult, false); + return ast.isValid; } public createFilters() { diff --git a/src/dash-table/conditional/index.ts b/src/dash-table/conditional/index.ts index b8173235c..f575435e4 100644 --- a/src/dash-table/conditional/index.ts +++ b/src/dash-table/conditional/index.ts @@ -1,5 +1,5 @@ import { ColumnId, Datum, ColumnType } from 'dash-table/components/Table/props'; -import SyntaxTree from 'core/syntax-tree'; +import { QuerySyntaxTree } from 'dash-table/syntax-tree'; export interface IConditionalElement { filter?: string; @@ -26,7 +26,7 @@ export type ConditionalDataCell = IConditionalElement & IIndexedRowElement & INa export type ConditionalCell = INamedElement & ITypedElement; export type ConditionalHeader = IIndexedHeaderElement & INamedElement & ITypedElement; -function ifAstFilter(ast: SyntaxTree, datum: Datum) { +function ifAstFilter(ast: QuerySyntaxTree, datum: Datum) { return ast.isValid && ast.evaluate(datum); } @@ -69,5 +69,5 @@ export function ifHeaderIndex(condition: IIndexedHeaderElement | undefined, head export function ifFilter(condition: IConditionalElement | undefined, datum: Datum) { return !condition || condition.filter === undefined || - ifAstFilter(new SyntaxTree(condition.filter), datum); + ifAstFilter(new QuerySyntaxTree(condition.filter), datum); } \ No newline at end of file diff --git a/src/dash-table/derived/cell/dropdowns.ts b/src/dash-table/derived/cell/dropdowns.ts index 06884658e..2f8e81ef1 100644 --- a/src/dash-table/derived/cell/dropdowns.ts +++ b/src/dash-table/derived/cell/dropdowns.ts @@ -2,7 +2,6 @@ import * as R from 'ramda'; import { memoizeOne } from 'core/memoizer'; import memoizerCache from 'core/cache/memoizer'; -import SyntaxTree from 'core/syntax-tree'; import { IConditionalDropdown @@ -18,6 +17,7 @@ import { IBaseVisibleColumn, IVisibleColumn } from 'dash-table/components/Table/props'; +import { QuerySyntaxTree } from 'dash-table/syntax-tree'; const mapData = R.addIndex(R.map); @@ -116,13 +116,13 @@ class Dropdowns { */ private readonly ast = memoizerCache<[ColumnId, number]>()(( query: string - ) => new SyntaxTree(query)); + ) => new QuerySyntaxTree(query)); /** * Evaluate if the query matches the cell's data. */ private readonly evaluation = memoizerCache<[ColumnId, number]>()(( - ast: SyntaxTree, + ast: QuerySyntaxTree, datum: Datum ) => ast.evaluate(datum)); } \ No newline at end of file diff --git a/src/dash-table/derived/data/virtual.ts b/src/dash-table/derived/data/virtual.ts index cebccb1b4..e8fd01c89 100644 --- a/src/dash-table/derived/data/virtual.ts +++ b/src/dash-table/derived/data/virtual.ts @@ -2,7 +2,6 @@ import * as R from 'ramda'; import { memoizeOneFactory } from 'core/memoizer'; import sort, { defaultIsNully, SortSettings } from 'core/sorting'; -import SyntaxTree from 'core/syntax-tree'; import { Data, Datum, @@ -10,6 +9,7 @@ import { IDerivedData, Sorting } from 'dash-table/components/Table/props'; +import { QuerySyntaxTree } from 'dash-table/syntax-tree'; const getter = ( data: Data, @@ -25,7 +25,7 @@ const getter = ( }, data); if (filtering === 'fe' || filtering === true) { - const tree = new SyntaxTree(filtering_settings); + const tree = new QuerySyntaxTree(filtering_settings); data = tree.isValid ? tree.filter(data) : diff --git a/src/dash-table/derived/style/index.ts b/src/dash-table/derived/style/index.ts index d73ad6008..641671bea 100644 --- a/src/dash-table/derived/style/index.ts +++ b/src/dash-table/derived/style/index.ts @@ -1,7 +1,6 @@ import * as R from 'ramda'; import { CSSProperties } from 'react'; -import SyntaxTree from 'core/syntax-tree'; import { memoizeOneFactory } from 'core/memoizer'; import { Datum, IVisibleColumn } from 'dash-table/components/Table/props'; @@ -25,6 +24,7 @@ import { ifColumnId, ifColumnType } from 'dash-table/conditional'; +import { QuerySyntaxTree } from 'dash-table/syntax-tree'; export interface IConvertedStyle { style: CSSProperties; @@ -38,7 +38,7 @@ type GenericStyle = Style & Partial<{ if: GenericIf }>; function convertElement(style: GenericStyle) { const indexFilter = style.if && (style.if.header_index || style.if.row_index); - let ast: SyntaxTree; + let ast: QuerySyntaxTree; return { matchesColumn: (column: IVisibleColumn) => @@ -55,7 +55,7 @@ function convertElement(style: GenericStyle) { matchesFilter: (datum: Datum) => !style.if || style.if.filter === undefined || - (ast = ast || new SyntaxTree(style.if.filter)).evaluate(datum), + (ast = ast || new QuerySyntaxTree(style.if.filter)).evaluate(datum), style: convertStyle(style) }; } diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts new file mode 100644 index 000000000..6dd1202af --- /dev/null +++ b/src/dash-table/syntax-tree/index.ts @@ -0,0 +1,70 @@ +import * as R from 'ramda'; + +import SyntaxTree from 'core/syntax-tree'; +import { LexemeType } from 'core/syntax-tree/lexicon'; +import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; + +import queryLexicon from './lexicon/query'; +import columnLexicon from './lexicon/column'; +import columnMultiLexicon from './lexicon/columnMulti'; + +export class SingleColumnSyntaxTree extends SyntaxTree { + constructor(query: string) { + super(columnLexicon, query); + } +} + +export class MultiColumnsSyntaxTree extends SyntaxTree { + constructor(query: string) { + super(columnMultiLexicon, query); + } + + get isValid() { + return super.isValid && + this.respectsBasicSyntax(); + } + + get statements() { + if (!this.syntaxerResult.tree) { + return; + } + + const statements: ISyntaxTree[] = []; + + const toCheck: ISyntaxTree[] = [this.syntaxerResult.tree]; + while (toCheck.length) { + const item = toCheck.pop(); + if (!item) { + continue; + } + + statements.push(item); + + if (item.left) { toCheck.push(item.left); } + if (item.block) { toCheck.push(item.block); } + if (item.right) { toCheck.push(item.right); } + } + + return statements; + } + + private respectsBasicSyntax() { + const fields = R.map( + item => item.value, + R.filter( + i => i.lexeme.name === LexemeType.Operand, + this.lexerResult.lexemes + ) + ); + + const uniqueFields = R.uniq(fields); + + return fields.length === uniqueFields.length; + } +} + +export class QuerySyntaxTree extends SyntaxTree { + constructor(query: string) { + super(queryLexicon, query); + } +} \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/and.ts b/src/dash-table/syntax-tree/lexeme/and.ts new file mode 100644 index 000000000..e9d5bae4b --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/and.ts @@ -0,0 +1,24 @@ +import Logger from 'core/Logger'; +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; + +const and: ILexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> &&', target, tree); + + const t = tree as any; + const lv = t.left.lexeme.evaluate(target, t.left); + const rv = t.right.lexeme.evaluate(target, t.right); + return lv && rv; + }, + name: LexemeType.And, + priority: 2, + regexp: /^(and\s|&&)/i, + syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { + return Object.assign({ + left: lexs.slice(0, pivotIndex), + right: lexs.slice(pivotIndex + 1) + }, pivot); + } +}; + +export default and; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/binaryOperator.ts b/src/dash-table/syntax-tree/lexeme/binaryOperator.ts new file mode 100644 index 000000000..171ceea2b --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/binaryOperator.ts @@ -0,0 +1,48 @@ +import Logger from 'core/Logger'; +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; + +const binaryOperator: ILexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> binary', target, tree); + + const t = tree as any; + + const opValue = t.left.lexeme.resolve(target, t.left); + const expValue = t.right.lexeme.resolve(target, t.right); + Logger.trace(`opValue: ${opValue}, expValue: ${expValue}`); + + switch (tree.value.toLowerCase()) { + case 'eq': + case '=': + return opValue === expValue; + case 'gt': + case '>': + return opValue > expValue; + case 'ge': + case '>=': + return opValue >= expValue; + case 'lt': + case '<': + return opValue < expValue; + case 'le': + case '<=': + return opValue <= expValue; + case 'ne': + case '!=': + return opValue !== expValue; + default: + throw new Error(); + } + }, + name: LexemeType.BinaryOperator, + priority: 0, + regexp: /^(>=|<=|>|<|!=|=|ge|le|gt|lt|eq|ne)/i, + syntaxer: (lexs: any[]) => { + let [left, lexeme, right] = lexs; + + return Object.assign({ left, right }, lexeme); + }, + when: [LexemeType.Operand] +}; + +export default binaryOperator; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/block.ts b/src/dash-table/syntax-tree/lexeme/block.ts new file mode 100644 index 000000000..5a1fb14f2 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/block.ts @@ -0,0 +1,28 @@ +import Logger from 'core/Logger'; +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; + +export const blockClose: ILexeme = { + name: LexemeType.BlockClose, + nesting: -1, + regexp: /^\)/ +}; + +export const blockOpen: ILexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> ()', target, tree); + + const t = tree as any; + + return t.block.lexeme.evaluate(target, t.block); + }, + name: LexemeType.BlockOpen, + nesting: 1, + priority: 1, + regexp: /^\(/, + syntaxer: (lexs: any[]) => { + return Object.assign({ + block: lexs.slice(1, lexs.length - 1) + }, lexs[0]); + }, + when: [LexemeType.UnaryNot] +}; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts new file mode 100644 index 000000000..a664cf586 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -0,0 +1,32 @@ +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; + +const expression: ILexeme = { + resolve: (target: any, tree: ISyntaxTree) => { + if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { + return tree.value.slice(1, tree.value.length - 1); + } else if (/^(num|str)\(.*\)$/.test(tree.value)) { + const res = tree.value.match(/^(\w+)\((.*)\)$/); + if (res) { + const [, op, value] = res; + + switch (op) { + case 'num': + return parseFloat(value); + case 'str': + default: + return value; + } + } else { + throw Error(); + } + } else { + return target[tree.value]; + } + }, + regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/, + name: LexemeType.Expression, + when: [LexemeType.BinaryOperator] +}; + +export default expression; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/index.ts b/src/dash-table/syntax-tree/lexeme/index.ts new file mode 100644 index 000000000..3d850358b --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/index.ts @@ -0,0 +1,20 @@ +import and from './and'; +import binaryOperator from './binaryOperator'; +import { blockClose, blockOpen } from './block'; +import expression from './expression'; +import operand from './operand'; +import or from './or'; +import unaryNot from './unaryNot'; +import unaryOperator from './unaryOperator'; + +export { + and, + binaryOperator, + blockClose, + blockOpen, + expression, + operand, + or, + unaryNot, + unaryOperator +}; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts new file mode 100644 index 000000000..2ad7ae615 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -0,0 +1,18 @@ +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; + +const operand: ILexeme = { + resolve: (target: any, tree: ISyntaxTree) => { + if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.")+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { + return target[ + tree.value.slice(1, tree.value.length - 1) + ]; + } else if (/^(\w|[:.\-+])+$/.test(tree.value)) { + return target[tree.value]; + } + }, + regexp: /^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/, + name: LexemeType.Operand +}; + +export default operand; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/or.ts b/src/dash-table/syntax-tree/lexeme/or.ts new file mode 100644 index 000000000..362afe4d5 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/or.ts @@ -0,0 +1,24 @@ +import Logger from 'core/Logger'; +import { ILexeme, LexemeType } from 'core/syntax-tree/lexicon'; + +const or: ILexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> ||', target, tree); + + const t = tree as any; + + return t.left.lexeme.evaluate(target, t.left) || + t.right.lexeme.evaluate(target, t.right); + }, + name: LexemeType.Or, + priority: 3, + regexp: /^(or\s|\|\|)/i, + syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { + return Object.assign({ + left: lexs.slice(0, pivotIndex), + right: lexs.slice(pivotIndex + 1) + }, pivot); + } +}; + +export default or; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unaryNot.ts b/src/dash-table/syntax-tree/lexeme/unaryNot.ts new file mode 100644 index 000000000..9c97a4f5b --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/unaryNot.ts @@ -0,0 +1,23 @@ +import Logger from 'core/Logger'; +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; + +const unaryNot: ILexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> unary not', target, tree); + + const t = tree as any; + + return !t.block.lexeme.evaluate(target, t.block); + }, + name: LexemeType.UnaryNot, + priority: 1.5, + regexp: /^!/, + syntaxer: (lexs: any[]) => { + return Object.assign({ + block: lexs.slice(1, lexs.length) + }, lexs[0]); + }, + when: [LexemeType.UnaryNot] +}; + +export default unaryNot; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unaryOperator.ts b/src/dash-table/syntax-tree/lexeme/unaryOperator.ts new file mode 100644 index 000000000..542314f7f --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/unaryOperator.ts @@ -0,0 +1,50 @@ +import Logger from 'core/Logger'; +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; + +const isPrime = (c: number) => { + if (c === 2) { return true; } + if (c < 2 || c % 2 === 0) { return false; } + for (let n = 3; n * n <= c; n += 2) { if (c % n === 0) { return false; } } + return true; +}; + +const unaryOperator: ILexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> unary', target, tree); + + const t = tree as any; + const opValue = t.block.lexeme.resolve(target, t.block); + + switch (tree.value.toLowerCase()) { + case 'is even': + return typeof opValue === 'number' && opValue % 2 === 0; + case 'is nil': + return opValue === undefined || opValue === null; + case 'is bool': + return typeof opValue === 'boolean'; + case 'is odd': + return typeof opValue === 'number' && opValue % 2 === 1; + case 'is num': + return typeof opValue === 'number'; + case 'is object': + return opValue !== null && typeof opValue === 'object'; + case 'is str': + return typeof opValue === 'string'; + case 'is prime': + return typeof opValue === 'number' && isPrime(opValue); + default: + throw new Error(); + } + }, + name: LexemeType.UnaryOperator, + priority: 0, + regexp: /^((is nil)|(is odd)|(is even)|(is bool)|(is num)|(is object)|(is str)|(is prime))/i, + syntaxer: (lexs: any[]) => { + let [block, lexeme] = lexs; + + return Object.assign({ block }, lexeme); + }, + when: [LexemeType.Operand] +}; + +export default unaryOperator; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts new file mode 100644 index 000000000..c10408b53 --- /dev/null +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -0,0 +1,15 @@ +import { + binaryOperator, + expression, + operand, + unaryNot, + unaryOperator +} from '../lexeme'; + +export default [ + operand, + binaryOperator, + unaryOperator, + unaryNot, + expression +]; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts new file mode 100644 index 000000000..835364f90 --- /dev/null +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -0,0 +1,9 @@ +import { + and +} from '../lexeme'; +import column from './column'; + +export default [ + and, + ...column +]; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts new file mode 100644 index 000000000..84496fa91 --- /dev/null +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -0,0 +1,23 @@ +import { + and, + binaryOperator, + blockClose, + blockOpen, + expression, + operand, + or, + unaryNot, + unaryOperator +} from '../lexeme'; + +export default [ + and, + or, + blockClose, + blockOpen, + operand, + binaryOperator, + unaryOperator, + unaryNot, + expression +]; \ No newline at end of file diff --git a/tests/cypress/tests/unit/syntactic_tree_test.ts b/tests/cypress/tests/unit/syntactic_tree_test.ts index 59640a040..339b44572 100644 --- a/tests/cypress/tests/unit/syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/syntactic_tree_test.ts @@ -1,4 +1,4 @@ -import SyntaxTree from 'core/syntax-tree'; +import { QuerySyntaxTree } from 'dash-table/syntax-tree'; describe('Syntax Tree', () => { const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '\'""\'': '0\'"dot' }; @@ -8,31 +8,31 @@ describe('Syntax Tree', () => { describe('operands', () => { it('does not support badly formed operands', () => { - expect(new SyntaxTree(`'myField' eq num(0)`).isValid).to.equal(true); - expect(new SyntaxTree(`"myField" eq num(0)`).isValid).to.equal(true); - expect(new SyntaxTree('`myField` eq num(0)').isValid).to.equal(true); - expect(new SyntaxTree(`'myField\\' eq num(0)`).isValid).to.equal(false); - expect(new SyntaxTree(`"myField\\" eq num(0)`).isValid).to.equal(false); - expect(new SyntaxTree('`myField\\` eq num(0)').isValid).to.equal(false); - expect(new SyntaxTree(`\\'myField' eq num(0)`).isValid).to.equal(false); - expect(new SyntaxTree(`\\"myField" eq num(0)`).isValid).to.equal(false); - expect(new SyntaxTree('\\`myField` eq num(0)').isValid).to.equal(false); + expect(new QuerySyntaxTree(`'myField' eq num(0)`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`"myField" eq num(0)`).isValid).to.equal(true); + expect(new QuerySyntaxTree('`myField` eq num(0)').isValid).to.equal(true); + expect(new QuerySyntaxTree(`'myField\\' eq num(0)`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`"myField\\" eq num(0)`).isValid).to.equal(false); + expect(new QuerySyntaxTree('`myField\\` eq num(0)').isValid).to.equal(false); + expect(new QuerySyntaxTree(`\\'myField' eq num(0)`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`\\"myField" eq num(0)`).isValid).to.equal(false); + expect(new QuerySyntaxTree('\\`myField` eq num(0)').isValid).to.equal(false); }); it('does not support badly formed expression', () => { - expect(new SyntaxTree(`myField eq 'value'`).isValid).to.equal(true); - expect(new SyntaxTree(`myField eq "value"`).isValid).to.equal(true); - expect(new SyntaxTree('myField eq `value`').isValid).to.equal(true); - expect(new SyntaxTree(`myField eq 'value\\'`).isValid).to.equal(false); - expect(new SyntaxTree(`myField eq "value\\"`).isValid).to.equal(false); - expect(new SyntaxTree('myField eq `value\\`').isValid).to.equal(false); - expect(new SyntaxTree(`myField eq \\'value'`).isValid).to.equal(false); - expect(new SyntaxTree(`myField eq \\"value"`).isValid).to.equal(false); - expect(new SyntaxTree('myField eq \\`value`').isValid).to.equal(false); + expect(new QuerySyntaxTree(`myField eq 'value'`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`myField eq "value"`).isValid).to.equal(true); + expect(new QuerySyntaxTree('myField eq `value`').isValid).to.equal(true); + expect(new QuerySyntaxTree(`myField eq 'value\\'`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`myField eq "value\\"`).isValid).to.equal(false); + expect(new QuerySyntaxTree('myField eq `value\\`').isValid).to.equal(false); + expect(new QuerySyntaxTree(`myField eq \\'value'`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`myField eq \\"value"`).isValid).to.equal(false); + expect(new QuerySyntaxTree('myField eq \\`value`').isValid).to.equal(false); }); it('support arbitrary quoted column name', () => { - const tree = new SyntaxTree(`'_-6.:+** *@$' eq "1*dot" || '_-6.:+** *@$' eq "2*dot"`); + const tree = new QuerySyntaxTree(`'_-6.:+** *@$' eq "1*dot" || '_-6.:+** *@$' eq "2*dot"`); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -42,7 +42,7 @@ describe('Syntax Tree', () => { }); it('support column name with "."', () => { - const tree = new SyntaxTree('a.dot eq "1.dot" || a.dot eq "2.dot"'); + const tree = new QuerySyntaxTree('a.dot eq "1.dot" || a.dot eq "2.dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -52,7 +52,7 @@ describe('Syntax Tree', () => { }); it('support column name with "-"', () => { - const tree = new SyntaxTree('a-dot eq "1-dot" || a-dot eq "2-dot"'); + const tree = new QuerySyntaxTree('a-dot eq "1-dot" || a-dot eq "2-dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -62,7 +62,7 @@ describe('Syntax Tree', () => { }); it('support column name with "_"', () => { - const tree = new SyntaxTree('a_dot eq "1_dot" || a_dot eq "2_dot"'); + const tree = new QuerySyntaxTree('a_dot eq "1_dot" || a_dot eq "2_dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -72,7 +72,7 @@ describe('Syntax Tree', () => { }); it('support column name with "+"', () => { - const tree = new SyntaxTree('a+dot eq "1+dot" || a+dot eq "2+dot"'); + const tree = new QuerySyntaxTree('a+dot eq "1+dot" || a+dot eq "2+dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -82,7 +82,7 @@ describe('Syntax Tree', () => { }); it('support column name with ":"', () => { - const tree = new SyntaxTree('a:dot eq "1:dot" || a:dot eq "2:dot"'); + const tree = new QuerySyntaxTree('a:dot eq "1:dot" || a:dot eq "2:dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -92,7 +92,7 @@ describe('Syntax Tree', () => { }); it('support double quoted column name with " " (space)', () => { - const tree = new SyntaxTree('"a dot" eq "1 dot" || "a dot" eq "2 dot"'); + const tree = new QuerySyntaxTree('"a dot" eq "1 dot" || "a dot" eq "2 dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -102,7 +102,7 @@ describe('Syntax Tree', () => { }); it('support single quoted column name with " " (space)', () => { - const tree = new SyntaxTree('\'a dot\' eq "1 dot" || \'a dot\' eq "2 dot"'); + const tree = new QuerySyntaxTree('\'a dot\' eq "1 dot" || \'a dot\' eq "2 dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -112,7 +112,7 @@ describe('Syntax Tree', () => { }); it('support nesting in quotes', () => { - const tree = new SyntaxTree(`\`'""'\` eq \`1'"dot\` || \`'""'\` eq \`2'"dot\``); + const tree = new QuerySyntaxTree(`\`'""'\` eq \`1'"dot\` || \`'""'\` eq \`2'"dot\``); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -124,7 +124,7 @@ describe('Syntax Tree', () => { describe('&& and ||', () => { it('can || two conditions', () => { - const tree = new SyntaxTree('a eq "1" || a eq "2"'); + const tree = new QuerySyntaxTree('a eq "1" || a eq "2"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -134,7 +134,7 @@ describe('Syntax Tree', () => { }); it('can "or" two conditions', () => { - const tree = new SyntaxTree('a eq "1" or a eq "2"'); + const tree = new QuerySyntaxTree('a eq "1" or a eq "2"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -144,7 +144,7 @@ describe('Syntax Tree', () => { }); it('can && two conditions', () => { - const tree = new SyntaxTree('a eq "1" && b eq "0"'); + const tree = new QuerySyntaxTree('a eq "1" && b eq "0"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -154,7 +154,7 @@ describe('Syntax Tree', () => { }); it('can "and" two conditions', () => { - const tree = new SyntaxTree('a eq "1" and b eq "0"'); + const tree = new QuerySyntaxTree('a eq "1" and b eq "0"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -164,7 +164,7 @@ describe('Syntax Tree', () => { }); it('gives priority to && over ||', () => { - const tree = new SyntaxTree('a eq "1" && a eq "0" || b eq "1"'); + const tree = new QuerySyntaxTree('a eq "1" && a eq "0" || b eq "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -174,7 +174,7 @@ describe('Syntax Tree', () => { }); it('gives priority to "and" over "or"', () => { - const tree = new SyntaxTree('a eq "1" and a eq "0" or b eq "1"'); + const tree = new QuerySyntaxTree('a eq "1" and a eq "0" or b eq "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -186,7 +186,7 @@ describe('Syntax Tree', () => { describe('data types', () => { it('can compare numbers', () => { - const tree = new SyntaxTree('c eq num(1)'); + const tree = new QuerySyntaxTree('c eq num(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -196,7 +196,7 @@ describe('Syntax Tree', () => { }); it('can compare floats', () => { - const tree = new SyntaxTree('field ge num(1.5)'); + const tree = new QuerySyntaxTree('field ge num(1.5)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ field: -1.501 })).to.equal(false); @@ -208,7 +208,7 @@ describe('Syntax Tree', () => { }); it('can compare string to number and return false', () => { - const tree = new SyntaxTree('a eq num(1)'); + const tree = new QuerySyntaxTree('a eq num(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -218,7 +218,7 @@ describe('Syntax Tree', () => { }); it('can compare strings', () => { - const tree = new SyntaxTree('a eq str(1)'); + const tree = new QuerySyntaxTree('a eq str(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -228,7 +228,7 @@ describe('Syntax Tree', () => { }); it('can compare string to number and return false', () => { - const tree = new SyntaxTree('c eq str(1)'); + const tree = new QuerySyntaxTree('c eq str(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -240,7 +240,7 @@ describe('Syntax Tree', () => { describe('block', () => { it('has priority over && and ||', () => { - const tree = new SyntaxTree('a eq "1" && (a eq "0" || b eq "1")'); + const tree = new QuerySyntaxTree('a eq "1" && (a eq "0" || b eq "1")'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -250,7 +250,7 @@ describe('Syntax Tree', () => { }); it('gives priority over "and" and "or"', () => { - const tree = new SyntaxTree('a eq "1" and (a eq "0" or b eq "1")'); + const tree = new QuerySyntaxTree('a eq "1" and (a eq "0" or b eq "1")'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -260,7 +260,7 @@ describe('Syntax Tree', () => { }); it('can be uselessly nested', () => { - const tree = new SyntaxTree('((a eq "1" and (((a eq "0" or b eq "1")))))'); + const tree = new QuerySyntaxTree('((a eq "1" and (((a eq "0" or b eq "1")))))'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -270,7 +270,7 @@ describe('Syntax Tree', () => { }); it('can be nested', () => { - const tree = new SyntaxTree('a eq "1" and (a eq "0" or (b eq "1" or b eq "0"))'); + const tree = new QuerySyntaxTree('a eq "1" and (a eq "0" or (b eq "1" or b eq "0"))'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -282,7 +282,7 @@ describe('Syntax Tree', () => { describe('unary operators', () => { it('can check nil', () => { - const tree = new SyntaxTree('d is nil'); + const tree = new QuerySyntaxTree('d is nil'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -292,7 +292,7 @@ describe('Syntax Tree', () => { }); it('can invert check nil', () => { - const tree = new SyntaxTree('!(d is nil)'); + const tree = new QuerySyntaxTree('!(d is nil)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -302,7 +302,7 @@ describe('Syntax Tree', () => { }); it('can check odd', () => { - const tree = new SyntaxTree('c is odd'); + const tree = new QuerySyntaxTree('c is odd'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -312,7 +312,7 @@ describe('Syntax Tree', () => { }); it('can check odd on string and return false', () => { - const tree = new SyntaxTree('a is odd'); + const tree = new QuerySyntaxTree('a is odd'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -322,7 +322,7 @@ describe('Syntax Tree', () => { }); it('can check even', () => { - const tree = new SyntaxTree('c is even'); + const tree = new QuerySyntaxTree('c is even'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -332,7 +332,7 @@ describe('Syntax Tree', () => { }); it('can check even on string and return false', () => { - const tree = new SyntaxTree('a is even'); + const tree = new QuerySyntaxTree('a is even'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -342,7 +342,7 @@ describe('Syntax Tree', () => { }); it('can check if string', () => { - const tree = new SyntaxTree('d is str'); + const tree = new QuerySyntaxTree('d is str'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -352,7 +352,7 @@ describe('Syntax Tree', () => { }); it('can check if number', () => { - const tree = new SyntaxTree('d is num'); + const tree = new QuerySyntaxTree('d is num'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -362,7 +362,7 @@ describe('Syntax Tree', () => { }); it('can check if bool', () => { - const tree = new SyntaxTree('d is bool'); + const tree = new QuerySyntaxTree('d is bool'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -372,7 +372,7 @@ describe('Syntax Tree', () => { }); it('can check if object', () => { - const tree = new SyntaxTree('d is object'); + const tree = new QuerySyntaxTree('d is object'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -384,7 +384,7 @@ describe('Syntax Tree', () => { describe('unary not', () => { it('can invert block', () => { - const tree = new SyntaxTree('!(a eq "1")'); + const tree = new QuerySyntaxTree('!(a eq "1")'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -394,7 +394,7 @@ describe('Syntax Tree', () => { }); it('can invert block multiple times', () => { - const tree = new SyntaxTree('!!(a eq "1")'); + const tree = new QuerySyntaxTree('!!(a eq "1")'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -406,7 +406,7 @@ describe('Syntax Tree', () => { describe('logical binary operators', () => { it('can do equality (eq) test', () => { - const tree = new SyntaxTree('a eq "1"'); + const tree = new QuerySyntaxTree('a eq "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -416,7 +416,7 @@ describe('Syntax Tree', () => { }); it('can do equality (=) test', () => { - const tree = new SyntaxTree('a = "1"'); + const tree = new QuerySyntaxTree('a = "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -426,7 +426,7 @@ describe('Syntax Tree', () => { }); it('can do difference (ne) test', () => { - const tree = new SyntaxTree('a ne "1"'); + const tree = new QuerySyntaxTree('a ne "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -436,7 +436,7 @@ describe('Syntax Tree', () => { }); it('can do difference (!=) test', () => { - const tree = new SyntaxTree('a != "1"'); + const tree = new QuerySyntaxTree('a != "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -446,7 +446,7 @@ describe('Syntax Tree', () => { }); it('can do greater than (gt) test', () => { - const tree = new SyntaxTree('a gt "1"'); + const tree = new QuerySyntaxTree('a gt "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -456,7 +456,7 @@ describe('Syntax Tree', () => { }); it('can do greater than (>) test', () => { - const tree = new SyntaxTree('a > "1"'); + const tree = new QuerySyntaxTree('a > "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -466,7 +466,7 @@ describe('Syntax Tree', () => { }); it('can do less than (lt) test', () => { - const tree = new SyntaxTree('a lt "1"'); + const tree = new QuerySyntaxTree('a lt "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -476,7 +476,7 @@ describe('Syntax Tree', () => { }); it('can do less than (<) test', () => { - const tree = new SyntaxTree('a < "1"'); + const tree = new QuerySyntaxTree('a < "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -486,7 +486,7 @@ describe('Syntax Tree', () => { }); it('can do greater or equal to (ge) test', () => { - const tree = new SyntaxTree('a ge "1"'); + const tree = new QuerySyntaxTree('a ge "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -496,7 +496,7 @@ describe('Syntax Tree', () => { }); it('can do greater or equal to (>=) test', () => { - const tree = new SyntaxTree('a >= "1"'); + const tree = new QuerySyntaxTree('a >= "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -506,7 +506,7 @@ describe('Syntax Tree', () => { }); it('can do less or equal to (le) test', () => { - const tree = new SyntaxTree('a le "1"'); + const tree = new QuerySyntaxTree('a le "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -516,7 +516,7 @@ describe('Syntax Tree', () => { }); it('can do less or equal to (<=) test', () => { - const tree = new SyntaxTree('a <= "1"'); + const tree = new QuerySyntaxTree('a <= "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); From 6c6fba91838355876ff24684da7577fff9506a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 13 Mar 2019 11:24:17 -0400 Subject: [PATCH 02/54] - single column tests - rework validation logic (wip) --- src/core/syntax-tree/index.ts | 17 ++++-- src/core/syntax-tree/lexer.ts | 20 ++++--- src/core/syntax-tree/lexicon.ts | 8 ++- src/dash-table/components/FilterFactory.tsx | 2 +- src/dash-table/syntax-tree/index.ts | 43 +++++++++++++- src/dash-table/syntax-tree/lexicon/column.ts | 18 +++--- .../syntax-tree/lexicon/columnMulti.ts | 11 ++-- src/dash-table/syntax-tree/lexicon/query.ts | 25 ++++---- ...e_test.ts => query_syntactic_tree_test.ts} | 2 +- .../unit/single_column_syntatic_tree_test.ts | 58 +++++++++++++++++++ 10 files changed, 162 insertions(+), 42 deletions(-) rename tests/cypress/tests/unit/{syntactic_tree_test.ts => query_syntactic_tree_test.ts} (99%) create mode 100644 tests/cypress/tests/unit/single_column_syntatic_tree_test.ts diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index 419a59d16..188456f9d 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -1,7 +1,9 @@ +import * as R from 'ramda'; + import Logger from 'core/Logger'; import lexer, { ILexerResult } from 'core/syntax-tree/lexer'; import syntaxer, { ISyntaxerResult } from 'core/syntax-tree/syntaxer'; -import { ILexeme } from './lexicon'; +import { ILexicon } from './lexicon'; export default class SyntaxTree { protected lexerResult: ILexerResult; @@ -16,10 +18,11 @@ export default class SyntaxTree { } constructor( - private readonly lexicon: ILexeme[], - private readonly query: string + private readonly lexicon: ILexicon, + private readonly query: string, + modifyLex: (res: ILexerResult) => ILexerResult = res => res ) { - this.lexerResult = lexer(this.lexicon, this.query); + this.lexerResult = modifyLex(lexer(this.lexicon, this.query)); this.syntaxerResult = syntaxer(this.lexerResult); } @@ -39,4 +42,10 @@ export default class SyntaxTree { filter = (targets: any[]) => { return targets.filter(this.evaluate); } + + toQueryString() { + return this.lexerResult.valid ? + R.map(l => l.value, this.lexerResult.lexemes).join(' ') : + ''; + } } \ No newline at end of file diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts index 40dad3707..2e31f3ea6 100644 --- a/src/core/syntax-tree/lexer.ts +++ b/src/core/syntax-tree/lexer.ts @@ -1,4 +1,4 @@ -import { ILexeme } from 'core/syntax-tree/lexicon'; +import { ILexeme, ILexicon } from 'core/syntax-tree/lexicon'; export interface ILexerResult { lexemes: ILexemeResult[]; @@ -11,20 +11,20 @@ export interface ILexemeResult { value?: string; } -export default function lexer(lexicon: ILexeme[], query: string): ILexerResult { +export default function lexer(lexicon: ILexicon, query: string): ILexerResult { let lexeme: ILexeme | null = null; let result: ILexemeResult[] = []; while (query.length) { query = query.replace(/^\s+/, ''); - let lexemes: ILexeme[] = lexicon.filter(_lexeme => - lexeme && - _lexeme.when && - _lexeme.when.indexOf(lexeme.name) !== -1); + let lexemes: ILexeme[] = lexicon.lexemes.filter(_lexeme => lexeme ? + _lexeme.when && _lexeme.when.indexOf(lexeme.name) !== -1 : + _lexeme.when && _lexeme.when.indexOf(undefined) !== -1 + ); - if (!lexemes.length) { - lexemes = lexicon; + if (lexicon.allowFreeForm && !lexemes.length) { + lexemes = lexicon.lexemes; } lexeme = lexemes.find(_lexeme => _lexeme.regexp.test(query)) || null; @@ -38,5 +38,7 @@ export default function lexer(lexicon: ILexeme[], query: string): ILexerResult { query = query.substring(value.length); } - return { lexemes: result, valid: true }; + const last = result.slice(-1)[0]; + + return { lexemes: result, valid: !last || last.lexeme.terminal !== false }; } \ No newline at end of file diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 627c132f6..377995b9b 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -20,5 +20,11 @@ export interface ILexeme { priority?: number; regexp: RegExp; syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any; - when?: string[]; + terminal?: boolean; + when?: (string | undefined)[]; +} + +export interface ILexicon { + allowFreeForm: boolean; + lexemes: ILexeme[]; } \ No newline at end of file diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index f5e9f660e..7c8f87f0b 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -103,7 +103,7 @@ export default class FilterFactory { private isFragmentValid(columnId: ColumnId) { const op = this.ops.get(columnId.toString()); - const ast = new SingleColumnSyntaxTree(`"${columnId}" ${op}`); + const ast = new SingleColumnSyntaxTree(columnId, op || ''); return ast.isValid; } diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index 6dd1202af..535cda234 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -1,16 +1,55 @@ import * as R from 'ramda'; import SyntaxTree from 'core/syntax-tree'; +import { ILexerResult, ILexemeResult } from 'core/syntax-tree/lexer'; import { LexemeType } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; +import { ColumnId } from 'dash-table/components/Table/props'; + +import { operand, binaryOperator } from './lexeme'; import queryLexicon from './lexicon/query'; import columnLexicon from './lexicon/column'; import columnMultiLexicon from './lexicon/columnMulti'; +function isBinary(lexemes: ILexemeResult[]) { + return lexemes.length === 2; +} + +function isExpression(lexemes: ILexemeResult[]) { + return lexemes.length === 1 && + lexemes[0].lexeme.name === LexemeType.Expression; +} + +function isUnary(lexemes: ILexemeResult[]) { + return lexemes.length === 1 && + lexemes[0].lexeme.name === LexemeType.UnaryOperator; +} + +function modifyLex(key: ColumnId, res: ILexerResult) { + if (!res.valid) { + return res; + } + + if (isBinary(res.lexemes) || isUnary(res.lexemes)) { + res.lexemes = [ + { lexeme: operand, value: `${key}` }, + ...res.lexemes + ]; + } else if (isExpression(res.lexemes)) { + res.lexemes = [ + { lexeme: operand, value: `${key}` }, + { lexeme: binaryOperator, value: 'eq' }, + ...res.lexemes + ]; + } + + return res; +} + export class SingleColumnSyntaxTree extends SyntaxTree { - constructor(query: string) { - super(columnLexicon, query); + constructor(key: ColumnId, query: string) { + super(columnLexicon, query, modifyLex.bind(undefined, key)); } } diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index c10408b53..4667c8fb1 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -1,15 +1,15 @@ import { binaryOperator, expression, - operand, - unaryNot, unaryOperator } from '../lexeme'; +import { LexemeType } from 'core/syntax-tree/lexicon'; -export default [ - operand, - binaryOperator, - unaryOperator, - unaryNot, - expression -]; \ No newline at end of file +export default { + allowFreeForm: false, + lexemes: [ + { ...binaryOperator, when: [undefined], terminal: false }, + { ...unaryOperator, when: [undefined] }, + { ...expression, when: [undefined, LexemeType.BinaryOperator] } + ] +}; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index 835364f90..4411c9e18 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -3,7 +3,10 @@ import { } from '../lexeme'; import column from './column'; -export default [ - and, - ...column -]; \ No newline at end of file +export default { + allowFreeForm: true, + lexemes: [ + and, + ...column.lexemes + ] +}; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index 84496fa91..87772139c 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -10,14 +10,17 @@ import { unaryOperator } from '../lexeme'; -export default [ - and, - or, - blockClose, - blockOpen, - operand, - binaryOperator, - unaryOperator, - unaryNot, - expression -]; \ No newline at end of file +export default { + allowFreeForm: true, + lexemes: [ + and, + or, + blockClose, + blockOpen, + operand, + binaryOperator, + unaryOperator, + unaryNot, + expression + ] +}; \ No newline at end of file diff --git a/tests/cypress/tests/unit/syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts similarity index 99% rename from tests/cypress/tests/unit/syntactic_tree_test.ts rename to tests/cypress/tests/unit/query_syntactic_tree_test.ts index 339b44572..882b72a1a 100644 --- a/tests/cypress/tests/unit/syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -1,6 +1,6 @@ import { QuerySyntaxTree } from 'dash-table/syntax-tree'; -describe('Syntax Tree', () => { +describe('Query Syntax Tree', () => { const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '\'""\'': '0\'"dot' }; const data1 = { a: '1', b: '0', c: 1, d: 0, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot', '\'""\'': '1\'"dot' }; const data2 = { a: '2', b: '1', c: 2, d: '', 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '\'""\'': '2\'"dot' }; diff --git a/tests/cypress/tests/unit/single_column_syntatic_tree_test.ts b/tests/cypress/tests/unit/single_column_syntatic_tree_test.ts new file mode 100644 index 000000000..f618d2883 --- /dev/null +++ b/tests/cypress/tests/unit/single_column_syntatic_tree_test.ts @@ -0,0 +1,58 @@ +import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; + +describe('Single Column Syntax Tree', () => { + it('cannot have operand', () => { + const tree = new SingleColumnSyntaxTree('a', 'a <= num(1)'); + + expect(tree.isValid).to.equal(false); + }); + + it('cannot have binary dangle', () => { + const tree = new SingleColumnSyntaxTree('a', '<='); + + expect(tree.isValid).to.equal(false); + }); + + it('cannot be unary + expression', () => { + const tree = new SingleColumnSyntaxTree('a', 'is prime "a"'); + + expect(tree.isValid).to.equal(false); + }); + + it('can be empty', () => { + const tree = new SingleColumnSyntaxTree('a', ''); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 0 })).to.equal(true); + }); + + it('can be binary + expression', () => { + const tree = new SingleColumnSyntaxTree('a', '<= num(1)'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 0 })).to.equal(true); + expect(tree.evaluate({ a: 2 })).to.equal(false); + + expect(tree.toQueryString()).to.equal('a <= num(1)'); + }); + + it('can be unary', () => { + const tree = new SingleColumnSyntaxTree('a', 'is prime'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 5 })).to.equal(true); + expect(tree.evaluate({ a: 6 })).to.equal(false); + + expect(tree.toQueryString()).to.equal('a is prime'); + }); + + it('can be expression', () => { + const tree = new SingleColumnSyntaxTree('"a"', 'num(1)'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 1 })).to.equal(true); + expect(tree.evaluate({ a: 2 })).to.equal(false); + + expect(tree.toQueryString()).to.equal('"a" eq num(1)'); + }); +}); \ No newline at end of file From 86b092972da3757eb4145397f9d3da2fb06d172e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 13 Mar 2019 18:55:47 -0400 Subject: [PATCH 03/54] - lexer state machine --- src/core/syntax-tree/lexer.ts | 42 +++--- src/core/syntax-tree/lexicon.ts | 21 +-- src/dash-table/syntax-tree/index.ts | 8 +- src/dash-table/syntax-tree/lexeme/and.ts | 4 +- .../syntax-tree/lexeme/binaryOperator.ts | 7 +- src/dash-table/syntax-tree/lexeme/block.ts | 9 +- .../syntax-tree/lexeme/expression.ts | 7 +- src/dash-table/syntax-tree/lexeme/operand.ts | 4 +- src/dash-table/syntax-tree/lexeme/or.ts | 4 +- src/dash-table/syntax-tree/lexeme/unaryNot.ts | 7 +- .../syntax-tree/lexeme/unaryOperator.ts | 7 +- src/dash-table/syntax-tree/lexicon/column.ts | 32 +++-- .../syntax-tree/lexicon/columnMulti.ts | 29 ++-- src/dash-table/syntax-tree/lexicon/query.ts | 130 ++++++++++++++++-- 14 files changed, 225 insertions(+), 86 deletions(-) diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts index 2e31f3ea6..bd5b6bbfc 100644 --- a/src/core/syntax-tree/lexer.ts +++ b/src/core/syntax-tree/lexer.ts @@ -1,4 +1,4 @@ -import { ILexeme, ILexicon } from 'core/syntax-tree/lexicon'; +import { ILexeme, Lexicon } from 'core/syntax-tree/lexicon'; export interface ILexerResult { lexemes: ILexemeResult[]; @@ -11,34 +11,44 @@ export interface ILexemeResult { value?: string; } -export default function lexer(lexicon: ILexicon, query: string): ILexerResult { - let lexeme: ILexeme | null = null; - +export default function lexer(lexicon: Lexicon, query: string): ILexerResult { let result: ILexemeResult[] = []; + while (query.length) { query = query.replace(/^\s+/, ''); - let lexemes: ILexeme[] = lexicon.lexemes.filter(_lexeme => lexeme ? - _lexeme.when && _lexeme.when.indexOf(lexeme.name) !== -1 : - _lexeme.when && _lexeme.when.indexOf(undefined) !== -1 + const previous = result.slice(-1)[0]; + const previousLexeme = previous ? previous.lexeme : null; + + let lexemes: ILexeme[] = lexicon.filter(lexeme => + lexeme.if && + (!Array.isArray(lexeme.if) ? + lexeme.if(result, previous) : + (previousLexeme ? + lexeme.if && lexeme.if.indexOf(previousLexeme.name) !== -1 : + lexeme.if && lexeme.if.indexOf(undefined) !== -1) + ) ); - if (lexicon.allowFreeForm && !lexemes.length) { - lexemes = lexicon.lexemes; - } - - lexeme = lexemes.find(_lexeme => _lexeme.regexp.test(query)) || null; - if (!lexeme) { + const next = lexemes.find(_lexeme => _lexeme.regexp.test(query)) || null; + if (!next) { return { lexemes: result, valid: false, error: query }; } - const value = (query.match(lexeme.regexp) || [])[0]; - result.push({ lexeme, value }); + const value = (query.match(next.regexp) || [])[0]; + result.push({ lexeme: next, value }); query = query.substring(value.length); } const last = result.slice(-1)[0]; - return { lexemes: result, valid: !last || last.lexeme.terminal !== false }; + const terminal: boolean = last && (typeof last.lexeme.terminal === 'function' ? + last.lexeme.terminal(result, last) : + last.lexeme.terminal); + + return { + lexemes: result, + valid: !last || terminal + }; } \ No newline at end of file diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 377995b9b..212ddd82a 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -1,4 +1,5 @@ -import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; +import { ILexemeResult } from './lexer'; +import { ISyntaxTree } from './syntaxer'; export enum LexemeType { And = 'and', @@ -12,7 +13,7 @@ export enum LexemeType { UnaryOperator = 'logical-unary-operator' } -export interface ILexeme { +export interface IUnboundedLexeme { evaluate?: (target: any, tree: ISyntaxTree) => boolean; resolve?: (target: any, tree: ISyntaxTree) => any; name: string; @@ -20,11 +21,15 @@ export interface ILexeme { priority?: number; regexp: RegExp; syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any; - terminal?: boolean; - when?: (string | undefined)[]; } -export interface ILexicon { - allowFreeForm: boolean; - lexemes: ILexeme[]; -} \ No newline at end of file +export interface ILexeme extends IUnboundedLexeme { + terminal: boolean | ((lexemes: ILexemeResult[], previous: ILexemeResult) => boolean); + if: (string | undefined)[] | ((lexemes: ILexemeResult[], previous: ILexemeResult) => boolean); +} + +export function boundLexeme(lexeme: IUnboundedLexeme) { + return { ...lexeme, if: () => false, terminal: false }; +} + +export type Lexicon = ILexeme[]; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index 535cda234..6b75afb90 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -2,7 +2,7 @@ import * as R from 'ramda'; import SyntaxTree from 'core/syntax-tree'; import { ILexerResult, ILexemeResult } from 'core/syntax-tree/lexer'; -import { LexemeType } from 'core/syntax-tree/lexicon'; +import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; import { ColumnId } from 'dash-table/components/Table/props'; @@ -33,13 +33,13 @@ function modifyLex(key: ColumnId, res: ILexerResult) { if (isBinary(res.lexemes) || isUnary(res.lexemes)) { res.lexemes = [ - { lexeme: operand, value: `${key}` }, + { lexeme: boundLexeme(operand), value: `${key}` }, ...res.lexemes ]; } else if (isExpression(res.lexemes)) { res.lexemes = [ - { lexeme: operand, value: `${key}` }, - { lexeme: binaryOperator, value: 'eq' }, + { lexeme: boundLexeme(operand), value: `${key}` }, + { lexeme: boundLexeme(binaryOperator), value: 'eq' }, ...res.lexemes ]; } diff --git a/src/dash-table/syntax-tree/lexeme/and.ts b/src/dash-table/syntax-tree/lexeme/and.ts index e9d5bae4b..314c521ec 100644 --- a/src/dash-table/syntax-tree/lexeme/and.ts +++ b/src/dash-table/syntax-tree/lexeme/and.ts @@ -1,7 +1,7 @@ import Logger from 'core/Logger'; -import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -const and: ILexeme = { +const and: IUnboundedLexeme = { evaluate: (target, tree) => { Logger.trace('evaluate -> &&', target, tree); diff --git a/src/dash-table/syntax-tree/lexeme/binaryOperator.ts b/src/dash-table/syntax-tree/lexeme/binaryOperator.ts index 171ceea2b..6f9dfef5f 100644 --- a/src/dash-table/syntax-tree/lexeme/binaryOperator.ts +++ b/src/dash-table/syntax-tree/lexeme/binaryOperator.ts @@ -1,7 +1,7 @@ import Logger from 'core/Logger'; -import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -const binaryOperator: ILexeme = { +const binaryOperator: IUnboundedLexeme = { evaluate: (target, tree) => { Logger.trace('evaluate -> binary', target, tree); @@ -41,8 +41,7 @@ const binaryOperator: ILexeme = { let [left, lexeme, right] = lexs; return Object.assign({ left, right }, lexeme); - }, - when: [LexemeType.Operand] + } }; export default binaryOperator; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/block.ts b/src/dash-table/syntax-tree/lexeme/block.ts index 5a1fb14f2..dd0076882 100644 --- a/src/dash-table/syntax-tree/lexeme/block.ts +++ b/src/dash-table/syntax-tree/lexeme/block.ts @@ -1,13 +1,13 @@ import Logger from 'core/Logger'; -import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -export const blockClose: ILexeme = { +export const blockClose: IUnboundedLexeme = { name: LexemeType.BlockClose, nesting: -1, regexp: /^\)/ }; -export const blockOpen: ILexeme = { +export const blockOpen: IUnboundedLexeme = { evaluate: (target, tree) => { Logger.trace('evaluate -> ()', target, tree); @@ -23,6 +23,5 @@ export const blockOpen: ILexeme = { return Object.assign({ block: lexs.slice(1, lexs.length - 1) }, lexs[0]); - }, - when: [LexemeType.UnaryNot] + } }; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index a664cf586..a671b64c5 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -1,7 +1,7 @@ -import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -const expression: ILexeme = { +const expression: IUnboundedLexeme = { resolve: (target: any, tree: ISyntaxTree) => { if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { return tree.value.slice(1, tree.value.length - 1); @@ -25,8 +25,7 @@ const expression: ILexeme = { } }, regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/, - name: LexemeType.Expression, - when: [LexemeType.BinaryOperator] + name: LexemeType.Expression }; export default expression; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts index 2ad7ae615..b5c08fb58 100644 --- a/src/dash-table/syntax-tree/lexeme/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -1,7 +1,7 @@ -import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -const operand: ILexeme = { +const operand: IUnboundedLexeme = { resolve: (target: any, tree: ISyntaxTree) => { if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.")+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { return target[ diff --git a/src/dash-table/syntax-tree/lexeme/or.ts b/src/dash-table/syntax-tree/lexeme/or.ts index 362afe4d5..93471058c 100644 --- a/src/dash-table/syntax-tree/lexeme/or.ts +++ b/src/dash-table/syntax-tree/lexeme/or.ts @@ -1,7 +1,7 @@ import Logger from 'core/Logger'; -import { ILexeme, LexemeType } from 'core/syntax-tree/lexicon'; +import { IUnboundedLexeme, LexemeType } from 'core/syntax-tree/lexicon'; -const or: ILexeme = { +const or: IUnboundedLexeme = { evaluate: (target, tree) => { Logger.trace('evaluate -> ||', target, tree); diff --git a/src/dash-table/syntax-tree/lexeme/unaryNot.ts b/src/dash-table/syntax-tree/lexeme/unaryNot.ts index 9c97a4f5b..0e7f5e2db 100644 --- a/src/dash-table/syntax-tree/lexeme/unaryNot.ts +++ b/src/dash-table/syntax-tree/lexeme/unaryNot.ts @@ -1,7 +1,7 @@ import Logger from 'core/Logger'; -import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -const unaryNot: ILexeme = { +const unaryNot: IUnboundedLexeme = { evaluate: (target, tree) => { Logger.trace('evaluate -> unary not', target, tree); @@ -16,8 +16,7 @@ const unaryNot: ILexeme = { return Object.assign({ block: lexs.slice(1, lexs.length) }, lexs[0]); - }, - when: [LexemeType.UnaryNot] + } }; export default unaryNot; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unaryOperator.ts b/src/dash-table/syntax-tree/lexeme/unaryOperator.ts index 542314f7f..3a6c31686 100644 --- a/src/dash-table/syntax-tree/lexeme/unaryOperator.ts +++ b/src/dash-table/syntax-tree/lexeme/unaryOperator.ts @@ -1,5 +1,5 @@ import Logger from 'core/Logger'; -import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; const isPrime = (c: number) => { if (c === 2) { return true; } @@ -8,7 +8,7 @@ const isPrime = (c: number) => { return true; }; -const unaryOperator: ILexeme = { +const unaryOperator: IUnboundedLexeme = { evaluate: (target, tree) => { Logger.trace('evaluate -> unary', target, tree); @@ -43,8 +43,7 @@ const unaryOperator: ILexeme = { let [block, lexeme] = lexs; return Object.assign({ block }, lexeme); - }, - when: [LexemeType.Operand] + } }; export default unaryOperator; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index 4667c8fb1..dc499f9ba 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -3,13 +3,27 @@ import { expression, unaryOperator } from '../lexeme'; -import { LexemeType } from 'core/syntax-tree/lexicon'; +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; +import { ILexemeResult } from 'core/syntax-tree/lexer'; -export default { - allowFreeForm: false, - lexemes: [ - { ...binaryOperator, when: [undefined], terminal: false }, - { ...unaryOperator, when: [undefined] }, - { ...expression, when: [undefined, LexemeType.BinaryOperator] } - ] -}; \ No newline at end of file +const lexicon: ILexeme[] = [ + { + ...binaryOperator, + terminal: false, + if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous + }, + { + ...unaryOperator, + if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous, + terminal: true + }, + { + ...expression, + if: (_lexs: ILexemeResult[], previous: ILexemeResult) => + !previous || + previous.lexeme.name === LexemeType.BinaryOperator, + terminal: true + } +]; + +export default lexicon; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index 4411c9e18..926e43202 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -1,12 +1,25 @@ import { and } from '../lexeme'; -import column from './column'; +import columnLexicon from './column'; +import { ILexeme, LexemeType } from 'core/syntax-tree/lexicon'; +import { ILexemeResult } from 'core/syntax-tree/lexer'; +import R from 'ramda'; -export default { - allowFreeForm: true, - lexemes: [ - and, - ...column.lexemes - ] -}; \ No newline at end of file +const lexicon: ILexeme[] = [ + { + ...and, + if: (_: ILexemeResult[], previous: ILexemeResult) => + previous && R.contains( + previous.lexeme.name, + [ + LexemeType.Expression, + LexemeType.UnaryOperator + ] + ), + terminal: false + }, + ...columnLexicon +]; + +export default lexicon; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index 87772139c..632a545c5 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -1,3 +1,5 @@ +import * as R from 'ramda'; + import { and, binaryOperator, @@ -9,18 +11,118 @@ import { unaryNot, unaryOperator } from '../lexeme'; +import { ILexemeResult } from 'core/syntax-tree/lexer'; +import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; + +const nestingReducer = R.reduce( + (nesting, l) => nesting + (l.lexeme.nesting || 0) +); + +const isTerminal = (lexemes: ILexemeResult[], previous: ILexemeResult) => + previous && nestingReducer(0, lexemes) === 0; + +const ifExpression = (_: ILexemeResult[], previous: ILexemeResult) => + previous && R.contains( + previous.lexeme.name, + [LexemeType.BinaryOperator] + ); + +const ifLogicalOperator = (_: ILexemeResult[], previous: ILexemeResult) => + previous && R.contains( + previous.lexeme.name, + [ + LexemeType.BlockClose, + LexemeType.Expression, + LexemeType.UnaryOperator + ] + ); + +const ifOperator = (_: ILexemeResult[], previous: ILexemeResult) => + previous && R.contains( + previous.lexeme.name, + [LexemeType.Operand] + ); + +const lexicon: ILexeme[] = [ + { + ...and, + if: ifLogicalOperator, + terminal: false + }, + { + ...or, + if: ifLogicalOperator, + terminal: false + }, + { + ...blockClose, + if: (lexemes: ILexemeResult[], previous: ILexemeResult) => + !previous || R.contains( + previous.lexeme.name, + [ + LexemeType.BlockClose, + LexemeType.BlockOpen, + LexemeType.Expression, + LexemeType.UnaryOperator + ] + ) && nestingReducer(0, lexemes) > 0, + terminal: isTerminal + }, + { + ...blockOpen, + if: (_: ILexemeResult[], previous: ILexemeResult) => + !previous || R.contains( + previous.lexeme.name, + [ + LexemeType.And, + LexemeType.BlockOpen, + LexemeType.Or, + LexemeType.UnaryNot + ] + ), + terminal: false + }, + { + ...operand, + if: (_: ILexemeResult[], previous: ILexemeResult) => + !previous || R.contains( + previous.lexeme.name, + [ + LexemeType.And, + LexemeType.BlockOpen, + LexemeType.Or + ] + ), + terminal: false + }, + { + ...binaryOperator, + if: ifOperator, + terminal: false + }, + { + ...unaryOperator, + if: ifOperator, + terminal: isTerminal + }, + { + ...unaryNot, + if: (_: ILexemeResult[], previous: ILexemeResult) => + !previous || R.contains( + previous.lexeme.name, + [ + LexemeType.And, + LexemeType.Or, + LexemeType.UnaryNot + ] + ), + terminal: false + }, + { + ...expression, + if: ifExpression, + terminal: isTerminal + } +]; -export default { - allowFreeForm: true, - lexemes: [ - and, - or, - blockClose, - blockOpen, - operand, - binaryOperator, - unaryOperator, - unaryNot, - expression - ] -}; \ No newline at end of file +export default lexicon; \ No newline at end of file From a158c3ca8517f4a40c665fda8d6a4ed51e1e4c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 13 Mar 2019 19:13:56 -0400 Subject: [PATCH 04/54] fix build --- src/core/syntax-tree/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index 188456f9d..9741993d1 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -3,7 +3,7 @@ import * as R from 'ramda'; import Logger from 'core/Logger'; import lexer, { ILexerResult } from 'core/syntax-tree/lexer'; import syntaxer, { ISyntaxerResult } from 'core/syntax-tree/syntaxer'; -import { ILexicon } from './lexicon'; +import { Lexicon } from './lexicon'; export default class SyntaxTree { protected lexerResult: ILexerResult; @@ -18,7 +18,7 @@ export default class SyntaxTree { } constructor( - private readonly lexicon: ILexicon, + private readonly lexicon: Lexicon, private readonly query: string, modifyLex: (res: ILexerResult) => ILexerResult = res => res ) { From e9a35cf8f736b891bb70d945a3c132062d28da53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 13 Mar 2019 20:44:34 -0400 Subject: [PATCH 05/54] - fix multi column query - add basic multi column tests --- src/core/syntax-tree/lexer.ts | 3 +- src/dash-table/syntax-tree/lexicon/column.ts | 8 +++- .../syntax-tree/lexicon/columnMulti.ts | 47 +++++++++++++++++-- .../unit/multi_columns_syntactic_tree.ts | 15 ++++++ ...s => single_column_syntactic_tree_test.ts} | 0 5 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 tests/cypress/tests/unit/multi_columns_syntactic_tree.ts rename tests/cypress/tests/unit/{single_column_syntatic_tree_test.ts => single_column_syntactic_tree_test.ts} (100%) diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts index bd5b6bbfc..b68c7044f 100644 --- a/src/core/syntax-tree/lexer.ts +++ b/src/core/syntax-tree/lexer.ts @@ -26,8 +26,7 @@ export default function lexer(lexicon: Lexicon, query: string): ILexerResult { lexeme.if(result, previous) : (previousLexeme ? lexeme.if && lexeme.if.indexOf(previousLexeme.name) !== -1 : - lexeme.if && lexeme.if.indexOf(undefined) !== -1) - ) + lexeme.if && lexeme.if.indexOf(undefined) !== -1)) ); const next = lexemes.find(_lexeme => _lexeme.regexp.test(query)) || null; diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index dc499f9ba..870c8dc71 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -1,3 +1,5 @@ +import * as R from 'ramda'; + import { binaryOperator, expression, @@ -20,8 +22,10 @@ const lexicon: ILexeme[] = [ { ...expression, if: (_lexs: ILexemeResult[], previous: ILexemeResult) => - !previous || - previous.lexeme.name === LexemeType.BinaryOperator, + !previous || R.contains( + previous.lexeme.name, + [LexemeType.BinaryOperator] + ), terminal: true } ]; diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index 926e43202..ce7c48060 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -1,10 +1,14 @@ +import * as R from 'ramda'; + import { - and + and, + operand, + binaryOperator, + unaryOperator, + expression } from '../lexeme'; -import columnLexicon from './column'; import { ILexeme, LexemeType } from 'core/syntax-tree/lexicon'; import { ILexemeResult } from 'core/syntax-tree/lexer'; -import R from 'ramda'; const lexicon: ILexeme[] = [ { @@ -19,7 +23,42 @@ const lexicon: ILexeme[] = [ ), terminal: false }, - ...columnLexicon + { + ...operand, + if: (_: ILexemeResult[], previous: ILexemeResult) => + !previous || R.contains( + previous.lexeme.name, + [LexemeType.And] + ), + terminal: false + }, + { + ...binaryOperator, + if: (_: ILexemeResult[], previous: ILexemeResult) => + previous && R.contains( + previous.lexeme.name, + [LexemeType.Operand] + ), + terminal: false + }, + { + ...unaryOperator, + if: (_: ILexemeResult[], previous: ILexemeResult) => + previous && R.contains( + previous.lexeme.name, + [LexemeType.Operand] + ), + terminal: true + }, + { + ...expression, + if: (_: ILexemeResult[], previous: ILexemeResult) => + previous && R.contains( + previous.lexeme.name, + [LexemeType.BinaryOperator] + ), + terminal: true + } ]; export default lexicon; \ No newline at end of file diff --git a/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts b/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts new file mode 100644 index 000000000..a4777c65c --- /dev/null +++ b/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts @@ -0,0 +1,15 @@ +import { MultiColumnsSyntaxTree } from 'dash-table/syntax-tree'; + +describe('Multi Columns Syntax Tree', () => { + it('can do single', () => { + const tree = new MultiColumnsSyntaxTree('a >= 3'); + + expect(tree.isValid).to.equal(true); + }); + + it('can "and"', () => { + const tree = new MultiColumnsSyntaxTree('a >= 3 && b is even'); + + expect(tree.isValid).to.equal(true); + }); +}); \ No newline at end of file diff --git a/tests/cypress/tests/unit/single_column_syntatic_tree_test.ts b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts similarity index 100% rename from tests/cypress/tests/unit/single_column_syntatic_tree_test.ts rename to tests/cypress/tests/unit/single_column_syntactic_tree_test.ts From e0d9d37534213f22fe40000eef41f687e4f6dbb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 Mar 2019 10:31:12 -0400 Subject: [PATCH 06/54] - fix UI filtering tests (+ new breaking test) --- src/core/syntax-tree/index.ts | 4 +- src/dash-table/components/FilterFactory.tsx | 53 +++---- .../tests/standalone/filtering_test.ts | 149 ++++++++++-------- 3 files changed, 114 insertions(+), 92 deletions(-) diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index 9741993d1..a98627b7c 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -18,8 +18,8 @@ export default class SyntaxTree { } constructor( - private readonly lexicon: Lexicon, - private readonly query: string, + public readonly lexicon: Lexicon, + public readonly query: string, modifyLex: (res: ILexerResult) => ILexerResult = res => res ) { this.lexerResult = modifyLex(lexer(this.lexicon, this.query)); diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 7c8f87f0b..22b61ffc4 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -30,7 +30,7 @@ export interface IFilterOptions { export default class FilterFactory { private readonly handlers = new Map(); - private readonly ops = new Map(); + private readonly ops = new Map(); private readonly filterStyles = derivedFilterStyles(); private readonly relevantStyles = derivedRelevantFilterStyles(); @@ -42,27 +42,30 @@ export default class FilterFactory { } - private onChange = (columnId: ColumnId, ops: Map, setFilter: SetFilter, ev: any) => { + private onChange = (columnId: ColumnId, ops: Map, setFilter: SetFilter, ev: any) => { Logger.debug('Filter -- onChange', columnId, ev.target.value && ev.target.value.trim()); const value = ev.target.value.trim(); + const safeColumnId = `"${columnId}"`; if (value && value.length) { - ops.set(columnId.toString(), value); + ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, value)); } else { - ops.delete(columnId.toString()); + ops.delete(safeColumnId); } - setFilter(R.map( - ([cId, filter]) => `"${cId}" ${filter}`, + const globalFilter = R.map( + ([, ast]) => (ast && ast.toQueryString()) || '', R.filter( - ([cId]) => this.isFragmentValid(cId), + ([, ast]) => ast && ast.isValid, Array.from(ops.entries()) ) - ).join(' && ')); + ).join(' && '); + + setFilter(globalFilter); } - private getEventHandler = (fn: Function, columnId: ColumnId, ops: Map, setFilter: SetFilter): any => { + private getEventHandler = (fn: Function, columnId: ColumnId, ops: Map, setFilter: SetFilter): any => { const fnHandler = (this.handlers.get(fn) || this.handlers.set(fn, new Map()).get(fn)); const columnIdHandler = (fnHandler.get(columnId) || fnHandler.set(columnId, new Map()).get(columnId)); @@ -72,6 +75,12 @@ export default class FilterFactory { ); } + private getSafeColumnId(columnId: ColumnId) { + const id = columnId.toString(); + + return /^"[^"]+"$/.test(id) ? id : `"${id}"`; + } + private updateOps(query: string) { const ast = new MultiColumnsSyntaxTree(query); @@ -87,27 +96,15 @@ export default class FilterFactory { R.forEach(s => { if (s.lexeme.name === LexemeType.UnaryOperator && s.block) { - this.ops.set(s.block.value, s.value); + let safeColumnId = this.getSafeColumnId(s.block.value); + this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, s.value)); } else if (s.lexeme.name === LexemeType.BinaryOperator && s.left && s.right) { - this.ops.set(s.left.value, `${s.value} ${s.right.value}`); + let safeColumnId = this.getSafeColumnId(s.left.value); + this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, `${s.value} ${s.right.value}`)); } }, statements); } - private isFragmentValidOrNull(columnId: ColumnId) { - const op = this.ops.get(columnId.toString()); - - return !op || !op.trim().length || this.isFragmentValid(columnId); - } - - private isFragmentValid(columnId: ColumnId) { - const op = this.ops.get(columnId.toString()); - - const ast = new SingleColumnSyntaxTree(columnId, op || ''); - - return ast.isValid; - } - public createFilters() { const { columns, @@ -142,13 +139,15 @@ export default class FilterFactory { ); const filters = R.addIndex(R.map)((column, index) => { + const ast = this.ops.get(column.id.toString()); + return (); }, columns); diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index 7c8094004..93ab4cd1e 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -4,77 +4,100 @@ import Key from 'cypress/Key'; import { AppMode } from 'demo/AppMode'; -describe(`filter`, () => { - describe(`special characters`, () => { - beforeEach(() => { - cy.visit(`http://localhost:8080?mode=${AppMode.ColumnsInSpace}`); - DashTable.toggleScroll(false); - }); +describe(`filter special characters`, () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.ColumnsInSpace}`); + DashTable.toggleScroll(false); + }); + + it('can filter on special column id', () => { + DashTable.getFilterById('c cc').click(); + DOM.focused.type(`gt num(90)${Key.Enter}`); + + DashTable.getFilterById('d:dd').click(); + DOM.focused.type(`lt num(12500)${Key.Enter}`); - it('can filter on special column id', () => { - DashTable.getFilterById('c cc').click(); - DOM.focused.type(`gt num(90)${Key.Enter}`); + DashTable.getFilterById('e-ee').click(); + DOM.focused.type(`is prime${Key.Enter}`); - DashTable.getFilterById('d:dd').click(); - DOM.focused.type(`lt num(12500)${Key.Enter}`); + DashTable.getFilterById('f_ff').click(); + DOM.focused.type(`le num(106)${Key.Enter}`); - DashTable.getFilterById('e-ee').click(); - DOM.focused.type(`is prime${Key.Enter}`); + DashTable.getFilterById('g.gg').click(); + DOM.focused.type(`gt num(1000)${Key.Enter}`); - DashTable.getFilterById('f_ff').click(); - DOM.focused.type(`le num(106)${Key.Enter}`); + DashTable.getFilterById('b+bb').click(); + DOM.focused.type(`eq "Wet"${Key.Enter}`); - DashTable.getFilterById('g.gg').click(); - DOM.focused.type(`gt num(1000)${Key.Enter}`); + DashTable.getCellById(0, 'rows').within(() => cy.get('.dash-cell-value').should('have.html', '101')); + DashTable.getCellById(1, 'rows').should('not.exist'); + }); +}); - DashTable.getFilterById('b+bb').click(); - DOM.focused.type(`eq "Wet"${Key.Enter}`); +describe('filter', () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.Filtering}`); + DashTable.toggleScroll(false); + }); - DashTable.getCellById(0, 'rows').within(() => cy.get('.dash-cell-value').should('have.html', '101')); - DashTable.getCellById(1, 'rows').should('not.exist'); - }); + it.only('handles invalid queries', () => { + let cell_0; + let cell_1; + + DashTable.getCellById(0, 'ccc') + .within(() => cy.get('.dash-cell-value') + .then($el => cell_0 = $el[0].innerHTML)); + + DashTable.getCellById(1, 'ccc') + .within(() => cy.get('.dash-cell-value') + .then($el => cell_1 = $el[0].innerHTML)); + + DashTable.getFilterById('ccc').click(); + DOM.focused.type(`gt`); + DashTable.getFilterById('ddd').click(); + DOM.focused.type('num(20000)'); + DashTable.getFilterById('eee').click(); + DOM.focused.type('is prime2'); + DashTable.getFilterById('bbb').click(); + DOM.focused.type(`!`); + + DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_0)); + DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_1)); }); - describe('reset', () => { - beforeEach(() => { - cy.visit(`http://localhost:8080?mode=${AppMode.Filtering}`); - DashTable.toggleScroll(false); - }); - - it('updates results and filter fields', () => { - let cell_0; - let cell_1; - - DashTable.getCellById(0, 'ccc') - .within(() => cy.get('.dash-cell-value') - .then($el => cell_0 = $el[0].innerHTML)); - - DashTable.getCellById(1, 'ccc') - .within(() => cy.get('.dash-cell-value') - .then($el => cell_1 = $el[0].innerHTML)); - - DashTable.getFilterById('ccc').click(); - DOM.focused.type(`gt num(100)`); - DashTable.getFilterById('ddd').click(); - DOM.focused.type('lt num(20000)'); - DashTable.getFilterById('eee').click(); - DOM.focused.type('is prime'); - DashTable.getFilterById('bbb').click(); - DOM.focused.type(`eq "Wet"`); - DashTable.getFilterById('ccc').click(); - - DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '101')); - DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '109')); - - cy.get('.clear-filters').click(); - - DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_0)); - DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_1)); - - DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '')); - DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', '')); - DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '')); - DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', '')); - }); + it('reset updates results and filter fields', () => { + let cell_0; + let cell_1; + + DashTable.getCellById(0, 'ccc') + .within(() => cy.get('.dash-cell-value') + .then($el => cell_0 = $el[0].innerHTML)); + + DashTable.getCellById(1, 'ccc') + .within(() => cy.get('.dash-cell-value') + .then($el => cell_1 = $el[0].innerHTML)); + + DashTable.getFilterById('ccc').click(); + DOM.focused.type(`gt num(100)`); + DashTable.getFilterById('ddd').click(); + DOM.focused.type('lt num(20000)'); + DashTable.getFilterById('eee').click(); + DOM.focused.type('is prime'); + DashTable.getFilterById('bbb').click(); + DOM.focused.type(`eq "Wet"`); + DashTable.getFilterById('ccc').click(); + + DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '101')); + DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '109')); + + cy.get('.clear-filters').click(); + + DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_0)); + DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_1)); + + DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '')); + DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', '')); + DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '')); + DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', '')); }); }); \ No newline at end of file From ba1f414da54e126c5c928d8ca8ed1cdc39e83bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 Mar 2019 12:26:38 -0400 Subject: [PATCH 07/54] - fix filters behavior on invalid query fragments - fix UI invalid styling --- src/dash-table/components/FilterFactory.tsx | 31 +++++++++++++------ src/dash-table/components/Table/index.tsx | 1 + src/dash-table/components/Table/props.ts | 1 + src/dash-table/derived/table/index.tsx | 12 +++++-- .../tests/standalone/filtering_test.ts | 16 ++++++++-- 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 22b61ffc4..878d8d716 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -11,8 +11,9 @@ import derivedFilterStyles from 'dash-table/derived/filter/wrapperStyles'; import { derivedRelevantFilterStyles } from 'dash-table/derived/style'; import { BasicFilters, Cells, Style } from 'dash-table/derived/style/props'; import { MultiColumnsSyntaxTree, SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; +import { memoizeOne } from 'core/memoizer'; -type SetFilter = (filter: string) => void; +type SetFilter = (filter: string, rawFilter: string) => void; export interface IFilterOptions { columns: VisibleColumns; @@ -21,6 +22,7 @@ export interface IFilterOptions { filtering_settings: string; filtering_type: FilteringType; id: string; + rawFilterQuery: string; setFilter: SetFilter; style_cell: Style; style_cell_conditional: Cells; @@ -55,14 +57,22 @@ export default class FilterFactory { } const globalFilter = R.map( - ([, ast]) => (ast && ast.toQueryString()) || '', + ast => (ast && ast.toQueryString()) || '', R.filter( - ([, ast]) => ast && ast.isValid, - Array.from(ops.entries()) + ast => ast && ast.isValid, + Array.from(ops.values()) ) ).join(' && '); - setFilter(globalFilter); + const rawGlobalFilter = R.map( + ast => ast.query || '', + R.filter( + ast => Boolean(ast), + Array.from(ops.values()) + ) + ).join(' && '); + + setFilter(globalFilter, rawGlobalFilter); } private getEventHandler = (fn: Function, columnId: ColumnId, ops: Map, setFilter: SetFilter): any => { @@ -81,7 +91,7 @@ export default class FilterFactory { return /^"[^"]+"$/.test(id) ? id : `"${id}"`; } - private updateOps(query: string) { + private updateOps = memoizeOne((query: string) => { const ast = new MultiColumnsSyntaxTree(query); if (!ast.isValid) { @@ -96,14 +106,14 @@ export default class FilterFactory { R.forEach(s => { if (s.lexeme.name === LexemeType.UnaryOperator && s.block) { - let safeColumnId = this.getSafeColumnId(s.block.value); + const safeColumnId = this.getSafeColumnId(s.block.value); this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, s.value)); } else if (s.lexeme.name === LexemeType.BinaryOperator && s.left && s.right) { - let safeColumnId = this.getSafeColumnId(s.left.value); + const safeColumnId = this.getSafeColumnId(s.left.value); this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, `${s.value} ${s.right.value}`)); } }, statements); - } + }); public createFilters() { const { @@ -139,7 +149,8 @@ export default class FilterFactory { ); const filters = R.addIndex(R.map)((column, index) => { - const ast = this.ops.get(column.id.toString()); + const safeColumnId = this.getSafeColumnId(column.id.toString()); + const ast = this.ops.get(safeColumnId); return ( setProps({ filtering_settings }); +const handleSetFilter = (setProps: SetProps, setState: SetState, filtering_settings: string, rawFilterQuery: string) => { + setProps({ filtering_settings }); + setState({ rawFilterQuery }); +}; function filterPropsFn(propsFn: () => ControlledTableProps) { const { @@ -12,9 +15,11 @@ function filterPropsFn(propsFn: () => ControlledTableProps) { filtering_settings, filtering_type, id, + rawFilterQuery, row_deletable, row_selectable, setProps, + setState, style_cell, style_cell_conditional, style_filter, @@ -32,7 +37,8 @@ function filterPropsFn(propsFn: () => ControlledTableProps) { filtering_settings, filtering_type, id, - setFilter: handleSetFilter.bind(undefined, setProps), + rawFilterQuery, + setFilter: handleSetFilter.bind(undefined, setProps, setState), style_cell, style_cell_conditional, style_filter, diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index 93ab4cd1e..0a4b89bc7 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -55,14 +55,26 @@ describe('filter', () => { DashTable.getFilterById('ccc').click(); DOM.focused.type(`gt`); DashTable.getFilterById('ddd').click(); - DOM.focused.type('num(20000)'); + DOM.focused.type('numpy(20000)'); DashTable.getFilterById('eee').click(); DOM.focused.type('is prime2'); DashTable.getFilterById('bbb').click(); - DOM.focused.type(`!`); + DOM.focused.type('!'); + DashTable.getFilterById('ccc').click(); DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_0)); DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_1)); + + DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '!')); + DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt')); + DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'numpy(20000)')); + DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', 'is prime2')); + + DashTable.getFilterById('bbb').should('have.class', 'invalid'); + DashTable.getFilterById('ccc').should('have.class', 'invalid'); + DashTable.getFilterById('ddd').should('have.class', 'invalid'); + DashTable.getFilterById('eee').should('have.class', 'invalid'); + }); it('reset updates results and filter fields', () => { From e3b3631db6b763c9d7d6c65ba3c727a71b1411eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 Mar 2019 17:38:38 -0400 Subject: [PATCH 08/54] - fix empty table styling issue - style filter cells correctly --- demo/AppMode.ts | 2 +- src/core/syntax-tree/index.ts | 4 +-- src/dash-table/components/Filter/Column.tsx | 5 ++- src/dash-table/components/FilterFactory.tsx | 18 +++++++--- src/dash-table/derived/table/index.tsx | 40 ++++----------------- 5 files changed, 26 insertions(+), 43 deletions(-) diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 631c922a4..b5ed206d1 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -65,7 +65,7 @@ function getBaseTableProps(mock: IDataMock) { max_width: '1000px', width: '1000px' }, - style_data_conditional: [ + style_cell_conditional: [ { max_width: 150, min_width: 150, width: 150 }, { if: { column_id: 'rows' }, max_width: 60, min_width: 60, width: 60 }, { if: { column_id: 'bbb' }, max_width: 200, min_width: 200, width: 200 }, diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index a98627b7c..e3357d5fc 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -20,9 +20,9 @@ export default class SyntaxTree { constructor( public readonly lexicon: Lexicon, public readonly query: string, - modifyLex: (res: ILexerResult) => ILexerResult = res => res + postProcessor: (res: ILexerResult) => ILexerResult = res => res ) { - this.lexerResult = modifyLex(lexer(this.lexicon, this.query)); + this.lexerResult = postProcessor(lexer(this.lexicon, this.query)); this.syntaxerResult = syntaxer(this.lexerResult); } diff --git a/src/dash-table/components/Filter/Column.tsx b/src/dash-table/components/Filter/Column.tsx index 5f1aad2ce..1c32da746 100644 --- a/src/dash-table/components/Filter/Column.tsx +++ b/src/dash-table/components/Filter/Column.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { CSSProperties, PureComponent } from 'react'; import IsolatedInput from 'core/components/IsolatedInput'; @@ -11,6 +11,7 @@ interface IColumnFilterProps { columnId: ColumnId; isValid: boolean; setFilter: SetFilter; + style?: CSSProperties; value?: string; } @@ -36,12 +37,14 @@ export default class ColumnFilter extends PureComponent { classes, columnId, isValid, + style, value } = this.props; return ( void; export interface IFilterOptions { columns: VisibleColumns; - fillerColumns: number; filtering: Filtering; filtering_settings: string; filtering_type: FilteringType; id: string; rawFilterQuery: string; + row_deletable: boolean; + row_selectable: RowSelection; setFilter: SetFilter; style_cell: Style; style_cell_conditional: Cells; @@ -35,6 +37,7 @@ export default class FilterFactory { private readonly ops = new Map(); private readonly filterStyles = derivedFilterStyles(); private readonly relevantStyles = derivedRelevantFilterStyles(); + private readonly headerOperations = derivedHeaderOperations(); private get props() { return this.propsFn(); @@ -118,10 +121,11 @@ export default class FilterFactory { public createFilters() { const { columns, - fillerColumns, filtering, filtering_settings, filtering_type, + row_deletable, + row_selectable, setFilter, style_cell, style_cell_conditional, @@ -167,9 +171,13 @@ export default class FilterFactory { wrapperStyles, (f, s) => React.cloneElement(f, { style: s })); - const offsets = R.range(0, fillerColumns).map(i => ()); + const operations = this.headerOperations( + 1, + row_selectable, + row_deletable + )[0]; - return [offsets.concat(styledFilters)]; + return [operations.concat(styledFilters)]; } else { return [[]]; } diff --git a/src/dash-table/derived/table/index.tsx b/src/dash-table/derived/table/index.tsx index 30a09fdd5..e01765fe3 100644 --- a/src/dash-table/derived/table/index.tsx +++ b/src/dash-table/derived/table/index.tsx @@ -1,3 +1,5 @@ +import * as R from 'ramda'; + import CellFactory from 'dash-table/components/CellFactory'; import FilterFactory from 'dash-table/components/FilterFactory'; import HeaderFactory from 'dash-table/components/HeaderFactory'; @@ -9,41 +11,11 @@ const handleSetFilter = (setProps: SetProps, setState: SetState, filtering_setti }; function filterPropsFn(propsFn: () => ControlledTableProps) { - const { - columns, - filtering, - filtering_settings, - filtering_type, - id, - rawFilterQuery, - row_deletable, - row_selectable, - setProps, - setState, - style_cell, - style_cell_conditional, - style_filter, - style_filter_conditional - } = propsFn(); - - const fillerColumns = - (row_deletable ? 1 : 0) + - (row_selectable ? 1 : 0); + const props = propsFn(); - return { - columns, - fillerColumns, - filtering, - filtering_settings, - filtering_type, - id, - rawFilterQuery, - setFilter: handleSetFilter.bind(undefined, setProps, setState), - style_cell, - style_cell_conditional, - style_filter, - style_filter_conditional - }; + return R.merge(props, { + setFilter: handleSetFilter.bind(undefined, props.setProps, props.setState) + }); } function getter( From fd8540e5d73a6c7f4470981ca3841ed7684034e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 Mar 2019 18:13:59 -0400 Subject: [PATCH 09/54] - table rendering with different filters --- .../percy-storybook/Width.empty.percy.tsx | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/visual/percy-storybook/Width.empty.percy.tsx diff --git a/tests/visual/percy-storybook/Width.empty.percy.tsx b/tests/visual/percy-storybook/Width.empty.percy.tsx new file mode 100644 index 000000000..e390b0d43 --- /dev/null +++ b/tests/visual/percy-storybook/Width.empty.percy.tsx @@ -0,0 +1,61 @@ +import * as R from 'ramda'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import random from 'core/math/random'; +import DataTable from 'dash-table/dash/DataTable'; +import { FilteringType } from 'dash-table/components/Table/props'; + +const setProps = () => { }; + +const columns = ['a', 'b', 'c']; + +const data = (() => { + const r = random(1); + + return R.range(0, 5).map(() => ( + ['a', 'b', 'c'].reduce((obj: any, key) => { + obj[key] = Math.floor(r() * 1000); + return obj; + }, {}) + )); +})(); + +const baseProps = { + setProps, + id: 'table', + content_style: 'fit', + data, + filtering: 'fe', + filtering_type: FilteringType.Basic, + style_cell: { width: 100, max_width: 100, min_width: 100 } +}; + +const props = Object.assign({}, baseProps, { + columns: columns.map((id => ({ id: id, name: id.toUpperCase() }))) +}); + +storiesOf('DashTable/Empty', module) + .add('with column filters -- no query', () => ()) + .add('with column filters -- invalid query', () => ()) + .add('with column filters -- single query', () => ()) + .add('with column filters -- multi query', () => ()) + .add('with column filters -- multi query, no data', () => ()); \ No newline at end of file From 77cc573472d003110bc86293cbb4e7035fc3060c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 Mar 2019 20:04:14 -0400 Subject: [PATCH 10/54] - fix closing syntax (can't be first item) --- src/dash-table/syntax-tree/lexicon/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index 632a545c5..fc4dbefa6 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -57,7 +57,7 @@ const lexicon: ILexeme[] = [ { ...blockClose, if: (lexemes: ILexemeResult[], previous: ILexemeResult) => - !previous || R.contains( + previous && R.contains( previous.lexeme.name, [ LexemeType.BlockClose, From 90de09d30f9acfb2b02d4cf1eb72ad60ae661ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 Mar 2019 11:49:00 -0400 Subject: [PATCH 11/54] unary lexemes use left and right instead of block --- src/dash-table/syntax-tree/lexeme/unaryNot.ts | 4 ++-- src/dash-table/syntax-tree/lexeme/unaryOperator.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dash-table/syntax-tree/lexeme/unaryNot.ts b/src/dash-table/syntax-tree/lexeme/unaryNot.ts index 0e7f5e2db..6c0f6c1b7 100644 --- a/src/dash-table/syntax-tree/lexeme/unaryNot.ts +++ b/src/dash-table/syntax-tree/lexeme/unaryNot.ts @@ -7,14 +7,14 @@ const unaryNot: IUnboundedLexeme = { const t = tree as any; - return !t.block.lexeme.evaluate(target, t.block); + return !t.right.lexeme.evaluate(target, t.right); }, name: LexemeType.UnaryNot, priority: 1.5, regexp: /^!/, syntaxer: (lexs: any[]) => { return Object.assign({ - block: lexs.slice(1, lexs.length) + right: lexs.slice(1, lexs.length) }, lexs[0]); } }; diff --git a/src/dash-table/syntax-tree/lexeme/unaryOperator.ts b/src/dash-table/syntax-tree/lexeme/unaryOperator.ts index 3a6c31686..5e4fc4711 100644 --- a/src/dash-table/syntax-tree/lexeme/unaryOperator.ts +++ b/src/dash-table/syntax-tree/lexeme/unaryOperator.ts @@ -13,7 +13,7 @@ const unaryOperator: IUnboundedLexeme = { Logger.trace('evaluate -> unary', target, tree); const t = tree as any; - const opValue = t.block.lexeme.resolve(target, t.block); + const opValue = t.left.lexeme.resolve(target, t.left); switch (tree.value.toLowerCase()) { case 'is even': @@ -40,9 +40,9 @@ const unaryOperator: IUnboundedLexeme = { priority: 0, regexp: /^((is nil)|(is odd)|(is even)|(is bool)|(is num)|(is object)|(is str)|(is prime))/i, syntaxer: (lexs: any[]) => { - let [block, lexeme] = lexs; + let [left, lexeme] = lexs; - return Object.assign({ block }, lexeme); + return Object.assign({ left }, lexeme); } }; From 60a5914ec84d02ab301714c29ffbe817c35d3273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Mar 2019 15:33:37 -0400 Subject: [PATCH 12/54] update operand syntax --- demo/AppMode.ts | 2 +- src/dash-table/syntax-tree/index.ts | 4 +- src/dash-table/syntax-tree/lexeme/operand.ts | 12 +- .../unit/multi_columns_syntactic_tree.ts | 4 +- .../tests/unit/query_syntactic_tree_test.ts | 154 ++++++++++-------- .../unit/single_column_syntactic_tree_test.ts | 10 +- 6 files changed, 99 insertions(+), 87 deletions(-) diff --git a/demo/AppMode.ts b/demo/AppMode.ts index b5ed206d1..277419d9a 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -150,7 +150,7 @@ function getTooltipsState() { state.tableProps.column_conditional_tooltips = [{ if: { column_id: 'aaa-readonly', - filter: `aaa is prime` + filter: `{aaa} is prime` }, type: TooltipSyntax.Markdown, value: `### Go Proverbs\nCapture three to get an eye` diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index 6b75afb90..5d9128790 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -33,12 +33,12 @@ function modifyLex(key: ColumnId, res: ILexerResult) { if (isBinary(res.lexemes) || isUnary(res.lexemes)) { res.lexemes = [ - { lexeme: boundLexeme(operand), value: `${key}` }, + { lexeme: boundLexeme(operand), value: `{${key}}` }, ...res.lexemes ]; } else if (isExpression(res.lexemes)) { res.lexemes = [ - { lexeme: boundLexeme(operand), value: `${key}` }, + { lexeme: boundLexeme(operand), value: `{${key}}` }, { lexeme: boundLexeme(binaryOperator), value: 'eq' }, ...res.lexemes ]; diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts index b5c08fb58..387666fd9 100644 --- a/src/dash-table/syntax-tree/lexeme/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -1,17 +1,19 @@ import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; +const REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; + const operand: IUnboundedLexeme = { resolve: (target: any, tree: ISyntaxTree) => { - if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.")+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { + if (REGEX.test(tree.value)) { return target[ - tree.value.slice(1, tree.value.length - 1) + tree.value.slice(1, tree.value.length - 1).replace(/\\([{}])/g, '$1') ]; - } else if (/^(\w|[:.\-+])+$/.test(tree.value)) { - return target[tree.value]; + } else { + throw new Error(); } }, - regexp: /^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/, + regexp: REGEX, name: LexemeType.Operand }; diff --git a/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts b/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts index a4777c65c..1dd31c181 100644 --- a/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts +++ b/tests/cypress/tests/unit/multi_columns_syntactic_tree.ts @@ -2,13 +2,13 @@ import { MultiColumnsSyntaxTree } from 'dash-table/syntax-tree'; describe('Multi Columns Syntax Tree', () => { it('can do single', () => { - const tree = new MultiColumnsSyntaxTree('a >= 3'); + const tree = new MultiColumnsSyntaxTree('{a} >= 3'); expect(tree.isValid).to.equal(true); }); it('can "and"', () => { - const tree = new MultiColumnsSyntaxTree('a >= 3 && b is even'); + const tree = new MultiColumnsSyntaxTree('{a} >= 3 && {b} is even'); expect(tree.isValid).to.equal(true); }); diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index 882b72a1a..d88e29dac 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -1,38 +1,38 @@ import { QuerySyntaxTree } from 'dash-table/syntax-tree'; describe('Query Syntax Tree', () => { - const data0 = { a: '0', b: '0', c: 0, d: null, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '\'""\'': '0\'"dot' }; - const data1 = { a: '1', b: '0', c: 1, d: 0, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot', '\'""\'': '1\'"dot' }; - const data2 = { a: '2', b: '1', c: 2, d: '', 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '\'""\'': '2\'"dot' }; - const data3 = { a: '3', b: '1', c: 3, d: false, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '\'""\'': '3\'"dot' }; + const data0 = { a: '0', b: '0', c: 0, d: null, '\\\{': 0, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '{a:dot}': '0*dot*', '\'""\'': '0\'"dot' }; + const data1 = { a: '1', b: '0', c: 1, d: 0, '\\\{': 1, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot', '{a:dot}': '1*dot*', '\'""\'': '1\'"dot' }; + const data2 = { a: '2', b: '1', c: 2, d: '', '\\\{': 2, 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '{a:dot}': '2*dot*', '\'""\'': '2\'"dot' }; + const data3 = { a: '3', b: '1', c: 3, d: false, '\\\{': 3, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '{a:dot}': '3*dot*', '\'""\'': '3\'"dot' }; describe('operands', () => { it('does not support badly formed operands', () => { - expect(new QuerySyntaxTree(`'myField' eq num(0)`).isValid).to.equal(true); - expect(new QuerySyntaxTree(`"myField" eq num(0)`).isValid).to.equal(true); - expect(new QuerySyntaxTree('`myField` eq num(0)').isValid).to.equal(true); - expect(new QuerySyntaxTree(`'myField\\' eq num(0)`).isValid).to.equal(false); - expect(new QuerySyntaxTree(`"myField\\" eq num(0)`).isValid).to.equal(false); - expect(new QuerySyntaxTree('`myField\\` eq num(0)').isValid).to.equal(false); - expect(new QuerySyntaxTree(`\\'myField' eq num(0)`).isValid).to.equal(false); - expect(new QuerySyntaxTree(`\\"myField" eq num(0)`).isValid).to.equal(false); - expect(new QuerySyntaxTree('\\`myField` eq num(0)').isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq num(0)`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`{'myField'} eq num(0)`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`{"myField"} eq num(0)`).isValid).to.equal(true); + expect(new QuerySyntaxTree('{`myField`} eq num(0)').isValid).to.equal(true); + expect(new QuerySyntaxTree('{\\{myField\\}} eq num(0)').isValid).to.equal(true); + expect(new QuerySyntaxTree('{{myField}} eq num(0)').isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField eq num(0)`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`myField} eq num(0)`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`myField eq num(0)`).isValid).to.equal(false); }); it('does not support badly formed expression', () => { - expect(new QuerySyntaxTree(`myField eq 'value'`).isValid).to.equal(true); - expect(new QuerySyntaxTree(`myField eq "value"`).isValid).to.equal(true); - expect(new QuerySyntaxTree('myField eq `value`').isValid).to.equal(true); - expect(new QuerySyntaxTree(`myField eq 'value\\'`).isValid).to.equal(false); - expect(new QuerySyntaxTree(`myField eq "value\\"`).isValid).to.equal(false); - expect(new QuerySyntaxTree('myField eq `value\\`').isValid).to.equal(false); - expect(new QuerySyntaxTree(`myField eq \\'value'`).isValid).to.equal(false); - expect(new QuerySyntaxTree(`myField eq \\"value"`).isValid).to.equal(false); - expect(new QuerySyntaxTree('myField eq \\`value`').isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq 'value'`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`{myField} eq "value"`).isValid).to.equal(true); + expect(new QuerySyntaxTree('{myField} eq `value`').isValid).to.equal(true); + expect(new QuerySyntaxTree(`{myField} eq 'value\\'`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq "value\\"`).isValid).to.equal(false); + expect(new QuerySyntaxTree('{myField} eq `value\\`').isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq \\'value'`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq \\"value"`).isValid).to.equal(false); + expect(new QuerySyntaxTree('{myField} eq \\`value`').isValid).to.equal(false); }); it('support arbitrary quoted column name', () => { - const tree = new QuerySyntaxTree(`'_-6.:+** *@$' eq "1*dot" || '_-6.:+** *@$' eq "2*dot"`); + const tree = new QuerySyntaxTree(`{_-6.:+** *@$} eq "1*dot" || {_-6.:+** *@$} eq "2*dot"`); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -42,7 +42,7 @@ describe('Query Syntax Tree', () => { }); it('support column name with "."', () => { - const tree = new QuerySyntaxTree('a.dot eq "1.dot" || a.dot eq "2.dot"'); + const tree = new QuerySyntaxTree('{a.dot} eq "1.dot" || {a.dot} eq "2.dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -52,7 +52,7 @@ describe('Query Syntax Tree', () => { }); it('support column name with "-"', () => { - const tree = new QuerySyntaxTree('a-dot eq "1-dot" || a-dot eq "2-dot"'); + const tree = new QuerySyntaxTree('{a-dot} eq "1-dot" || {a-dot} eq "2-dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -62,7 +62,7 @@ describe('Query Syntax Tree', () => { }); it('support column name with "_"', () => { - const tree = new QuerySyntaxTree('a_dot eq "1_dot" || a_dot eq "2_dot"'); + const tree = new QuerySyntaxTree('{a_dot} eq "1_dot" || {a_dot} eq "2_dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -72,7 +72,7 @@ describe('Query Syntax Tree', () => { }); it('support column name with "+"', () => { - const tree = new QuerySyntaxTree('a+dot eq "1+dot" || a+dot eq "2+dot"'); + const tree = new QuerySyntaxTree('{a+dot} eq "1+dot" || {a+dot} eq "2+dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -82,7 +82,7 @@ describe('Query Syntax Tree', () => { }); it('support column name with ":"', () => { - const tree = new QuerySyntaxTree('a:dot eq "1:dot" || a:dot eq "2:dot"'); + const tree = new QuerySyntaxTree('{a:dot} eq "1:dot" || {a:dot} eq "2:dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -91,8 +91,8 @@ describe('Query Syntax Tree', () => { expect(tree.evaluate(data3)).to.equal(false); }); - it('support double quoted column name with " " (space)', () => { - const tree = new QuerySyntaxTree('"a dot" eq "1 dot" || "a dot" eq "2 dot"'); + it('support column name with " " (space)', () => { + const tree = new QuerySyntaxTree('{a dot} eq "1 dot" || {a dot} eq "2 dot"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -101,8 +101,18 @@ describe('Query Syntax Tree', () => { expect(tree.evaluate(data3)).to.equal(false); }); - it('support single quoted column name with " " (space)', () => { - const tree = new QuerySyntaxTree('\'a dot\' eq "1 dot" || \'a dot\' eq "2 dot"'); + it('support column name with "{}"', () => { + const tree = new QuerySyntaxTree('{\\{a:dot\\}} eq "1*dot*" || {\\{a:dot\\}} eq "2*dot*"'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(false); + expect(tree.evaluate(data1)).to.equal(true); + expect(tree.evaluate(data2)).to.equal(true); + expect(tree.evaluate(data3)).to.equal(false); + }); + + it('support column name with "\\"', () => { + const tree = new QuerySyntaxTree('{\\\\{} eq num(1) || {\\\\{} eq num(2)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -112,7 +122,7 @@ describe('Query Syntax Tree', () => { }); it('support nesting in quotes', () => { - const tree = new QuerySyntaxTree(`\`'""'\` eq \`1'"dot\` || \`'""'\` eq \`2'"dot\``); + const tree = new QuerySyntaxTree(`{'""'} eq \`1'"dot\` || {'""'} eq \`2'"dot\``); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -124,7 +134,7 @@ describe('Query Syntax Tree', () => { describe('&& and ||', () => { it('can || two conditions', () => { - const tree = new QuerySyntaxTree('a eq "1" || a eq "2"'); + const tree = new QuerySyntaxTree('{a} eq "1" || {a} eq "2"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -134,7 +144,7 @@ describe('Query Syntax Tree', () => { }); it('can "or" two conditions', () => { - const tree = new QuerySyntaxTree('a eq "1" or a eq "2"'); + const tree = new QuerySyntaxTree('{a} eq "1" or {a} eq "2"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -144,7 +154,7 @@ describe('Query Syntax Tree', () => { }); it('can && two conditions', () => { - const tree = new QuerySyntaxTree('a eq "1" && b eq "0"'); + const tree = new QuerySyntaxTree('{a} eq "1" && {b} eq "0"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -154,7 +164,7 @@ describe('Query Syntax Tree', () => { }); it('can "and" two conditions', () => { - const tree = new QuerySyntaxTree('a eq "1" and b eq "0"'); + const tree = new QuerySyntaxTree('{a} eq "1" and {b} eq "0"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -164,7 +174,7 @@ describe('Query Syntax Tree', () => { }); it('gives priority to && over ||', () => { - const tree = new QuerySyntaxTree('a eq "1" && a eq "0" || b eq "1"'); + const tree = new QuerySyntaxTree('{a} eq "1" && {a} eq "0" || {b} eq "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -174,7 +184,7 @@ describe('Query Syntax Tree', () => { }); it('gives priority to "and" over "or"', () => { - const tree = new QuerySyntaxTree('a eq "1" and a eq "0" or b eq "1"'); + const tree = new QuerySyntaxTree('{a} eq "1" and {a} eq "0" or {b} eq "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -186,7 +196,7 @@ describe('Query Syntax Tree', () => { describe('data types', () => { it('can compare numbers', () => { - const tree = new QuerySyntaxTree('c eq num(1)'); + const tree = new QuerySyntaxTree('{c} eq num(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -196,7 +206,7 @@ describe('Query Syntax Tree', () => { }); it('can compare floats', () => { - const tree = new QuerySyntaxTree('field ge num(1.5)'); + const tree = new QuerySyntaxTree('{field} ge num(1.5)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ field: -1.501 })).to.equal(false); @@ -208,7 +218,7 @@ describe('Query Syntax Tree', () => { }); it('can compare string to number and return false', () => { - const tree = new QuerySyntaxTree('a eq num(1)'); + const tree = new QuerySyntaxTree('{a} eq num(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -218,7 +228,7 @@ describe('Query Syntax Tree', () => { }); it('can compare strings', () => { - const tree = new QuerySyntaxTree('a eq str(1)'); + const tree = new QuerySyntaxTree('{a} eq str(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -228,7 +238,7 @@ describe('Query Syntax Tree', () => { }); it('can compare string to number and return false', () => { - const tree = new QuerySyntaxTree('c eq str(1)'); + const tree = new QuerySyntaxTree('{c} eq str(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -240,7 +250,7 @@ describe('Query Syntax Tree', () => { describe('block', () => { it('has priority over && and ||', () => { - const tree = new QuerySyntaxTree('a eq "1" && (a eq "0" || b eq "1")'); + const tree = new QuerySyntaxTree('{a} eq "1" && ({a} eq "0" || {b} eq "1")'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -250,7 +260,7 @@ describe('Query Syntax Tree', () => { }); it('gives priority over "and" and "or"', () => { - const tree = new QuerySyntaxTree('a eq "1" and (a eq "0" or b eq "1")'); + const tree = new QuerySyntaxTree('{a} eq "1" and ({a} eq "0" or {b} eq "1")'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -260,7 +270,7 @@ describe('Query Syntax Tree', () => { }); it('can be uselessly nested', () => { - const tree = new QuerySyntaxTree('((a eq "1" and (((a eq "0" or b eq "1")))))'); + const tree = new QuerySyntaxTree('(({a} eq "1" and ((({a} eq "0" or {b} eq "1")))))'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -270,7 +280,7 @@ describe('Query Syntax Tree', () => { }); it('can be nested', () => { - const tree = new QuerySyntaxTree('a eq "1" and (a eq "0" or (b eq "1" or b eq "0"))'); + const tree = new QuerySyntaxTree('{a} eq "1" and ({a} eq "0" or ({b} eq "1" or {b} eq "0"))'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -282,7 +292,7 @@ describe('Query Syntax Tree', () => { describe('unary operators', () => { it('can check nil', () => { - const tree = new QuerySyntaxTree('d is nil'); + const tree = new QuerySyntaxTree('{d} is nil'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -292,7 +302,7 @@ describe('Query Syntax Tree', () => { }); it('can invert check nil', () => { - const tree = new QuerySyntaxTree('!(d is nil)'); + const tree = new QuerySyntaxTree('!({d} is nil)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -302,7 +312,7 @@ describe('Query Syntax Tree', () => { }); it('can check odd', () => { - const tree = new QuerySyntaxTree('c is odd'); + const tree = new QuerySyntaxTree('{c} is odd'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -312,7 +322,7 @@ describe('Query Syntax Tree', () => { }); it('can check odd on string and return false', () => { - const tree = new QuerySyntaxTree('a is odd'); + const tree = new QuerySyntaxTree('{a} is odd'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -322,7 +332,7 @@ describe('Query Syntax Tree', () => { }); it('can check even', () => { - const tree = new QuerySyntaxTree('c is even'); + const tree = new QuerySyntaxTree('{c} is even'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -332,7 +342,7 @@ describe('Query Syntax Tree', () => { }); it('can check even on string and return false', () => { - const tree = new QuerySyntaxTree('a is even'); + const tree = new QuerySyntaxTree('{a} is even'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -342,7 +352,7 @@ describe('Query Syntax Tree', () => { }); it('can check if string', () => { - const tree = new QuerySyntaxTree('d is str'); + const tree = new QuerySyntaxTree('{d} is str'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -352,7 +362,7 @@ describe('Query Syntax Tree', () => { }); it('can check if number', () => { - const tree = new QuerySyntaxTree('d is num'); + const tree = new QuerySyntaxTree('{d} is num'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -362,7 +372,7 @@ describe('Query Syntax Tree', () => { }); it('can check if bool', () => { - const tree = new QuerySyntaxTree('d is bool'); + const tree = new QuerySyntaxTree('{d} is bool'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -372,7 +382,7 @@ describe('Query Syntax Tree', () => { }); it('can check if object', () => { - const tree = new QuerySyntaxTree('d is object'); + const tree = new QuerySyntaxTree('{d} is object'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -384,7 +394,7 @@ describe('Query Syntax Tree', () => { describe('unary not', () => { it('can invert block', () => { - const tree = new QuerySyntaxTree('!(a eq "1")'); + const tree = new QuerySyntaxTree('!({a} eq "1")'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -394,7 +404,7 @@ describe('Query Syntax Tree', () => { }); it('can invert block multiple times', () => { - const tree = new QuerySyntaxTree('!!(a eq "1")'); + const tree = new QuerySyntaxTree('!!({a} eq "1")'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -406,7 +416,7 @@ describe('Query Syntax Tree', () => { describe('logical binary operators', () => { it('can do equality (eq) test', () => { - const tree = new QuerySyntaxTree('a eq "1"'); + const tree = new QuerySyntaxTree('{a} eq "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -416,7 +426,7 @@ describe('Query Syntax Tree', () => { }); it('can do equality (=) test', () => { - const tree = new QuerySyntaxTree('a = "1"'); + const tree = new QuerySyntaxTree('{a} = "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -426,7 +436,7 @@ describe('Query Syntax Tree', () => { }); it('can do difference (ne) test', () => { - const tree = new QuerySyntaxTree('a ne "1"'); + const tree = new QuerySyntaxTree('{a} ne "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -436,7 +446,7 @@ describe('Query Syntax Tree', () => { }); it('can do difference (!=) test', () => { - const tree = new QuerySyntaxTree('a != "1"'); + const tree = new QuerySyntaxTree('{a} != "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -446,7 +456,7 @@ describe('Query Syntax Tree', () => { }); it('can do greater than (gt) test', () => { - const tree = new QuerySyntaxTree('a gt "1"'); + const tree = new QuerySyntaxTree('{a} gt "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -456,7 +466,7 @@ describe('Query Syntax Tree', () => { }); it('can do greater than (>) test', () => { - const tree = new QuerySyntaxTree('a > "1"'); + const tree = new QuerySyntaxTree('{a} > "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -466,7 +476,7 @@ describe('Query Syntax Tree', () => { }); it('can do less than (lt) test', () => { - const tree = new QuerySyntaxTree('a lt "1"'); + const tree = new QuerySyntaxTree('{a} lt "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -476,7 +486,7 @@ describe('Query Syntax Tree', () => { }); it('can do less than (<) test', () => { - const tree = new QuerySyntaxTree('a < "1"'); + const tree = new QuerySyntaxTree('{a} < "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -486,7 +496,7 @@ describe('Query Syntax Tree', () => { }); it('can do greater or equal to (ge) test', () => { - const tree = new QuerySyntaxTree('a ge "1"'); + const tree = new QuerySyntaxTree('{a} ge "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -496,7 +506,7 @@ describe('Query Syntax Tree', () => { }); it('can do greater or equal to (>=) test', () => { - const tree = new QuerySyntaxTree('a >= "1"'); + const tree = new QuerySyntaxTree('{a} >= "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -506,7 +516,7 @@ describe('Query Syntax Tree', () => { }); it('can do less or equal to (le) test', () => { - const tree = new QuerySyntaxTree('a le "1"'); + const tree = new QuerySyntaxTree('{a} le "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); @@ -516,7 +526,7 @@ describe('Query Syntax Tree', () => { }); it('can do less or equal to (<=) test', () => { - const tree = new QuerySyntaxTree('a <= "1"'); + const tree = new QuerySyntaxTree('{a} <= "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(true); diff --git a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts index f618d2883..3413dc046 100644 --- a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts @@ -2,7 +2,7 @@ import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; describe('Single Column Syntax Tree', () => { it('cannot have operand', () => { - const tree = new SingleColumnSyntaxTree('a', 'a <= num(1)'); + const tree = new SingleColumnSyntaxTree('a', '{a} <= num(1)'); expect(tree.isValid).to.equal(false); }); @@ -33,7 +33,7 @@ describe('Single Column Syntax Tree', () => { expect(tree.evaluate({ a: 0 })).to.equal(true); expect(tree.evaluate({ a: 2 })).to.equal(false); - expect(tree.toQueryString()).to.equal('a <= num(1)'); + expect(tree.toQueryString()).to.equal('{a} <= num(1)'); }); it('can be unary', () => { @@ -43,16 +43,16 @@ describe('Single Column Syntax Tree', () => { expect(tree.evaluate({ a: 5 })).to.equal(true); expect(tree.evaluate({ a: 6 })).to.equal(false); - expect(tree.toQueryString()).to.equal('a is prime'); + expect(tree.toQueryString()).to.equal('{a} is prime'); }); it('can be expression', () => { - const tree = new SingleColumnSyntaxTree('"a"', 'num(1)'); + const tree = new SingleColumnSyntaxTree('a', 'num(1)'); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 1 })).to.equal(true); expect(tree.evaluate({ a: 2 })).to.equal(false); - expect(tree.toQueryString()).to.equal('"a" eq num(1)'); + expect(tree.toQueryString()).to.equal('{a} eq num(1)'); }); }); \ No newline at end of file From a806ec2ac0708721c63e9c1cc5d0029100a85efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Mar 2019 16:05:57 -0400 Subject: [PATCH 13/54] fix filtering tests --- src/dash-table/components/FilterFactory.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 16844b163..293c7a50f 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -51,7 +51,7 @@ export default class FilterFactory { Logger.debug('Filter -- onChange', columnId, ev.target.value && ev.target.value.trim()); const value = ev.target.value.trim(); - const safeColumnId = `"${columnId}"`; + const safeColumnId = this.getSafeColumnId(columnId); if (value && value.length) { ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, value)); @@ -89,9 +89,7 @@ export default class FilterFactory { } private getSafeColumnId(columnId: ColumnId) { - const id = columnId.toString(); - - return /^"[^"]+"$/.test(id) ? id : `"${id}"`; + return columnId.toString(); } private updateOps = memoizeOne((query: string) => { @@ -113,7 +111,7 @@ export default class FilterFactory { this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, s.value)); } else if (s.lexeme.name === LexemeType.BinaryOperator && s.left && s.right) { const safeColumnId = this.getSafeColumnId(s.left.value); - this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, `${s.value} ${s.right.value}`)); + this.ops.set(safeColumnId, new SingleColumnSyntaxTree(s.left.value, `${s.value} ${s.right.value}`)); } }, statements); }); From 89e62316b829c3b34f76bf20fc769e155f9ab2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Mar 2019 16:19:39 -0400 Subject: [PATCH 14/54] percy - fix filtering --- tests/visual/percy-storybook/Width.empty.percy.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/visual/percy-storybook/Width.empty.percy.tsx b/tests/visual/percy-storybook/Width.empty.percy.tsx index e390b0d43..4c5659599 100644 --- a/tests/visual/percy-storybook/Width.empty.percy.tsx +++ b/tests/visual/percy-storybook/Width.empty.percy.tsx @@ -41,21 +41,21 @@ storiesOf('DashTable/Empty', module) />)) .add('with column filters -- invalid query', () => ()) .add('with column filters -- single query', () => ()) .add('with column filters -- multi query', () => ()) .add('with column filters -- multi query, no data', () => ()); \ No newline at end of file From c3081412d8403a777908a756403468238117b5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Mar 2019 16:25:05 -0400 Subject: [PATCH 15/54] remove waitfor --- tests/dash/test_integration.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/dash/test_integration.py b/tests/dash/test_integration.py index a19546cbf..73df6a3d1 100644 --- a/tests/dash/test_integration.py +++ b/tests/dash/test_integration.py @@ -18,9 +18,7 @@ def test_review_app(self): def visit_and_snapshot(href): self.driver.get(href) - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.ID, "waitfor")) - ) + time.sleep(2) self.snapshot(href) self.driver.back() From ba1b01504f4bf373af97d5cbe29af97c7ccd6fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Mar 2019 11:27:28 -0400 Subject: [PATCH 16/54] refactor single & multi query mapping logic --- src/dash-table/components/FilterFactory.tsx | 51 ++------ .../syntax-tree/MultiColumnsSyntaxTree.ts | 46 +++++++ src/dash-table/syntax-tree/QuerySyntaxTree.ts | 9 ++ .../syntax-tree/SingleColumnSyntaxTree.ts | 49 +++++++ src/dash-table/syntax-tree/index.ts | 122 ++++-------------- 5 files changed, 140 insertions(+), 137 deletions(-) create mode 100644 src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts create mode 100644 src/dash-table/syntax-tree/QuerySyntaxTree.ts create mode 100644 src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 293c7a50f..584525417 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -3,7 +3,7 @@ import React from 'react'; import Logger from 'core/Logger'; import { arrayMap } from 'core/math/arrayZipMap'; -import { LexemeType } from 'core/syntax-tree/lexicon'; +import { memoizeOne } from 'core/memoizer'; import ColumnFilter from 'dash-table/components/Filter/Column'; import { ColumnId, Filtering, FilteringType, IVisibleColumn, VisibleColumns, RowSelection } from 'dash-table/components/Table/props'; @@ -11,8 +11,7 @@ import derivedFilterStyles from 'dash-table/derived/filter/wrapperStyles'; import derivedHeaderOperations from 'dash-table/derived/header/operations'; import { derivedRelevantFilterStyles } from 'dash-table/derived/style'; import { BasicFilters, Cells, Style } from 'dash-table/derived/style/props'; -import { MultiColumnsSyntaxTree, SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; -import { memoizeOne } from 'core/memoizer'; +import { MultiColumnsSyntaxTree, SingleColumnSyntaxTree, getMultiColumnQueryString, getSingleColumnMap } from 'dash-table/syntax-tree'; type SetFilter = (filter: string, rawFilter: string) => void; @@ -34,11 +33,12 @@ export interface IFilterOptions { export default class FilterFactory { private readonly handlers = new Map(); - private readonly ops = new Map(); private readonly filterStyles = derivedFilterStyles(); private readonly relevantStyles = derivedRelevantFilterStyles(); private readonly headerOperations = derivedHeaderOperations(); + private ops = new Map(); + private get props() { return this.propsFn(); } @@ -51,7 +51,7 @@ export default class FilterFactory { Logger.debug('Filter -- onChange', columnId, ev.target.value && ev.target.value.trim()); const value = ev.target.value.trim(); - const safeColumnId = this.getSafeColumnId(columnId); + const safeColumnId = columnId.toString(); if (value && value.length) { ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, value)); @@ -59,20 +59,12 @@ export default class FilterFactory { ops.delete(safeColumnId); } - const globalFilter = R.map( - ast => (ast && ast.toQueryString()) || '', - R.filter( - ast => ast && ast.isValid, - Array.from(ops.values()) - ) - ).join(' && '); + const asts = Array.from(ops.values()); + const globalFilter = getMultiColumnQueryString(asts); const rawGlobalFilter = R.map( ast => ast.query || '', - R.filter( - ast => Boolean(ast), - Array.from(ops.values()) - ) + R.filter(ast => Boolean(ast), asts) ).join(' && '); setFilter(globalFilter, rawGlobalFilter); @@ -88,32 +80,10 @@ export default class FilterFactory { ); } - private getSafeColumnId(columnId: ColumnId) { - return columnId.toString(); - } - private updateOps = memoizeOne((query: string) => { const ast = new MultiColumnsSyntaxTree(query); - if (!ast.isValid) { - return; - } - - const statements = ast.statements; - if (!statements) { - this.ops.clear(); - return; - } - - R.forEach(s => { - if (s.lexeme.name === LexemeType.UnaryOperator && s.block) { - const safeColumnId = this.getSafeColumnId(s.block.value); - this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, s.value)); - } else if (s.lexeme.name === LexemeType.BinaryOperator && s.left && s.right) { - const safeColumnId = this.getSafeColumnId(s.left.value); - this.ops.set(safeColumnId, new SingleColumnSyntaxTree(s.left.value, `${s.value} ${s.right.value}`)); - } - }, statements); + this.ops = getSingleColumnMap(ast) || this.ops; }); public createFilters() { @@ -151,8 +121,7 @@ export default class FilterFactory { ); const filters = R.addIndex(R.map)((column, index) => { - const safeColumnId = this.getSafeColumnId(column.id.toString()); - const ast = this.ops.get(safeColumnId); + const ast = this.ops.get(column.id.toString()); return ( item.value, R.filter(i => i.lexeme.name === LexemeType.Operand, this.lexerResult.lexemes)); + const uniqueFields = R.uniq(fields); + return fields.length === uniqueFields.length; + } +} \ No newline at end of file diff --git a/src/dash-table/syntax-tree/QuerySyntaxTree.ts b/src/dash-table/syntax-tree/QuerySyntaxTree.ts new file mode 100644 index 000000000..a309b9d2d --- /dev/null +++ b/src/dash-table/syntax-tree/QuerySyntaxTree.ts @@ -0,0 +1,9 @@ +import SyntaxTree from 'core/syntax-tree'; + +import queryLexicon from './lexicon/query'; + +export default class QuerySyntaxTree extends SyntaxTree { + constructor(query: string) { + super(queryLexicon, query); + } +} \ No newline at end of file diff --git a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts new file mode 100644 index 000000000..f3e1366ff --- /dev/null +++ b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts @@ -0,0 +1,49 @@ +import SyntaxTree from 'core/syntax-tree'; +import { ILexemeResult, ILexerResult } from 'core/syntax-tree/lexer'; +import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon'; + +import { ColumnId } from 'dash-table/components/Table/props'; + +import { operand, binaryOperator } from './lexeme'; +import columnLexicon from './lexicon/column'; + +function isBinary(lexemes: ILexemeResult[]) { + return lexemes.length === 2; +} + +function isExpression(lexemes: ILexemeResult[]) { + return lexemes.length === 1 && + lexemes[0].lexeme.name === LexemeType.Expression; +} + +function isUnary(lexemes: ILexemeResult[]) { + return lexemes.length === 1 && + lexemes[0].lexeme.name === LexemeType.UnaryOperator; +} + +export function modifyLex(key: ColumnId, res: ILexerResult) { + if (!res.valid) { + return res; + } + + if (isBinary(res.lexemes) || isUnary(res.lexemes)) { + res.lexemes = [ + { lexeme: boundLexeme(operand), value: `{${key}}` }, + ...res.lexemes + ]; + } else if (isExpression(res.lexemes)) { + res.lexemes = [ + { lexeme: boundLexeme(operand), value: `{${key}}` }, + { lexeme: boundLexeme(binaryOperator), value: 'eq' }, + ...res.lexemes + ]; + } + + return res; +} + +export default class SingleColumnSyntaxTree extends SyntaxTree { + constructor(key: ColumnId, query: string) { + super(columnLexicon, query, modifyLex.bind(undefined, key)); + } +} \ No newline at end of file diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index 5d9128790..67357e806 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -1,109 +1,39 @@ import * as R from 'ramda'; -import SyntaxTree from 'core/syntax-tree'; -import { ILexerResult, ILexemeResult } from 'core/syntax-tree/lexer'; -import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon'; -import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; +import { LexemeType } from 'core/syntax-tree/lexicon'; -import { ColumnId } from 'dash-table/components/Table/props'; +import MultiColumnsSyntaxTree from './MultiColumnsSyntaxTree'; +import QuerySyntaxTree from './QuerySyntaxTree'; +import SingleColumnSyntaxTree from './SingleColumnSyntaxTree'; -import { operand, binaryOperator } from './lexeme'; -import queryLexicon from './lexicon/query'; -import columnLexicon from './lexicon/column'; -import columnMultiLexicon from './lexicon/columnMulti'; +export const getMultiColumnQueryString = ( + asts: SingleColumnSyntaxTree[] +) => R.map( + ast => ast.toQueryString(), + R.filter(ast => ast && ast.isValid, asts) + ).join(' && '); -function isBinary(lexemes: ILexemeResult[]) { - return lexemes.length === 2; -} - -function isExpression(lexemes: ILexemeResult[]) { - return lexemes.length === 1 && - lexemes[0].lexeme.name === LexemeType.Expression; -} - -function isUnary(lexemes: ILexemeResult[]) { - return lexemes.length === 1 && - lexemes[0].lexeme.name === LexemeType.UnaryOperator; -} - -function modifyLex(key: ColumnId, res: ILexerResult) { - if (!res.valid) { - return res; - } - - if (isBinary(res.lexemes) || isUnary(res.lexemes)) { - res.lexemes = [ - { lexeme: boundLexeme(operand), value: `{${key}}` }, - ...res.lexemes - ]; - } else if (isExpression(res.lexemes)) { - res.lexemes = [ - { lexeme: boundLexeme(operand), value: `{${key}}` }, - { lexeme: boundLexeme(binaryOperator), value: 'eq' }, - ...res.lexemes - ]; +export const getSingleColumnMap = (ast: MultiColumnsSyntaxTree) => { + if (!ast.isValid) { + return; } - return res; -} + const map = new Map(); -export class SingleColumnSyntaxTree extends SyntaxTree { - constructor(key: ColumnId, query: string) { - super(columnLexicon, query, modifyLex.bind(undefined, key)); + const statements = ast.statements; + if (!statements) { + return map; } -} - -export class MultiColumnsSyntaxTree extends SyntaxTree { - constructor(query: string) { - super(columnMultiLexicon, query); - } - - get isValid() { - return super.isValid && - this.respectsBasicSyntax(); - } - - get statements() { - if (!this.syntaxerResult.tree) { - return; - } - - const statements: ISyntaxTree[] = []; - - const toCheck: ISyntaxTree[] = [this.syntaxerResult.tree]; - while (toCheck.length) { - const item = toCheck.pop(); - if (!item) { - continue; - } - statements.push(item); - - if (item.left) { toCheck.push(item.left); } - if (item.block) { toCheck.push(item.block); } - if (item.right) { toCheck.push(item.right); } + R.forEach(s => { + if (s.lexeme.name === LexemeType.UnaryOperator && s.block) { + map.set(s.block.value, new SingleColumnSyntaxTree(s.block.value, s.value)); + } else if (s.lexeme.name === LexemeType.BinaryOperator && s.left && s.right) { + map.set(s.left.value, new SingleColumnSyntaxTree(s.left.value, `${s.value} ${s.right.value}`)); } + }, statements); - return statements; - } - - private respectsBasicSyntax() { - const fields = R.map( - item => item.value, - R.filter( - i => i.lexeme.name === LexemeType.Operand, - this.lexerResult.lexemes - ) - ); - - const uniqueFields = R.uniq(fields); + return map; +}; - return fields.length === uniqueFields.length; - } -} - -export class QuerySyntaxTree extends SyntaxTree { - constructor(query: string) { - super(queryLexicon, query); - } -} \ No newline at end of file +export { MultiColumnsSyntaxTree, QuerySyntaxTree, SingleColumnSyntaxTree }; \ No newline at end of file From fc4645d76bd1b596c7ccb1d63e923062f06a4a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Mar 2019 13:21:53 -0400 Subject: [PATCH 17/54] refactor relational operators --- src/core/syntax-tree/lexicon.ts | 2 +- .../syntax-tree/SingleColumnSyntaxTree.ts | 4 +- src/dash-table/syntax-tree/index.ts | 2 +- .../lexeme/{ => argument}/expression.ts | 0 .../lexeme/{ => argument}/operand.ts | 0 .../syntax-tree/lexeme/binaryOperator.ts | 47 ------------- src/dash-table/syntax-tree/lexeme/index.ts | 28 +++++--- .../syntax-tree/lexeme/{ => logical}/and.ts | 0 .../syntax-tree/lexeme/{ => logical}/or.ts | 0 .../syntax-tree/lexeme/relational/index.ts | 67 +++++++++++++++++++ .../{unaryOperator.ts => unary/index.ts} | 0 .../lexeme/{unaryNot.ts => unary/not.ts} | 0 src/dash-table/syntax-tree/lexicon/column.ts | 15 +++-- .../syntax-tree/lexicon/columnMulti.ts | 15 +++-- src/dash-table/syntax-tree/lexicon/query.ts | 15 +++-- 15 files changed, 121 insertions(+), 74 deletions(-) rename src/dash-table/syntax-tree/lexeme/{ => argument}/expression.ts (100%) rename src/dash-table/syntax-tree/lexeme/{ => argument}/operand.ts (100%) delete mode 100644 src/dash-table/syntax-tree/lexeme/binaryOperator.ts rename src/dash-table/syntax-tree/lexeme/{ => logical}/and.ts (100%) rename src/dash-table/syntax-tree/lexeme/{ => logical}/or.ts (100%) create mode 100644 src/dash-table/syntax-tree/lexeme/relational/index.ts rename src/dash-table/syntax-tree/lexeme/{unaryOperator.ts => unary/index.ts} (100%) rename src/dash-table/syntax-tree/lexeme/{unaryNot.ts => unary/not.ts} (100%) diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 212ddd82a..5487b9253 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -5,7 +5,7 @@ export enum LexemeType { And = 'and', BlockClose = 'close-block', BlockOpen = 'open-block', - BinaryOperator = 'logical-binary-operator', + RelationalOperator = 'relational-operator', Expression = 'expression', Or = 'or', Operand = 'operand', diff --git a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts index f3e1366ff..14334116e 100644 --- a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts +++ b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts @@ -4,7 +4,7 @@ import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon'; import { ColumnId } from 'dash-table/components/Table/props'; -import { operand, binaryOperator } from './lexeme'; +import { operand, equal } from './lexeme'; import columnLexicon from './lexicon/column'; function isBinary(lexemes: ILexemeResult[]) { @@ -34,7 +34,7 @@ export function modifyLex(key: ColumnId, res: ILexerResult) { } else if (isExpression(res.lexemes)) { res.lexemes = [ { lexeme: boundLexeme(operand), value: `{${key}}` }, - { lexeme: boundLexeme(binaryOperator), value: 'eq' }, + { lexeme: boundLexeme(equal), value: 'eq' }, ...res.lexemes ]; } diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index 67357e806..3d9d7b1e6 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -28,7 +28,7 @@ export const getSingleColumnMap = (ast: MultiColumnsSyntaxTree) => { R.forEach(s => { if (s.lexeme.name === LexemeType.UnaryOperator && s.block) { map.set(s.block.value, new SingleColumnSyntaxTree(s.block.value, s.value)); - } else if (s.lexeme.name === LexemeType.BinaryOperator && s.left && s.right) { + } else if (s.lexeme.name === LexemeType.RelationalOperator && s.left && s.right) { map.set(s.left.value, new SingleColumnSyntaxTree(s.left.value, `${s.value} ${s.right.value}`)); } }, statements); diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/argument/expression.ts similarity index 100% rename from src/dash-table/syntax-tree/lexeme/expression.ts rename to src/dash-table/syntax-tree/lexeme/argument/expression.ts diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/argument/operand.ts similarity index 100% rename from src/dash-table/syntax-tree/lexeme/operand.ts rename to src/dash-table/syntax-tree/lexeme/argument/operand.ts diff --git a/src/dash-table/syntax-tree/lexeme/binaryOperator.ts b/src/dash-table/syntax-tree/lexeme/binaryOperator.ts deleted file mode 100644 index 6f9dfef5f..000000000 --- a/src/dash-table/syntax-tree/lexeme/binaryOperator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Logger from 'core/Logger'; -import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; - -const binaryOperator: IUnboundedLexeme = { - evaluate: (target, tree) => { - Logger.trace('evaluate -> binary', target, tree); - - const t = tree as any; - - const opValue = t.left.lexeme.resolve(target, t.left); - const expValue = t.right.lexeme.resolve(target, t.right); - Logger.trace(`opValue: ${opValue}, expValue: ${expValue}`); - - switch (tree.value.toLowerCase()) { - case 'eq': - case '=': - return opValue === expValue; - case 'gt': - case '>': - return opValue > expValue; - case 'ge': - case '>=': - return opValue >= expValue; - case 'lt': - case '<': - return opValue < expValue; - case 'le': - case '<=': - return opValue <= expValue; - case 'ne': - case '!=': - return opValue !== expValue; - default: - throw new Error(); - } - }, - name: LexemeType.BinaryOperator, - priority: 0, - regexp: /^(>=|<=|>|<|!=|=|ge|le|gt|lt|eq|ne)/i, - syntaxer: (lexs: any[]) => { - let [left, lexeme, right] = lexs; - - return Object.assign({ left, right }, lexeme); - } -}; - -export default binaryOperator; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/index.ts b/src/dash-table/syntax-tree/lexeme/index.ts index 3d850358b..c411da603 100644 --- a/src/dash-table/syntax-tree/lexeme/index.ts +++ b/src/dash-table/syntax-tree/lexeme/index.ts @@ -1,15 +1,27 @@ -import and from './and'; -import binaryOperator from './binaryOperator'; +import and from './logical/and'; +import or from './logical/or'; +import { + equal, + greaterOrEqual, + greaterThan, + lessOrEqual, + lessThan, + notEqual +} from './relational'; import { blockClose, blockOpen } from './block'; -import expression from './expression'; -import operand from './operand'; -import or from './or'; -import unaryNot from './unaryNot'; -import unaryOperator from './unaryOperator'; +import expression from './argument/expression'; +import operand from './argument/operand'; +import unaryNot from './unary/not'; +import unaryOperator from './unary'; export { and, - binaryOperator, + equal, + greaterOrEqual, + greaterThan, + lessOrEqual, + lessThan, + notEqual, blockClose, blockOpen, expression, diff --git a/src/dash-table/syntax-tree/lexeme/and.ts b/src/dash-table/syntax-tree/lexeme/logical/and.ts similarity index 100% rename from src/dash-table/syntax-tree/lexeme/and.ts rename to src/dash-table/syntax-tree/lexeme/logical/and.ts diff --git a/src/dash-table/syntax-tree/lexeme/or.ts b/src/dash-table/syntax-tree/lexeme/logical/or.ts similarity index 100% rename from src/dash-table/syntax-tree/lexeme/or.ts rename to src/dash-table/syntax-tree/lexeme/logical/or.ts diff --git a/src/dash-table/syntax-tree/lexeme/relational/index.ts b/src/dash-table/syntax-tree/lexeme/relational/index.ts new file mode 100644 index 000000000..6e0e70e80 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/relational/index.ts @@ -0,0 +1,67 @@ +import Logger from 'core/Logger'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; + +function evaluator( + target: any, + tree: ISyntaxTree +): [any, any] { + Logger.trace('evaluate -> relational', target, tree); + + const t = tree as any; + + const opValue = t.left.lexeme.resolve(target, t.left); + const expValue = t.right.lexeme.resolve(target, t.right); + Logger.trace(`opValue: ${opValue}, expValue: ${expValue}`); + + return [opValue, expValue]; +} + +function relationalEvaluator( + fn: ([opValue, expValue]: any[]) => boolean +) { + return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree)); +} + +function relationalSyntaxer([left, lexeme, right]: any[]) { + return Object.assign({ left, right }, lexeme); +} + +const LEXEME_BASE = { + name: LexemeType.RelationalOperator, + priority: 0, + regexp: /^(=|eq)/i, + syntaxer: relationalSyntaxer +}; + +const equal: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op === exp), + ...LEXEME_BASE +}; + +const greaterOrEqual: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op >= exp), + ...LEXEME_BASE +}; + +const greaterThan: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op > exp), + ...LEXEME_BASE +}; + +const lessOrEqual: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op <= exp), + ...LEXEME_BASE +}; + +const lessThan: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op < exp), + ...LEXEME_BASE +}; + +const notEqual: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op !== exp), + ...LEXEME_BASE +}; + +export { equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual }; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unaryOperator.ts b/src/dash-table/syntax-tree/lexeme/unary/index.ts similarity index 100% rename from src/dash-table/syntax-tree/lexeme/unaryOperator.ts rename to src/dash-table/syntax-tree/lexeme/unary/index.ts diff --git a/src/dash-table/syntax-tree/lexeme/unaryNot.ts b/src/dash-table/syntax-tree/lexeme/unary/not.ts similarity index 100% rename from src/dash-table/syntax-tree/lexeme/unaryNot.ts rename to src/dash-table/syntax-tree/lexeme/unary/not.ts diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index 870c8dc71..dd72858d5 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -1,7 +1,12 @@ import * as R from 'ramda'; import { - binaryOperator, + equal, + greaterOrEqual, + greaterThan, + lessOrEqual, + lessThan, + notEqual, expression, unaryOperator } from '../lexeme'; @@ -9,11 +14,11 @@ import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; import { ILexemeResult } from 'core/syntax-tree/lexer'; const lexicon: ILexeme[] = [ - { - ...binaryOperator, + ...[equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual].map(op => ({ + ...op, terminal: false, if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous - }, + })), { ...unaryOperator, if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous, @@ -24,7 +29,7 @@ const lexicon: ILexeme[] = [ if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( previous.lexeme.name, - [LexemeType.BinaryOperator] + [LexemeType.RelationalOperator] ), terminal: true } diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index ce7c48060..534361cbb 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -3,7 +3,12 @@ import * as R from 'ramda'; import { and, operand, - binaryOperator, + equal, + greaterOrEqual, + greaterThan, + lessOrEqual, + lessThan, + notEqual, unaryOperator, expression } from '../lexeme'; @@ -32,15 +37,15 @@ const lexicon: ILexeme[] = [ ), terminal: false }, - { - ...binaryOperator, + ...[equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual].map(op => ({ + ...op, if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( previous.lexeme.name, [LexemeType.Operand] ), terminal: false - }, + })), { ...unaryOperator, if: (_: ILexemeResult[], previous: ILexemeResult) => @@ -55,7 +60,7 @@ const lexicon: ILexeme[] = [ if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( previous.lexeme.name, - [LexemeType.BinaryOperator] + [LexemeType.RelationalOperator] ), terminal: true } diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index fc4dbefa6..9bd3d7df2 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -2,7 +2,12 @@ import * as R from 'ramda'; import { and, - binaryOperator, + equal, + greaterOrEqual, + greaterThan, + lessOrEqual, + lessThan, + notEqual, blockClose, blockOpen, expression, @@ -24,7 +29,7 @@ const isTerminal = (lexemes: ILexemeResult[], previous: ILexemeResult) => const ifExpression = (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( previous.lexeme.name, - [LexemeType.BinaryOperator] + [LexemeType.RelationalOperator] ); const ifLogicalOperator = (_: ILexemeResult[], previous: ILexemeResult) => @@ -95,11 +100,11 @@ const lexicon: ILexeme[] = [ ), terminal: false }, - { - ...binaryOperator, + ...[equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual].map(op => ({ + ...op, if: ifOperator, terminal: false - }, + })), { ...unaryOperator, if: ifOperator, From 94896524c6fe1542df1afc3eb99498f0121adad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Mar 2019 13:30:14 -0400 Subject: [PATCH 18/54] one file per relational operator --- .../syntax-tree/lexeme/relational/equal.ts | 9 ++++ .../lexeme/relational/greaterOrEqual.ts | 9 ++++ .../lexeme/relational/greaterThan.ts | 9 ++++ .../syntax-tree/lexeme/relational/index.ts | 51 +++++-------------- .../lexeme/relational/lessOrEqual.ts | 9 ++++ .../syntax-tree/lexeme/relational/lessThan.ts | 9 ++++ .../syntax-tree/lexeme/relational/notEqual.ts | 9 ++++ 7 files changed, 68 insertions(+), 37 deletions(-) create mode 100644 src/dash-table/syntax-tree/lexeme/relational/equal.ts create mode 100644 src/dash-table/syntax-tree/lexeme/relational/greaterOrEqual.ts create mode 100644 src/dash-table/syntax-tree/lexeme/relational/greaterThan.ts create mode 100644 src/dash-table/syntax-tree/lexeme/relational/lessOrEqual.ts create mode 100644 src/dash-table/syntax-tree/lexeme/relational/lessThan.ts create mode 100644 src/dash-table/syntax-tree/lexeme/relational/notEqual.ts diff --git a/src/dash-table/syntax-tree/lexeme/relational/equal.ts b/src/dash-table/syntax-tree/lexeme/relational/equal.ts new file mode 100644 index 000000000..aa868a6f8 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/relational/equal.ts @@ -0,0 +1,9 @@ +import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { relationalEvaluator, LEXEME_BASE } from './index'; + +const equal: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op === exp), + ...LEXEME_BASE +}; + +export default equal; diff --git a/src/dash-table/syntax-tree/lexeme/relational/greaterOrEqual.ts b/src/dash-table/syntax-tree/lexeme/relational/greaterOrEqual.ts new file mode 100644 index 000000000..6415c5fd4 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/relational/greaterOrEqual.ts @@ -0,0 +1,9 @@ +import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { relationalEvaluator, LEXEME_BASE } from './index'; + +const greaterOrEqual: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op >= exp), + ...LEXEME_BASE +}; + +export default greaterOrEqual; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational/greaterThan.ts b/src/dash-table/syntax-tree/lexeme/relational/greaterThan.ts new file mode 100644 index 000000000..9819ef5e9 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/relational/greaterThan.ts @@ -0,0 +1,9 @@ +import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { relationalEvaluator, LEXEME_BASE } from './index'; + +const greaterThan: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op > exp), + ...LEXEME_BASE +}; + +export default greaterThan; diff --git a/src/dash-table/syntax-tree/lexeme/relational/index.ts b/src/dash-table/syntax-tree/lexeme/relational/index.ts index 6e0e70e80..f13eee918 100644 --- a/src/dash-table/syntax-tree/lexeme/relational/index.ts +++ b/src/dash-table/syntax-tree/lexeme/relational/index.ts @@ -1,7 +1,14 @@ import Logger from 'core/Logger'; -import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { LexemeType } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; +import equal from './equal'; +import greaterOrEqual from './greaterOrEqual'; +import greaterThan from './greaterThan'; +import lessOrEqual from './lessOrEqual'; +import lessThan from './lessThan'; +import notEqual from './notEqual'; + function evaluator( target: any, tree: ISyntaxTree @@ -17,51 +24,21 @@ function evaluator( return [opValue, expValue]; } -function relationalEvaluator( +function relationalSyntaxer([left, lexeme, right]: any[]) { + return Object.assign({ left, right }, lexeme); +} + +export function relationalEvaluator( fn: ([opValue, expValue]: any[]) => boolean ) { return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree)); } -function relationalSyntaxer([left, lexeme, right]: any[]) { - return Object.assign({ left, right }, lexeme); -} - -const LEXEME_BASE = { +export const LEXEME_BASE = { name: LexemeType.RelationalOperator, priority: 0, regexp: /^(=|eq)/i, syntaxer: relationalSyntaxer }; -const equal: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op === exp), - ...LEXEME_BASE -}; - -const greaterOrEqual: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op >= exp), - ...LEXEME_BASE -}; - -const greaterThan: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op > exp), - ...LEXEME_BASE -}; - -const lessOrEqual: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op <= exp), - ...LEXEME_BASE -}; - -const lessThan: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op < exp), - ...LEXEME_BASE -}; - -const notEqual: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op !== exp), - ...LEXEME_BASE -}; - export { equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual }; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational/lessOrEqual.ts b/src/dash-table/syntax-tree/lexeme/relational/lessOrEqual.ts new file mode 100644 index 000000000..7fc0a97ab --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/relational/lessOrEqual.ts @@ -0,0 +1,9 @@ +import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { relationalEvaluator, LEXEME_BASE } from './index'; + +const lessOrEqual: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op <= exp), + ...LEXEME_BASE +}; + +export default lessOrEqual; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational/lessThan.ts b/src/dash-table/syntax-tree/lexeme/relational/lessThan.ts new file mode 100644 index 000000000..bd6cafadd --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/relational/lessThan.ts @@ -0,0 +1,9 @@ +import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { relationalEvaluator, LEXEME_BASE } from './index'; + +const lessThan: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op < exp), + ...LEXEME_BASE +}; + +export default lessThan; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational/notEqual.ts b/src/dash-table/syntax-tree/lexeme/relational/notEqual.ts new file mode 100644 index 000000000..f72b5bba8 --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/relational/notEqual.ts @@ -0,0 +1,9 @@ +import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { relationalEvaluator, LEXEME_BASE } from './index'; + +const notEqual: IUnboundedLexeme = { + evaluate: relationalEvaluator(([op, exp]) => op !== exp), + ...LEXEME_BASE +}; + +export default notEqual; \ No newline at end of file From 529c39b4413ca22653df0431344a764405e61f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Mar 2019 14:10:11 -0400 Subject: [PATCH 19/54] rework - all in same file again.. --- .../syntax-tree/lexeme/relational/equal.ts | 9 ---- .../lexeme/relational/greaterOrEqual.ts | 9 ---- .../lexeme/relational/greaterThan.ts | 9 ---- .../syntax-tree/lexeme/relational/index.ts | 46 ++++++++++++++----- .../lexeme/relational/lessOrEqual.ts | 9 ---- .../syntax-tree/lexeme/relational/lessThan.ts | 9 ---- .../syntax-tree/lexeme/relational/notEqual.ts | 9 ---- 7 files changed, 34 insertions(+), 66 deletions(-) delete mode 100644 src/dash-table/syntax-tree/lexeme/relational/equal.ts delete mode 100644 src/dash-table/syntax-tree/lexeme/relational/greaterOrEqual.ts delete mode 100644 src/dash-table/syntax-tree/lexeme/relational/greaterThan.ts delete mode 100644 src/dash-table/syntax-tree/lexeme/relational/lessOrEqual.ts delete mode 100644 src/dash-table/syntax-tree/lexeme/relational/lessThan.ts delete mode 100644 src/dash-table/syntax-tree/lexeme/relational/notEqual.ts diff --git a/src/dash-table/syntax-tree/lexeme/relational/equal.ts b/src/dash-table/syntax-tree/lexeme/relational/equal.ts deleted file mode 100644 index aa868a6f8..000000000 --- a/src/dash-table/syntax-tree/lexeme/relational/equal.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -import { relationalEvaluator, LEXEME_BASE } from './index'; - -const equal: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op === exp), - ...LEXEME_BASE -}; - -export default equal; diff --git a/src/dash-table/syntax-tree/lexeme/relational/greaterOrEqual.ts b/src/dash-table/syntax-tree/lexeme/relational/greaterOrEqual.ts deleted file mode 100644 index 6415c5fd4..000000000 --- a/src/dash-table/syntax-tree/lexeme/relational/greaterOrEqual.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -import { relationalEvaluator, LEXEME_BASE } from './index'; - -const greaterOrEqual: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op >= exp), - ...LEXEME_BASE -}; - -export default greaterOrEqual; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational/greaterThan.ts b/src/dash-table/syntax-tree/lexeme/relational/greaterThan.ts deleted file mode 100644 index 9819ef5e9..000000000 --- a/src/dash-table/syntax-tree/lexeme/relational/greaterThan.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -import { relationalEvaluator, LEXEME_BASE } from './index'; - -const greaterThan: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op > exp), - ...LEXEME_BASE -}; - -export default greaterThan; diff --git a/src/dash-table/syntax-tree/lexeme/relational/index.ts b/src/dash-table/syntax-tree/lexeme/relational/index.ts index f13eee918..803048640 100644 --- a/src/dash-table/syntax-tree/lexeme/relational/index.ts +++ b/src/dash-table/syntax-tree/lexeme/relational/index.ts @@ -1,14 +1,9 @@ +import * as R from 'ramda'; + import Logger from 'core/Logger'; -import { LexemeType } from 'core/syntax-tree/lexicon'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -import equal from './equal'; -import greaterOrEqual from './greaterOrEqual'; -import greaterThan from './greaterThan'; -import lessOrEqual from './lessOrEqual'; -import lessThan from './lessThan'; -import notEqual from './notEqual'; - function evaluator( target: any, tree: ISyntaxTree @@ -28,17 +23,44 @@ function relationalSyntaxer([left, lexeme, right]: any[]) { return Object.assign({ left, right }, lexeme); } -export function relationalEvaluator( +function relationalEvaluator( fn: ([opValue, expValue]: any[]) => boolean ) { return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree)); } -export const LEXEME_BASE = { +const LEXEME_BASE = { name: LexemeType.RelationalOperator, priority: 0, - regexp: /^(=|eq)/i, syntaxer: relationalSyntaxer }; -export { equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual }; \ No newline at end of file +export const equal: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(([op, exp]) => op === exp), + regexp: /^(=|eq)/i +}, LEXEME_BASE); + +export const greaterOrEqual: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(([op, exp]) => op >= exp), + regexp: /^(>=|ge)/i +}, LEXEME_BASE); + +export const greaterThan: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(([op, exp]) => op > exp), + regexp: /^(>|gt)/i +}, LEXEME_BASE); + +export const lessOrEqual: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(([op, exp]) => op <= exp), + regexp: /^(<=|le)/i +}, LEXEME_BASE); + +export const lessThan: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(([op, exp]) => op < exp), + regexp: /^(<|lt)/i +}, LEXEME_BASE); + +export const notEqual: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(([op, exp]) => op !== exp), + regexp: /^(!=|ne)/i +}, LEXEME_BASE); \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational/lessOrEqual.ts b/src/dash-table/syntax-tree/lexeme/relational/lessOrEqual.ts deleted file mode 100644 index 7fc0a97ab..000000000 --- a/src/dash-table/syntax-tree/lexeme/relational/lessOrEqual.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -import { relationalEvaluator, LEXEME_BASE } from './index'; - -const lessOrEqual: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op <= exp), - ...LEXEME_BASE -}; - -export default lessOrEqual; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational/lessThan.ts b/src/dash-table/syntax-tree/lexeme/relational/lessThan.ts deleted file mode 100644 index bd6cafadd..000000000 --- a/src/dash-table/syntax-tree/lexeme/relational/lessThan.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -import { relationalEvaluator, LEXEME_BASE } from './index'; - -const lessThan: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op < exp), - ...LEXEME_BASE -}; - -export default lessThan; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational/notEqual.ts b/src/dash-table/syntax-tree/lexeme/relational/notEqual.ts deleted file mode 100644 index f72b5bba8..000000000 --- a/src/dash-table/syntax-tree/lexeme/relational/notEqual.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IUnboundedLexeme } from 'core/syntax-tree/lexicon'; -import { relationalEvaluator, LEXEME_BASE } from './index'; - -const notEqual: IUnboundedLexeme = { - evaluate: relationalEvaluator(([op, exp]) => op !== exp), - ...LEXEME_BASE -}; - -export default notEqual; \ No newline at end of file From 88baf5f40884d33e9ad99a744505dd3282e80f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Mar 2019 14:34:49 -0400 Subject: [PATCH 20/54] rework unary operators --- src/dash-table/syntax-tree/lexeme/index.ts | 28 ++++- .../syntax-tree/lexeme/unary/index.ts | 105 ++++++++++++------ src/dash-table/syntax-tree/lexicon/column.ts | 33 +++++- .../syntax-tree/lexicon/columnMulti.ts | 31 +++++- src/dash-table/syntax-tree/lexicon/query.ts | 33 +++++- .../tests/standalone/filtering_test.ts | 2 +- 6 files changed, 172 insertions(+), 60 deletions(-) diff --git a/src/dash-table/syntax-tree/lexeme/index.ts b/src/dash-table/syntax-tree/lexeme/index.ts index c411da603..87a7e086a 100644 --- a/src/dash-table/syntax-tree/lexeme/index.ts +++ b/src/dash-table/syntax-tree/lexeme/index.ts @@ -12,21 +12,37 @@ import { blockClose, blockOpen } from './block'; import expression from './argument/expression'; import operand from './argument/operand'; import unaryNot from './unary/not'; -import unaryOperator from './unary'; +import { + isBool, + isEven, + isNil, + isNum, + isObject, + isOdd, + isPrime, + isStr +} from './unary'; export { and, + blockClose, + blockOpen, equal, + expression, greaterOrEqual, greaterThan, + isBool, + isEven, + isNil, + isNum, + isObject, + isOdd, + isPrime, + isStr, lessOrEqual, lessThan, notEqual, - blockClose, - blockOpen, - expression, operand, or, - unaryNot, - unaryOperator + unaryNot }; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unary/index.ts b/src/dash-table/syntax-tree/lexeme/unary/index.ts index 5e4fc4711..c3c16464f 100644 --- a/src/dash-table/syntax-tree/lexeme/unary/index.ts +++ b/src/dash-table/syntax-tree/lexeme/unary/index.ts @@ -1,49 +1,82 @@ +import * as R from 'ramda'; + import Logger from 'core/Logger'; import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; +import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -const isPrime = (c: number) => { +const checkPrimality = (c: number) => { if (c === 2) { return true; } if (c < 2 || c % 2 === 0) { return false; } for (let n = 3; n * n <= c; n += 2) { if (c % n === 0) { return false; } } return true; }; -const unaryOperator: IUnboundedLexeme = { - evaluate: (target, tree) => { - Logger.trace('evaluate -> unary', target, tree); - - const t = tree as any; - const opValue = t.left.lexeme.resolve(target, t.left); - - switch (tree.value.toLowerCase()) { - case 'is even': - return typeof opValue === 'number' && opValue % 2 === 0; - case 'is nil': - return opValue === undefined || opValue === null; - case 'is bool': - return typeof opValue === 'boolean'; - case 'is odd': - return typeof opValue === 'number' && opValue % 2 === 1; - case 'is num': - return typeof opValue === 'number'; - case 'is object': - return opValue !== null && typeof opValue === 'object'; - case 'is str': - return typeof opValue === 'string'; - case 'is prime': - return typeof opValue === 'number' && isPrime(opValue); - default: - throw new Error(); - } - }, +function evaluator( + target: any, + tree: ISyntaxTree +): any { + Logger.trace('evaluate -> unary', target, tree); + + Logger.trace('evaluate -> unary', target, tree); + + const t = tree as any; + const opValue = t.left.lexeme.resolve(target, t.left); + + return opValue; +} + +function relationalSyntaxer([left, lexeme]: any[]) { + return Object.assign({ left }, lexeme); +} + +function relationalEvaluator( + fn: (opValue: any[]) => boolean +) { + return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree)); +} + +const LEXEME_BASE = { name: LexemeType.UnaryOperator, priority: 0, - regexp: /^((is nil)|(is odd)|(is even)|(is bool)|(is num)|(is object)|(is str)|(is prime))/i, - syntaxer: (lexs: any[]) => { - let [left, lexeme] = lexs; - - return Object.assign({ left }, lexeme); - } + syntaxer: relationalSyntaxer }; -export default unaryOperator; \ No newline at end of file +export const isBool: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => typeof opValue === 'boolean'), + regexp: /^(is bool)/i +}, LEXEME_BASE); + +export const isEven: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && opValue % 2 === 0), + regexp: /^(is even)/i +}, LEXEME_BASE); + +export const isNil: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => opValue === undefined || opValue === null), + regexp: /^(is nil)/i +}, LEXEME_BASE); + +export const isNum: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => typeof opValue === 'number'), + regexp: /^(is num)/i +}, LEXEME_BASE); + +export const isObject: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => opValue !== null && typeof opValue === 'object'), + regexp: /^(is object)/i +}, LEXEME_BASE); + +export const isOdd: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && opValue % 2 === 1), + regexp: /^(is odd)/i +}, LEXEME_BASE); + +export const isPrime: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => typeof opValue === 'number' && checkPrimality(opValue)), + regexp: /^(is prime)/i +}, LEXEME_BASE); + +export const isStr: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(opValue => typeof opValue === 'string'), + regexp: /^(is str)/i +}, LEXEME_BASE); \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index dd72858d5..3de808e51 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -4,26 +4,47 @@ import { equal, greaterOrEqual, greaterThan, + isBool, + isEven, + isNil, + isNum, + isObject, + isOdd, + isPrime, + isStr, lessOrEqual, lessThan, notEqual, - expression, - unaryOperator + expression } from '../lexeme'; import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; import { ILexemeResult } from 'core/syntax-tree/lexer'; const lexicon: ILexeme[] = [ - ...[equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual].map(op => ({ + ...[equal, + greaterOrEqual, + greaterThan, + lessOrEqual, + lessThan, + notEqual + ].map(op => ({ ...op, terminal: false, if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous })), - { - ...unaryOperator, + ...[isBool, + isEven, + isNil, + isNum, + isObject, + isOdd, + isPrime, + isStr + ].map(op => ({ + ...op, if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous, terminal: true - }, + })), { ...expression, if: (_lexs: ILexemeResult[], previous: ILexemeResult) => diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index 534361cbb..ae871118b 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -6,10 +6,17 @@ import { equal, greaterOrEqual, greaterThan, + isBool, + isEven, + isNil, + isNum, + isObject, + isOdd, + isPrime, + isStr, lessOrEqual, lessThan, notEqual, - unaryOperator, expression } from '../lexeme'; import { ILexeme, LexemeType } from 'core/syntax-tree/lexicon'; @@ -37,7 +44,13 @@ const lexicon: ILexeme[] = [ ), terminal: false }, - ...[equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual].map(op => ({ + ...[equal, + greaterOrEqual, + greaterThan, + lessOrEqual, + lessThan, + notEqual + ].map(op => ({ ...op, if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( @@ -46,15 +59,23 @@ const lexicon: ILexeme[] = [ ), terminal: false })), - { - ...unaryOperator, + ...[isBool, + isEven, + isNil, + isNum, + isObject, + isOdd, + isPrime, + isStr + ].map(op => ({ + ...op, if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( previous.lexeme.name, [LexemeType.Operand] ), terminal: true - }, + })), { ...expression, if: (_: ILexemeResult[], previous: ILexemeResult) => diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index 9bd3d7df2..b1838538b 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -5,6 +5,14 @@ import { equal, greaterOrEqual, greaterThan, + isBool, + isEven, + isNil, + isNum, + isObject, + isOdd, + isPrime, + isStr, lessOrEqual, lessThan, notEqual, @@ -13,8 +21,7 @@ import { expression, operand, or, - unaryNot, - unaryOperator + unaryNot } from '../lexeme'; import { ILexemeResult } from 'core/syntax-tree/lexer'; import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; @@ -100,16 +107,30 @@ const lexicon: ILexeme[] = [ ), terminal: false }, - ...[equal, greaterOrEqual, greaterThan, lessOrEqual, lessThan, notEqual].map(op => ({ + ...[equal, + greaterOrEqual, + greaterThan, + lessOrEqual, + lessThan, + notEqual + ].map(op => ({ ...op, if: ifOperator, terminal: false })), - { - ...unaryOperator, + ...[isBool, + isEven, + isNil, + isNum, + isObject, + isOdd, + isPrime, + isStr + ].map(op => ({ + ...op, if: ifOperator, terminal: isTerminal - }, + })), { ...unaryNot, if: (_: ILexemeResult[], previous: ILexemeResult) => diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index 0a4b89bc7..e0851b8db 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -40,7 +40,7 @@ describe('filter', () => { DashTable.toggleScroll(false); }); - it.only('handles invalid queries', () => { + it('handles invalid queries', () => { let cell_0; let cell_1; From dcd9af8a0e0eaab8849a01a76e1eda05b9e1187b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Mar 2019 15:51:10 -0400 Subject: [PATCH 21/54] fix standalone filtering tests --- src/dash-table/components/FilterFactory.tsx | 14 +++++++------- src/dash-table/syntax-tree/index.ts | 10 +++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 584525417..f1418f102 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -47,19 +47,19 @@ export default class FilterFactory { } - private onChange = (columnId: ColumnId, ops: Map, setFilter: SetFilter, ev: any) => { + private onChange = (columnId: ColumnId, setFilter: SetFilter, ev: any) => { Logger.debug('Filter -- onChange', columnId, ev.target.value && ev.target.value.trim()); const value = ev.target.value.trim(); const safeColumnId = columnId.toString(); if (value && value.length) { - ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, value)); + this.ops.set(safeColumnId, new SingleColumnSyntaxTree(safeColumnId, value)); } else { - ops.delete(safeColumnId); + this.ops.delete(safeColumnId); } - const asts = Array.from(ops.values()); + const asts = Array.from(this.ops.values()); const globalFilter = getMultiColumnQueryString(asts); const rawGlobalFilter = R.map( @@ -70,13 +70,13 @@ export default class FilterFactory { setFilter(globalFilter, rawGlobalFilter); } - private getEventHandler = (fn: Function, columnId: ColumnId, ops: Map, setFilter: SetFilter): any => { + private getEventHandler = (fn: Function, columnId: ColumnId, setFilter: SetFilter): any => { const fnHandler = (this.handlers.get(fn) || this.handlers.set(fn, new Map()).get(fn)); const columnIdHandler = (fnHandler.get(columnId) || fnHandler.set(columnId, new Map()).get(columnId)); return ( columnIdHandler.get(setFilter) || - (columnIdHandler.set(setFilter, fn.bind(this, columnId, ops, setFilter)).get(setFilter)) + (columnIdHandler.set(setFilter, fn.bind(this, columnId, setFilter)).get(setFilter)) ); } @@ -128,7 +128,7 @@ export default class FilterFactory { classes={`dash-filter column-${index}`} columnId={column.id} isValid={!ast || ast.isValid} - setFilter={this.getEventHandler(this.onChange, column.id, this.ops, setFilter)} + setFilter={this.getEventHandler(this.onChange, column.id, setFilter)} value={ast && ast.query} />); }, columns); diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index 3d9d7b1e6..51a7326ef 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -6,6 +6,8 @@ import MultiColumnsSyntaxTree from './MultiColumnsSyntaxTree'; import QuerySyntaxTree from './QuerySyntaxTree'; import SingleColumnSyntaxTree from './SingleColumnSyntaxTree'; +const EXTRACT_COLUMN_REGEX = /^{|}$/g; + export const getMultiColumnQueryString = ( asts: SingleColumnSyntaxTree[] ) => R.map( @@ -26,10 +28,12 @@ export const getSingleColumnMap = (ast: MultiColumnsSyntaxTree) => { } R.forEach(s => { - if (s.lexeme.name === LexemeType.UnaryOperator && s.block) { - map.set(s.block.value, new SingleColumnSyntaxTree(s.block.value, s.value)); + if (s.lexeme.name === LexemeType.UnaryOperator && s.left) { + const sanitizedColumnId = s.left.value.replace(EXTRACT_COLUMN_REGEX, ''); + map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, s.value)); } else if (s.lexeme.name === LexemeType.RelationalOperator && s.left && s.right) { - map.set(s.left.value, new SingleColumnSyntaxTree(s.left.value, `${s.value} ${s.right.value}`)); + const sanitizedColumnId = s.left.value.replace(EXTRACT_COLUMN_REGEX, ''); + map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, `${s.value} ${s.right.value}`)); } }, statements); From efb9a70e0566051f2e730894f75862a241da1993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Mar 2019 16:11:52 -0400 Subject: [PATCH 22/54] reorganize code --- src/core/syntax-tree/lexer.ts | 2 +- src/core/syntax-tree/lexicon.ts | 2 +- src/core/syntax-tree/syntaxer.ts | 2 +- .../syntax-tree/MultiColumnsSyntaxTree.ts | 2 +- .../syntax-tree/SingleColumnSyntaxTree.ts | 8 ++-- src/dash-table/syntax-tree/index.ts | 4 +- .../syntax-tree/lexeme/{logical => }/and.ts | 2 +- src/dash-table/syntax-tree/lexeme/block.ts | 6 +-- .../lexeme/{argument => }/expression.ts | 2 +- src/dash-table/syntax-tree/lexeme/index.ts | 48 ------------------- .../syntax-tree/lexeme/{unary => }/not.ts | 2 +- .../lexeme/{argument => }/operand.ts | 2 +- .../syntax-tree/lexeme/{logical => }/or.ts | 2 +- .../{relational/index.ts => relational.ts} | 4 +- .../lexeme/{unary/index.ts => unary.ts} | 4 +- src/dash-table/syntax-tree/lexicon/column.ts | 17 ++++--- .../syntax-tree/lexicon/columnMulti.ts | 29 ++++++----- src/dash-table/syntax-tree/lexicon/query.ts | 43 +++++++++-------- 18 files changed, 73 insertions(+), 108 deletions(-) rename src/dash-table/syntax-tree/lexeme/{logical => }/and.ts (96%) rename src/dash-table/syntax-tree/lexeme/{argument => }/expression.ts (97%) delete mode 100644 src/dash-table/syntax-tree/lexeme/index.ts rename src/dash-table/syntax-tree/lexeme/{unary => }/not.ts (94%) rename src/dash-table/syntax-tree/lexeme/{argument => }/operand.ts (94%) rename src/dash-table/syntax-tree/lexeme/{logical => }/or.ts (96%) rename src/dash-table/syntax-tree/lexeme/{relational/index.ts => relational.ts} (96%) rename src/dash-table/syntax-tree/lexeme/{unary/index.ts => unary.ts} (97%) diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts index b68c7044f..7e5db437f 100644 --- a/src/core/syntax-tree/lexer.ts +++ b/src/core/syntax-tree/lexer.ts @@ -25,7 +25,7 @@ export default function lexer(lexicon: Lexicon, query: string): ILexerResult { (!Array.isArray(lexeme.if) ? lexeme.if(result, previous) : (previousLexeme ? - lexeme.if && lexeme.if.indexOf(previousLexeme.name) !== -1 : + lexeme.if && lexeme.if.indexOf(previousLexeme.type) !== -1 : lexeme.if && lexeme.if.indexOf(undefined) !== -1)) ); diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 5487b9253..89b2f18f4 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -16,7 +16,7 @@ export enum LexemeType { export interface IUnboundedLexeme { evaluate?: (target: any, tree: ISyntaxTree) => boolean; resolve?: (target: any, tree: ISyntaxTree) => any; - name: string; + type: string; nesting?: number; priority?: number; regexp: RegExp; diff --git a/src/core/syntax-tree/syntaxer.ts b/src/core/syntax-tree/syntaxer.ts index 8729ed1df..d420747e0 100644 --- a/src/core/syntax-tree/syntaxer.ts +++ b/src/core/syntax-tree/syntaxer.ts @@ -53,7 +53,7 @@ const parser = (lexs: ILexemeResult[]): ISyntaxTree => { return tree; } else { - throw new Error(pivot.lexeme.name); + throw new Error(pivot.lexeme.type); } }; diff --git a/src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts b/src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts index 2fe5b131c..e492e1576 100644 --- a/src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts +++ b/src/dash-table/syntax-tree/MultiColumnsSyntaxTree.ts @@ -39,7 +39,7 @@ export default class MultiColumnsSyntaxTree extends SyntaxTree { return statements; } private respectsBasicSyntax() { - const fields = R.map(item => item.value, R.filter(i => i.lexeme.name === LexemeType.Operand, this.lexerResult.lexemes)); + const fields = R.map(item => item.value, R.filter(i => i.lexeme.type === LexemeType.Operand, this.lexerResult.lexemes)); const uniqueFields = R.uniq(fields); return fields.length === uniqueFields.length; } diff --git a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts index 14334116e..a2d2b7076 100644 --- a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts +++ b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts @@ -4,7 +4,9 @@ import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon'; import { ColumnId } from 'dash-table/components/Table/props'; -import { operand, equal } from './lexeme'; +import operand from './lexeme/operand'; +import { equal } from './lexeme/relational'; + import columnLexicon from './lexicon/column'; function isBinary(lexemes: ILexemeResult[]) { @@ -13,12 +15,12 @@ function isBinary(lexemes: ILexemeResult[]) { function isExpression(lexemes: ILexemeResult[]) { return lexemes.length === 1 && - lexemes[0].lexeme.name === LexemeType.Expression; + lexemes[0].lexeme.type === LexemeType.Expression; } function isUnary(lexemes: ILexemeResult[]) { return lexemes.length === 1 && - lexemes[0].lexeme.name === LexemeType.UnaryOperator; + lexemes[0].lexeme.type === LexemeType.UnaryOperator; } export function modifyLex(key: ColumnId, res: ILexerResult) { diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index 51a7326ef..b8ee4614a 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -28,10 +28,10 @@ export const getSingleColumnMap = (ast: MultiColumnsSyntaxTree) => { } R.forEach(s => { - if (s.lexeme.name === LexemeType.UnaryOperator && s.left) { + if (s.lexeme.type === LexemeType.UnaryOperator && s.left) { const sanitizedColumnId = s.left.value.replace(EXTRACT_COLUMN_REGEX, ''); map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, s.value)); - } else if (s.lexeme.name === LexemeType.RelationalOperator && s.left && s.right) { + } else if (s.lexeme.type === LexemeType.RelationalOperator && s.left && s.right) { const sanitizedColumnId = s.left.value.replace(EXTRACT_COLUMN_REGEX, ''); map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, `${s.value} ${s.right.value}`)); } diff --git a/src/dash-table/syntax-tree/lexeme/logical/and.ts b/src/dash-table/syntax-tree/lexeme/and.ts similarity index 96% rename from src/dash-table/syntax-tree/lexeme/logical/and.ts rename to src/dash-table/syntax-tree/lexeme/and.ts index 314c521ec..31a71184b 100644 --- a/src/dash-table/syntax-tree/lexeme/logical/and.ts +++ b/src/dash-table/syntax-tree/lexeme/and.ts @@ -10,7 +10,7 @@ const and: IUnboundedLexeme = { const rv = t.right.lexeme.evaluate(target, t.right); return lv && rv; }, - name: LexemeType.And, + type: LexemeType.And, priority: 2, regexp: /^(and\s|&&)/i, syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { diff --git a/src/dash-table/syntax-tree/lexeme/block.ts b/src/dash-table/syntax-tree/lexeme/block.ts index dd0076882..c5e9f7fff 100644 --- a/src/dash-table/syntax-tree/lexeme/block.ts +++ b/src/dash-table/syntax-tree/lexeme/block.ts @@ -2,9 +2,9 @@ import Logger from 'core/Logger'; import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; export const blockClose: IUnboundedLexeme = { - name: LexemeType.BlockClose, nesting: -1, - regexp: /^\)/ + regexp: /^\)/, + type: LexemeType.BlockClose }; export const blockOpen: IUnboundedLexeme = { @@ -15,7 +15,7 @@ export const blockOpen: IUnboundedLexeme = { return t.block.lexeme.evaluate(target, t.block); }, - name: LexemeType.BlockOpen, + type: LexemeType.BlockOpen, nesting: 1, priority: 1, regexp: /^\(/, diff --git a/src/dash-table/syntax-tree/lexeme/argument/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts similarity index 97% rename from src/dash-table/syntax-tree/lexeme/argument/expression.ts rename to src/dash-table/syntax-tree/lexeme/expression.ts index a671b64c5..d651c2926 100644 --- a/src/dash-table/syntax-tree/lexeme/argument/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -25,7 +25,7 @@ const expression: IUnboundedLexeme = { } }, regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/, - name: LexemeType.Expression + type: LexemeType.Expression }; export default expression; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/index.ts b/src/dash-table/syntax-tree/lexeme/index.ts deleted file mode 100644 index 87a7e086a..000000000 --- a/src/dash-table/syntax-tree/lexeme/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import and from './logical/and'; -import or from './logical/or'; -import { - equal, - greaterOrEqual, - greaterThan, - lessOrEqual, - lessThan, - notEqual -} from './relational'; -import { blockClose, blockOpen } from './block'; -import expression from './argument/expression'; -import operand from './argument/operand'; -import unaryNot from './unary/not'; -import { - isBool, - isEven, - isNil, - isNum, - isObject, - isOdd, - isPrime, - isStr -} from './unary'; - -export { - and, - blockClose, - blockOpen, - equal, - expression, - greaterOrEqual, - greaterThan, - isBool, - isEven, - isNil, - isNum, - isObject, - isOdd, - isPrime, - isStr, - lessOrEqual, - lessThan, - notEqual, - operand, - or, - unaryNot -}; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unary/not.ts b/src/dash-table/syntax-tree/lexeme/not.ts similarity index 94% rename from src/dash-table/syntax-tree/lexeme/unary/not.ts rename to src/dash-table/syntax-tree/lexeme/not.ts index 6c0f6c1b7..5bd0597c1 100644 --- a/src/dash-table/syntax-tree/lexeme/unary/not.ts +++ b/src/dash-table/syntax-tree/lexeme/not.ts @@ -9,7 +9,7 @@ const unaryNot: IUnboundedLexeme = { return !t.right.lexeme.evaluate(target, t.right); }, - name: LexemeType.UnaryNot, + type: LexemeType.UnaryNot, priority: 1.5, regexp: /^!/, syntaxer: (lexs: any[]) => { diff --git a/src/dash-table/syntax-tree/lexeme/argument/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts similarity index 94% rename from src/dash-table/syntax-tree/lexeme/argument/operand.ts rename to src/dash-table/syntax-tree/lexeme/operand.ts index 387666fd9..2400eaa0c 100644 --- a/src/dash-table/syntax-tree/lexeme/argument/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -14,7 +14,7 @@ const operand: IUnboundedLexeme = { } }, regexp: REGEX, - name: LexemeType.Operand + type: LexemeType.Operand }; export default operand; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/logical/or.ts b/src/dash-table/syntax-tree/lexeme/or.ts similarity index 96% rename from src/dash-table/syntax-tree/lexeme/logical/or.ts rename to src/dash-table/syntax-tree/lexeme/or.ts index 93471058c..19d91760e 100644 --- a/src/dash-table/syntax-tree/lexeme/logical/or.ts +++ b/src/dash-table/syntax-tree/lexeme/or.ts @@ -10,7 +10,7 @@ const or: IUnboundedLexeme = { return t.left.lexeme.evaluate(target, t.left) || t.right.lexeme.evaluate(target, t.right); }, - name: LexemeType.Or, + type: LexemeType.Or, priority: 3, regexp: /^(or\s|\|\|)/i, syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { diff --git a/src/dash-table/syntax-tree/lexeme/relational/index.ts b/src/dash-table/syntax-tree/lexeme/relational.ts similarity index 96% rename from src/dash-table/syntax-tree/lexeme/relational/index.ts rename to src/dash-table/syntax-tree/lexeme/relational.ts index 803048640..1b244a785 100644 --- a/src/dash-table/syntax-tree/lexeme/relational/index.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -30,9 +30,9 @@ function relationalEvaluator( } const LEXEME_BASE = { - name: LexemeType.RelationalOperator, priority: 0, - syntaxer: relationalSyntaxer + syntaxer: relationalSyntaxer, + type: LexemeType.RelationalOperator }; export const equal: IUnboundedLexeme = R.merge({ diff --git a/src/dash-table/syntax-tree/lexeme/unary/index.ts b/src/dash-table/syntax-tree/lexeme/unary.ts similarity index 97% rename from src/dash-table/syntax-tree/lexeme/unary/index.ts rename to src/dash-table/syntax-tree/lexeme/unary.ts index c3c16464f..fd326dfa3 100644 --- a/src/dash-table/syntax-tree/lexeme/unary/index.ts +++ b/src/dash-table/syntax-tree/lexeme/unary.ts @@ -36,9 +36,9 @@ function relationalEvaluator( } const LEXEME_BASE = { - name: LexemeType.UnaryOperator, priority: 0, - syntaxer: relationalSyntaxer + syntaxer: relationalSyntaxer, + type: LexemeType.UnaryOperator }; export const isBool: IUnboundedLexeme = R.merge({ diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index 3de808e51..47cc0d805 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -1,9 +1,15 @@ import * as R from 'ramda'; +import expression from '../lexeme/expression'; import { equal, greaterOrEqual, greaterThan, + lessOrEqual, + lessThan, + notEqual +} from '../lexeme/relational'; +import { isBool, isEven, isNil, @@ -11,12 +17,9 @@ import { isObject, isOdd, isPrime, - isStr, - lessOrEqual, - lessThan, - notEqual, - expression -} from '../lexeme'; + isStr +} from '../lexeme/unary'; + import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; import { ILexemeResult } from 'core/syntax-tree/lexer'; @@ -49,7 +52,7 @@ const lexicon: ILexeme[] = [ ...expression, if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( - previous.lexeme.name, + previous.lexeme.type, [LexemeType.RelationalOperator] ), terminal: true diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index ae871118b..31c3be4a6 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -1,11 +1,17 @@ import * as R from 'ramda'; +import and from '../lexeme/and'; +import expression from '../lexeme/expression'; +import operand from '../lexeme/operand'; import { - and, - operand, equal, greaterOrEqual, greaterThan, + lessOrEqual, + lessThan, + notEqual +} from '../lexeme/relational'; +import { isBool, isEven, isNil, @@ -13,12 +19,9 @@ import { isObject, isOdd, isPrime, - isStr, - lessOrEqual, - lessThan, - notEqual, - expression -} from '../lexeme'; + isStr +} from '../lexeme/unary'; + import { ILexeme, LexemeType } from 'core/syntax-tree/lexicon'; import { ILexemeResult } from 'core/syntax-tree/lexer'; @@ -27,7 +30,7 @@ const lexicon: ILexeme[] = [ ...and, if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( - previous.lexeme.name, + previous.lexeme.type, [ LexemeType.Expression, LexemeType.UnaryOperator @@ -39,7 +42,7 @@ const lexicon: ILexeme[] = [ ...operand, if: (_: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( - previous.lexeme.name, + previous.lexeme.type, [LexemeType.And] ), terminal: false @@ -54,7 +57,7 @@ const lexicon: ILexeme[] = [ ...op, if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( - previous.lexeme.name, + previous.lexeme.type, [LexemeType.Operand] ), terminal: false @@ -71,7 +74,7 @@ const lexicon: ILexeme[] = [ ...op, if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( - previous.lexeme.name, + previous.lexeme.type, [LexemeType.Operand] ), terminal: true @@ -80,7 +83,7 @@ const lexicon: ILexeme[] = [ ...expression, if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( - previous.lexeme.name, + previous.lexeme.type, [LexemeType.RelationalOperator] ), terminal: true diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index b1838538b..9a5835687 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -1,10 +1,23 @@ import * as R from 'ramda'; +import and from '../lexeme/and'; +import expression from '../lexeme/expression'; +import operand from '../lexeme/operand'; +import or from '../lexeme/or'; +import unaryNot from '../lexeme/not'; +import { + blockClose, + blockOpen +} from '../lexeme/block'; import { - and, equal, greaterOrEqual, greaterThan, + lessOrEqual, + lessThan, + notEqual +} from '../lexeme/relational'; +import { isBool, isEven, isNil, @@ -12,17 +25,9 @@ import { isObject, isOdd, isPrime, - isStr, - lessOrEqual, - lessThan, - notEqual, - blockClose, - blockOpen, - expression, - operand, - or, - unaryNot -} from '../lexeme'; + isStr +} from '../lexeme/unary'; + import { ILexemeResult } from 'core/syntax-tree/lexer'; import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; @@ -35,13 +40,13 @@ const isTerminal = (lexemes: ILexemeResult[], previous: ILexemeResult) => const ifExpression = (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( - previous.lexeme.name, + previous.lexeme.type, [LexemeType.RelationalOperator] ); const ifLogicalOperator = (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( - previous.lexeme.name, + previous.lexeme.type, [ LexemeType.BlockClose, LexemeType.Expression, @@ -51,7 +56,7 @@ const ifLogicalOperator = (_: ILexemeResult[], previous: ILexemeResult) => const ifOperator = (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( - previous.lexeme.name, + previous.lexeme.type, [LexemeType.Operand] ); @@ -70,7 +75,7 @@ const lexicon: ILexeme[] = [ ...blockClose, if: (lexemes: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( - previous.lexeme.name, + previous.lexeme.type, [ LexemeType.BlockClose, LexemeType.BlockOpen, @@ -84,7 +89,7 @@ const lexicon: ILexeme[] = [ ...blockOpen, if: (_: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( - previous.lexeme.name, + previous.lexeme.type, [ LexemeType.And, LexemeType.BlockOpen, @@ -98,7 +103,7 @@ const lexicon: ILexeme[] = [ ...operand, if: (_: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( - previous.lexeme.name, + previous.lexeme.type, [ LexemeType.And, LexemeType.BlockOpen, @@ -135,7 +140,7 @@ const lexicon: ILexeme[] = [ ...unaryNot, if: (_: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( - previous.lexeme.name, + previous.lexeme.type, [ LexemeType.And, LexemeType.Or, From 3da02a15e1da4a33d8cd16452a443d05315b78e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Mar 2019 16:28:07 -0400 Subject: [PATCH 23/54] lexeme nomenclature --- src/core/syntax-tree/lexicon.ts | 9 ++++----- src/dash-table/syntax-tree/lexeme/and.ts | 3 ++- src/dash-table/syntax-tree/lexeme/block.ts | 1 + src/dash-table/syntax-tree/lexeme/expression.ts | 1 + src/dash-table/syntax-tree/lexeme/not.ts | 3 ++- src/dash-table/syntax-tree/lexeme/operand.ts | 11 ++++++++--- src/dash-table/syntax-tree/lexeme/or.ts | 3 ++- src/dash-table/syntax-tree/lexeme/relational.ts | 6 ++++++ src/dash-table/syntax-tree/lexeme/unary.ts | 1 + src/dash-table/syntax-tree/lexicon/columnMulti.ts | 2 +- src/dash-table/syntax-tree/lexicon/query.ts | 13 +++++-------- 11 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 89b2f18f4..9891d4d6e 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -2,19 +2,18 @@ import { ILexemeResult } from './lexer'; import { ISyntaxTree } from './syntaxer'; export enum LexemeType { - And = 'and', BlockClose = 'close-block', BlockOpen = 'open-block', + LogicalOperator = 'logical-operator', RelationalOperator = 'relational-operator', + UnaryOperator = 'unary-operator', Expression = 'expression', - Or = 'or', - Operand = 'operand', - UnaryNot = 'unary-not', - UnaryOperator = 'logical-unary-operator' + Operand = 'operand' } export interface IUnboundedLexeme { evaluate?: (target: any, tree: ISyntaxTree) => boolean; + present?: string | ((tree: ISyntaxTree) => string); resolve?: (target: any, tree: ISyntaxTree) => any; type: string; nesting?: number; diff --git a/src/dash-table/syntax-tree/lexeme/and.ts b/src/dash-table/syntax-tree/lexeme/and.ts index 31a71184b..5c724536d 100644 --- a/src/dash-table/syntax-tree/lexeme/and.ts +++ b/src/dash-table/syntax-tree/lexeme/and.ts @@ -10,9 +10,10 @@ const and: IUnboundedLexeme = { const rv = t.right.lexeme.evaluate(target, t.right); return lv && rv; }, - type: LexemeType.And, + type: LexemeType.LogicalOperator, priority: 2, regexp: /^(and\s|&&)/i, + present: '&&', syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { return Object.assign({ left: lexs.slice(0, pivotIndex), diff --git a/src/dash-table/syntax-tree/lexeme/block.ts b/src/dash-table/syntax-tree/lexeme/block.ts index c5e9f7fff..935167ff8 100644 --- a/src/dash-table/syntax-tree/lexeme/block.ts +++ b/src/dash-table/syntax-tree/lexeme/block.ts @@ -17,6 +17,7 @@ export const blockOpen: IUnboundedLexeme = { }, type: LexemeType.BlockOpen, nesting: 1, + present: '()', priority: 1, regexp: /^\(/, syntaxer: (lexs: any[]) => { diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index d651c2926..7fc49fd41 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -2,6 +2,7 @@ import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; const expression: IUnboundedLexeme = { + present: (tree: ISyntaxTree) => tree.value, resolve: (target: any, tree: ISyntaxTree) => { if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { return tree.value.slice(1, tree.value.length - 1); diff --git a/src/dash-table/syntax-tree/lexeme/not.ts b/src/dash-table/syntax-tree/lexeme/not.ts index 5bd0597c1..796390455 100644 --- a/src/dash-table/syntax-tree/lexeme/not.ts +++ b/src/dash-table/syntax-tree/lexeme/not.ts @@ -9,7 +9,8 @@ const unaryNot: IUnboundedLexeme = { return !t.right.lexeme.evaluate(target, t.right); }, - type: LexemeType.UnaryNot, + type: LexemeType.UnaryOperator, + present: '!', priority: 1.5, regexp: /^!/, syntaxer: (lexs: any[]) => { diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts index 2400eaa0c..53a7dad20 100644 --- a/src/dash-table/syntax-tree/lexeme/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -3,12 +3,17 @@ import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; const REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; +const getValue = ( + value: string +) => value + .slice(1, value.length - 1) + .replace(/\\([{}])/g, '$1'); + const operand: IUnboundedLexeme = { + present: (tree: ISyntaxTree) => getValue(tree.value), resolve: (target: any, tree: ISyntaxTree) => { if (REGEX.test(tree.value)) { - return target[ - tree.value.slice(1, tree.value.length - 1).replace(/\\([{}])/g, '$1') - ]; + return target[getValue(tree.value)]; } else { throw new Error(); } diff --git a/src/dash-table/syntax-tree/lexeme/or.ts b/src/dash-table/syntax-tree/lexeme/or.ts index 19d91760e..28ec037a1 100644 --- a/src/dash-table/syntax-tree/lexeme/or.ts +++ b/src/dash-table/syntax-tree/lexeme/or.ts @@ -10,7 +10,8 @@ const or: IUnboundedLexeme = { return t.left.lexeme.evaluate(target, t.left) || t.right.lexeme.evaluate(target, t.right); }, - type: LexemeType.Or, + type: LexemeType.LogicalOperator, + present: '||', priority: 3, regexp: /^(or\s|\|\|)/i, syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts index 1b244a785..435d7abdb 100644 --- a/src/dash-table/syntax-tree/lexeme/relational.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -37,30 +37,36 @@ const LEXEME_BASE = { export const equal: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op === exp), + present: '=', regexp: /^(=|eq)/i }, LEXEME_BASE); export const greaterOrEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op >= exp), + present: '>=', regexp: /^(>=|ge)/i }, LEXEME_BASE); export const greaterThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op > exp), + present: '>', regexp: /^(>|gt)/i }, LEXEME_BASE); export const lessOrEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op <= exp), + present: '<=', regexp: /^(<=|le)/i }, LEXEME_BASE); export const lessThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op < exp), + present: '<', regexp: /^(<|lt)/i }, LEXEME_BASE); export const notEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op !== exp), + present: '!=', regexp: /^(!=|ne)/i }, LEXEME_BASE); \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unary.ts b/src/dash-table/syntax-tree/lexeme/unary.ts index fd326dfa3..a20c5c2f7 100644 --- a/src/dash-table/syntax-tree/lexeme/unary.ts +++ b/src/dash-table/syntax-tree/lexeme/unary.ts @@ -36,6 +36,7 @@ function relationalEvaluator( } const LEXEME_BASE = { + present: (tree: ISyntaxTree) => tree.value, priority: 0, syntaxer: relationalSyntaxer, type: LexemeType.UnaryOperator diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index 31c3be4a6..c7bcfc182 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -43,7 +43,7 @@ const lexicon: ILexeme[] = [ if: (_: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( previous.lexeme.type, - [LexemeType.And] + [LexemeType.LogicalOperator] ), terminal: false }, diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index 9a5835687..b82e7567e 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -91,10 +91,9 @@ const lexicon: ILexeme[] = [ !previous || R.contains( previous.lexeme.type, [ - LexemeType.And, LexemeType.BlockOpen, - LexemeType.Or, - LexemeType.UnaryNot + LexemeType.LogicalOperator, + LexemeType.UnaryOperator ] ), terminal: false @@ -105,9 +104,8 @@ const lexicon: ILexeme[] = [ !previous || R.contains( previous.lexeme.type, [ - LexemeType.And, LexemeType.BlockOpen, - LexemeType.Or + LexemeType.LogicalOperator ] ), terminal: false @@ -142,9 +140,8 @@ const lexicon: ILexeme[] = [ !previous || R.contains( previous.lexeme.type, [ - LexemeType.And, - LexemeType.Or, - LexemeType.UnaryNot + LexemeType.LogicalOperator, + LexemeType.UnaryOperator ] ), terminal: false From f497ce289f8d528a6946b2bec54421962053d77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Mar 2019 10:46:57 -0400 Subject: [PATCH 24/54] rework, add presentation, add sub-type enums --- src/core/syntax-tree/lexicon.ts | 2 +- src/dash-table/syntax-tree/index.ts | 6 +-- src/dash-table/syntax-tree/lexeme/and.ts | 25 ---------- src/dash-table/syntax-tree/lexeme/block.ts | 2 +- src/dash-table/syntax-tree/lexeme/logical.ts | 49 +++++++++++++++++++ src/dash-table/syntax-tree/lexeme/not.ts | 23 --------- src/dash-table/syntax-tree/lexeme/or.ts | 25 ---------- .../syntax-tree/lexeme/relational.ts | 21 +++++--- src/dash-table/syntax-tree/lexeme/unary.ts | 23 +++++++++ .../syntax-tree/lexicon/columnMulti.ts | 4 +- src/dash-table/syntax-tree/lexicon/query.ts | 12 +++-- 11 files changed, 101 insertions(+), 91 deletions(-) delete mode 100644 src/dash-table/syntax-tree/lexeme/and.ts create mode 100644 src/dash-table/syntax-tree/lexeme/logical.ts delete mode 100644 src/dash-table/syntax-tree/lexeme/not.ts delete mode 100644 src/dash-table/syntax-tree/lexeme/or.ts diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 9891d4d6e..e7d05e7ee 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -13,7 +13,7 @@ export enum LexemeType { export interface IUnboundedLexeme { evaluate?: (target: any, tree: ISyntaxTree) => boolean; - present?: string | ((tree: ISyntaxTree) => string); + present?: (tree: ISyntaxTree) => string; resolve?: (target: any, tree: ISyntaxTree) => any; type: string; nesting?: number; diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index b8ee4614a..a028f8cd2 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -6,8 +6,6 @@ import MultiColumnsSyntaxTree from './MultiColumnsSyntaxTree'; import QuerySyntaxTree from './QuerySyntaxTree'; import SingleColumnSyntaxTree from './SingleColumnSyntaxTree'; -const EXTRACT_COLUMN_REGEX = /^{|}$/g; - export const getMultiColumnQueryString = ( asts: SingleColumnSyntaxTree[] ) => R.map( @@ -29,10 +27,10 @@ export const getSingleColumnMap = (ast: MultiColumnsSyntaxTree) => { R.forEach(s => { if (s.lexeme.type === LexemeType.UnaryOperator && s.left) { - const sanitizedColumnId = s.left.value.replace(EXTRACT_COLUMN_REGEX, ''); + const sanitizedColumnId = s.left.lexeme.present ? s.left.lexeme.present(s.left) : s.left.value; map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, s.value)); } else if (s.lexeme.type === LexemeType.RelationalOperator && s.left && s.right) { - const sanitizedColumnId = s.left.value.replace(EXTRACT_COLUMN_REGEX, ''); + const sanitizedColumnId = s.left.lexeme.present ? s.left.lexeme.present(s.left) : s.left.value; map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, `${s.value} ${s.right.value}`)); } }, statements); diff --git a/src/dash-table/syntax-tree/lexeme/and.ts b/src/dash-table/syntax-tree/lexeme/and.ts deleted file mode 100644 index 5c724536d..000000000 --- a/src/dash-table/syntax-tree/lexeme/and.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Logger from 'core/Logger'; -import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; - -const and: IUnboundedLexeme = { - evaluate: (target, tree) => { - Logger.trace('evaluate -> &&', target, tree); - - const t = tree as any; - const lv = t.left.lexeme.evaluate(target, t.left); - const rv = t.right.lexeme.evaluate(target, t.right); - return lv && rv; - }, - type: LexemeType.LogicalOperator, - priority: 2, - regexp: /^(and\s|&&)/i, - present: '&&', - syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { - return Object.assign({ - left: lexs.slice(0, pivotIndex), - right: lexs.slice(pivotIndex + 1) - }, pivot); - } -}; - -export default and; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/block.ts b/src/dash-table/syntax-tree/lexeme/block.ts index 935167ff8..2131e22c4 100644 --- a/src/dash-table/syntax-tree/lexeme/block.ts +++ b/src/dash-table/syntax-tree/lexeme/block.ts @@ -17,7 +17,7 @@ export const blockOpen: IUnboundedLexeme = { }, type: LexemeType.BlockOpen, nesting: 1, - present: '()', + present: () => '()', priority: 1, regexp: /^\(/, syntaxer: (lexs: any[]) => { diff --git a/src/dash-table/syntax-tree/lexeme/logical.ts b/src/dash-table/syntax-tree/lexeme/logical.ts new file mode 100644 index 000000000..2477235ee --- /dev/null +++ b/src/dash-table/syntax-tree/lexeme/logical.ts @@ -0,0 +1,49 @@ +import Logger from 'core/Logger'; +import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; + +enum LogicalOperator { + And = '&&', + Or = '||' +} + +export const and: IUnboundedLexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> &&', target, tree); + + const t = tree as any; + const lv = t.left.lexeme.evaluate(target, t.left); + const rv = t.right.lexeme.evaluate(target, t.right); + return lv && rv; + }, + type: LexemeType.LogicalOperator, + priority: 2, + regexp: /^(and\s|&&)/i, + present: () => LogicalOperator.And, + syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { + return Object.assign({ + left: lexs.slice(0, pivotIndex), + right: lexs.slice(pivotIndex + 1) + }, pivot); + } +}; + +export const or: IUnboundedLexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> ||', target, tree); + + const t = tree as any; + + return t.left.lexeme.evaluate(target, t.left) || + t.right.lexeme.evaluate(target, t.right); + }, + type: LexemeType.LogicalOperator, + present: () => LogicalOperator.Or, + priority: 3, + regexp: /^(or\s|\|\|)/i, + syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { + return Object.assign({ + left: lexs.slice(0, pivotIndex), + right: lexs.slice(pivotIndex + 1) + }, pivot); + } +}; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/not.ts b/src/dash-table/syntax-tree/lexeme/not.ts deleted file mode 100644 index 796390455..000000000 --- a/src/dash-table/syntax-tree/lexeme/not.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Logger from 'core/Logger'; -import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; - -const unaryNot: IUnboundedLexeme = { - evaluate: (target, tree) => { - Logger.trace('evaluate -> unary not', target, tree); - - const t = tree as any; - - return !t.right.lexeme.evaluate(target, t.right); - }, - type: LexemeType.UnaryOperator, - present: '!', - priority: 1.5, - regexp: /^!/, - syntaxer: (lexs: any[]) => { - return Object.assign({ - right: lexs.slice(1, lexs.length) - }, lexs[0]); - } -}; - -export default unaryNot; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/or.ts b/src/dash-table/syntax-tree/lexeme/or.ts deleted file mode 100644 index 28ec037a1..000000000 --- a/src/dash-table/syntax-tree/lexeme/or.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Logger from 'core/Logger'; -import { IUnboundedLexeme, LexemeType } from 'core/syntax-tree/lexicon'; - -const or: IUnboundedLexeme = { - evaluate: (target, tree) => { - Logger.trace('evaluate -> ||', target, tree); - - const t = tree as any; - - return t.left.lexeme.evaluate(target, t.left) || - t.right.lexeme.evaluate(target, t.right); - }, - type: LexemeType.LogicalOperator, - present: '||', - priority: 3, - regexp: /^(or\s|\|\|)/i, - syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { - return Object.assign({ - left: lexs.slice(0, pivotIndex), - right: lexs.slice(pivotIndex + 1) - }, pivot); - } -}; - -export default or; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts index 435d7abdb..a3d10535c 100644 --- a/src/dash-table/syntax-tree/lexeme/relational.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -29,6 +29,15 @@ function relationalEvaluator( return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree)); } +enum RelationalOperator { + Equal = '=', + GreaterOrEqual = '>=', + GreatherThan = '>', + LessOrEqual = '<=', + LessThan = '<', + NotEqual = '!=' +} + const LEXEME_BASE = { priority: 0, syntaxer: relationalSyntaxer, @@ -37,36 +46,36 @@ const LEXEME_BASE = { export const equal: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op === exp), - present: '=', + present: () => RelationalOperator.Equal, regexp: /^(=|eq)/i }, LEXEME_BASE); export const greaterOrEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op >= exp), - present: '>=', + present: () => RelationalOperator.GreaterOrEqual, regexp: /^(>=|ge)/i }, LEXEME_BASE); export const greaterThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op > exp), - present: '>', + present: () => RelationalOperator.GreatherThan, regexp: /^(>|gt)/i }, LEXEME_BASE); export const lessOrEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op <= exp), - present: '<=', + present: () => RelationalOperator.LessOrEqual, regexp: /^(<=|le)/i }, LEXEME_BASE); export const lessThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op < exp), - present: '<', + present: () => RelationalOperator.LessThan, regexp: /^(<|lt)/i }, LEXEME_BASE); export const notEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op !== exp), - present: '!=', + present: () => RelationalOperator.NotEqual, regexp: /^(!=|ne)/i }, LEXEME_BASE); \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unary.ts b/src/dash-table/syntax-tree/lexeme/unary.ts index a20c5c2f7..643d70075 100644 --- a/src/dash-table/syntax-tree/lexeme/unary.ts +++ b/src/dash-table/syntax-tree/lexeme/unary.ts @@ -35,6 +35,10 @@ function relationalEvaluator( return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree)); } +enum UnaryOperator { + Not = '!' +} + const LEXEME_BASE = { present: (tree: ISyntaxTree) => tree.value, priority: 0, @@ -42,6 +46,25 @@ const LEXEME_BASE = { type: LexemeType.UnaryOperator }; +export const not: IUnboundedLexeme = { + evaluate: (target, tree) => { + Logger.trace('evaluate -> unary not', target, tree); + + const t = tree as any; + + return !t.right.lexeme.evaluate(target, t.right); + }, + type: LexemeType.UnaryOperator, + present: () => UnaryOperator.Not, + priority: 1.5, + regexp: /^!/, + syntaxer: (lexs: any[]) => { + return Object.assign({ + right: lexs.slice(1, lexs.length) + }, lexs[0]); + } +}; + export const isBool: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(opValue => typeof opValue === 'boolean'), regexp: /^(is bool)/i diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index c7bcfc182..dcc4dd035 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -1,8 +1,10 @@ import * as R from 'ramda'; -import and from '../lexeme/and'; import expression from '../lexeme/expression'; import operand from '../lexeme/operand'; +import { + and +} from '../lexeme/logical'; import { equal, greaterOrEqual, diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index b82e7567e..93ade6caa 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -1,14 +1,15 @@ import * as R from 'ramda'; -import and from '../lexeme/and'; import expression from '../lexeme/expression'; import operand from '../lexeme/operand'; -import or from '../lexeme/or'; -import unaryNot from '../lexeme/not'; import { blockClose, blockOpen } from '../lexeme/block'; +import { + and, + or +} from '../lexeme/logical'; import { equal, greaterOrEqual, @@ -25,7 +26,8 @@ import { isObject, isOdd, isPrime, - isStr + isStr, + not } from '../lexeme/unary'; import { ILexemeResult } from 'core/syntax-tree/lexer'; @@ -135,7 +137,7 @@ const lexicon: ILexeme[] = [ terminal: isTerminal })), { - ...unaryNot, + ...not, if: (_: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( previous.lexeme.type, From c4d784973769eb38dc94de3a11a16e74274ec2ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Mar 2019 16:31:29 -0400 Subject: [PATCH 25/54] rework expression lexeme formatting --- src/dash-table/syntax-tree/index.ts | 8 +++- .../syntax-tree/lexeme/expression.ts | 37 +++++++++--------- .../syntax-tree/lexeme/relational.ts | 2 +- .../tests/standalone/filtering_test.ts | 38 ++++++++++++------- .../tests/unit/query_syntactic_tree_test.ts | 34 ++++++++--------- .../unit/single_column_syntactic_tree_test.ts | 10 ++--- tests/dash/app_dataframe_updating_graph_fe.py | 2 +- tests/dash/app_styling.py | 4 +- .../percy-storybook/Width.empty.percy.tsx | 6 +-- 9 files changed, 78 insertions(+), 63 deletions(-) diff --git a/src/dash-table/syntax-tree/index.ts b/src/dash-table/syntax-tree/index.ts index a028f8cd2..a651fbef0 100644 --- a/src/dash-table/syntax-tree/index.ts +++ b/src/dash-table/syntax-tree/index.ts @@ -5,6 +5,7 @@ import { LexemeType } from 'core/syntax-tree/lexicon'; import MultiColumnsSyntaxTree from './MultiColumnsSyntaxTree'; import QuerySyntaxTree from './QuerySyntaxTree'; import SingleColumnSyntaxTree from './SingleColumnSyntaxTree'; +import { RelationalOperator } from './lexeme/relational'; export const getMultiColumnQueryString = ( asts: SingleColumnSyntaxTree[] @@ -31,7 +32,12 @@ export const getSingleColumnMap = (ast: MultiColumnsSyntaxTree) => { map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, s.value)); } else if (s.lexeme.type === LexemeType.RelationalOperator && s.left && s.right) { const sanitizedColumnId = s.left.lexeme.present ? s.left.lexeme.present(s.left) : s.left.value; - map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, `${s.value} ${s.right.value}`)); + + if (s.lexeme.present && s.lexeme.present(s) === RelationalOperator.Equal) { + map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, `${s.right.value}`)); + } else { + map.set(sanitizedColumnId, new SingleColumnSyntaxTree(sanitizedColumnId, `${s.value} ${s.right.value}`)); + } } }, statements); diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index 7fc49fd41..d32c4155a 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -1,31 +1,30 @@ +import isNumeric from 'fast-isnumeric'; + import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; +const REGEX = /^(({(([^{}\\]|\\{|\\}|\\)+)})|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|([^\s'"]+))/; + +const FIELD_REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; +const STRING_REGEX = /^(('([^'\\]|\\')+')|("([^"\\]|\\")+"))/; +const DIGITS_REGEX = /^[^\s'"]+/; + const expression: IUnboundedLexeme = { present: (tree: ISyntaxTree) => tree.value, resolve: (target: any, tree: ISyntaxTree) => { - if (/^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))$/.test(tree.value)) { - return tree.value.slice(1, tree.value.length - 1); - } else if (/^(num|str)\(.*\)$/.test(tree.value)) { - const res = tree.value.match(/^(\w+)\((.*)\)$/); - if (res) { - const [, op, value] = res; - - switch (op) { - case 'num': - return parseFloat(value); - case 'str': - default: - return value; - } - } else { - throw Error(); - } + if (FIELD_REGEX.test(tree.value)) { + return target[tree.value.slice(1, tree.value.length - 1)]; + } else if (STRING_REGEX.test(tree.value)) { + return tree.value.slice(1, tree.value.length - 1).replace(/\\('|")/g, '$1'); + } else if (DIGITS_REGEX.test(tree.value)) { + return isNumeric(tree.value) ? + +tree.value : + tree.value; } else { - return target[tree.value]; + throw new Error(); } }, - regexp: /^(((num|str)\([^()]*\))|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`)|(\w|[:.\-+])+)/, + regexp: REGEX, type: LexemeType.Expression }; diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts index a3d10535c..6dce950dd 100644 --- a/src/dash-table/syntax-tree/lexeme/relational.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -29,7 +29,7 @@ function relationalEvaluator( return (target: any, tree: ISyntaxTree) => fn(evaluator(target, tree)); } -enum RelationalOperator { +export enum RelationalOperator { Equal = '=', GreaterOrEqual = '>=', GreatherThan = '>', diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index e0851b8db..dbe6b2ddd 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -11,26 +11,33 @@ describe(`filter special characters`, () => { }); it('can filter on special column id', () => { + DashTable.getFilterById('b+bb').click(); + DOM.focused.type(`Wet${Key.Enter}`); + DashTable.getFilterById('c cc').click(); - DOM.focused.type(`gt num(90)${Key.Enter}`); + DOM.focused.type(`gt 90${Key.Enter}`); DashTable.getFilterById('d:dd').click(); - DOM.focused.type(`lt num(12500)${Key.Enter}`); + DOM.focused.type(`lt 12500${Key.Enter}`); DashTable.getFilterById('e-ee').click(); DOM.focused.type(`is prime${Key.Enter}`); DashTable.getFilterById('f_ff').click(); - DOM.focused.type(`le num(106)${Key.Enter}`); + DOM.focused.type(`le 106${Key.Enter}`); DashTable.getFilterById('g.gg').click(); - DOM.focused.type(`gt num(1000)${Key.Enter}`); - + DOM.focused.type(`gt 1000${Key.Enter}`); DashTable.getFilterById('b+bb').click(); - DOM.focused.type(`eq "Wet"${Key.Enter}`); DashTable.getCellById(0, 'rows').within(() => cy.get('.dash-cell-value').should('have.html', '101')); DashTable.getCellById(1, 'rows').should('not.exist'); + DashTable.getFilterById('b+bb').within(() => cy.get('input').should('have.value', 'Wet')); + DashTable.getFilterById('c cc').within(() => cy.get('input').should('have.value', 'gt 90')); + DashTable.getFilterById('d:dd').within(() => cy.get('input').should('have.value', 'lt 12500')); + DashTable.getFilterById('e-ee').within(() => cy.get('input').should('have.value', 'is prime')); + DashTable.getFilterById('f_ff').within(() => cy.get('input').should('have.value', 'le 106')); + DashTable.getFilterById('g.gg').within(() => cy.get('input').should('have.value', 'gt 1000')); }); }); @@ -55,19 +62,19 @@ describe('filter', () => { DashTable.getFilterById('ccc').click(); DOM.focused.type(`gt`); DashTable.getFilterById('ddd').click(); - DOM.focused.type('numpy(20000)'); + DOM.focused.type('20 a000'); DashTable.getFilterById('eee').click(); DOM.focused.type('is prime2'); DashTable.getFilterById('bbb').click(); - DOM.focused.type('!'); + DOM.focused.type('! !'); DashTable.getFilterById('ccc').click(); DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_0)); DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_1)); - DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '!')); + DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', '! !')); DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt')); - DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'numpy(20000)')); + DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', '20 a000')); DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', 'is prime2')); DashTable.getFilterById('bbb').should('have.class', 'invalid'); @@ -90,18 +97,23 @@ describe('filter', () => { .then($el => cell_1 = $el[0].innerHTML)); DashTable.getFilterById('ccc').click(); - DOM.focused.type(`gt num(100)`); + DOM.focused.type(`gt 100`); DashTable.getFilterById('ddd').click(); - DOM.focused.type('lt num(20000)'); + DOM.focused.type('lt 20000'); DashTable.getFilterById('eee').click(); DOM.focused.type('is prime'); DashTable.getFilterById('bbb').click(); - DOM.focused.type(`eq "Wet"`); + DOM.focused.type(`Wet`); DashTable.getFilterById('ccc').click(); DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '101')); DashTable.getCellById(1, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', '109')); + DashTable.getFilterById('bbb').within(() => cy.get('input').should('have.value', 'Wet')); + DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt 100')); + DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'lt 20000')); + DashTable.getFilterById('eee').within(() => cy.get('input').should('have.value', 'is prime')); + cy.get('.clear-filters').click(); DashTable.getCellById(0, 'ccc').within(() => cy.get('.dash-cell-value').should('have.html', cell_0)); diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index d88e29dac..4342642c1 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -8,15 +8,15 @@ describe('Query Syntax Tree', () => { describe('operands', () => { it('does not support badly formed operands', () => { - expect(new QuerySyntaxTree(`{myField} eq num(0)`).isValid).to.equal(true); - expect(new QuerySyntaxTree(`{'myField'} eq num(0)`).isValid).to.equal(true); - expect(new QuerySyntaxTree(`{"myField"} eq num(0)`).isValid).to.equal(true); - expect(new QuerySyntaxTree('{`myField`} eq num(0)').isValid).to.equal(true); - expect(new QuerySyntaxTree('{\\{myField\\}} eq num(0)').isValid).to.equal(true); - expect(new QuerySyntaxTree('{{myField}} eq num(0)').isValid).to.equal(false); - expect(new QuerySyntaxTree(`{myField eq num(0)`).isValid).to.equal(false); - expect(new QuerySyntaxTree(`myField} eq num(0)`).isValid).to.equal(false); - expect(new QuerySyntaxTree(`myField eq num(0)`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq 0`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`{'myField'} eq 0`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`{"myField"} eq 0`).isValid).to.equal(true); + expect(new QuerySyntaxTree('{`myField`} eq 0').isValid).to.equal(true); + expect(new QuerySyntaxTree('{\\{myField\\}} eq 0').isValid).to.equal(true); + expect(new QuerySyntaxTree('{{myField}} eq 0').isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField eq 0`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`myField} eq 0`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`myField eq 0`).isValid).to.equal(false); }); it('does not support badly formed expression', () => { @@ -25,10 +25,8 @@ describe('Query Syntax Tree', () => { expect(new QuerySyntaxTree('{myField} eq `value`').isValid).to.equal(true); expect(new QuerySyntaxTree(`{myField} eq 'value\\'`).isValid).to.equal(false); expect(new QuerySyntaxTree(`{myField} eq "value\\"`).isValid).to.equal(false); - expect(new QuerySyntaxTree('{myField} eq `value\\`').isValid).to.equal(false); expect(new QuerySyntaxTree(`{myField} eq \\'value'`).isValid).to.equal(false); expect(new QuerySyntaxTree(`{myField} eq \\"value"`).isValid).to.equal(false); - expect(new QuerySyntaxTree('{myField} eq \\`value`').isValid).to.equal(false); }); it('support arbitrary quoted column name', () => { @@ -112,7 +110,7 @@ describe('Query Syntax Tree', () => { }); it('support column name with "\\"', () => { - const tree = new QuerySyntaxTree('{\\\\{} eq num(1) || {\\\\{} eq num(2)'); + const tree = new QuerySyntaxTree('{\\\\{} eq 1 || {\\\\{} eq 2'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -122,7 +120,7 @@ describe('Query Syntax Tree', () => { }); it('support nesting in quotes', () => { - const tree = new QuerySyntaxTree(`{'""'} eq \`1'"dot\` || {'""'} eq \`2'"dot\``); + const tree = new QuerySyntaxTree(`{'""'} eq '1\\'"dot' || {'""'} eq '2\\'"dot'`); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -196,7 +194,7 @@ describe('Query Syntax Tree', () => { describe('data types', () => { it('can compare numbers', () => { - const tree = new QuerySyntaxTree('{c} eq num(1)'); + const tree = new QuerySyntaxTree('{c} eq 1'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -206,7 +204,7 @@ describe('Query Syntax Tree', () => { }); it('can compare floats', () => { - const tree = new QuerySyntaxTree('{field} ge num(1.5)'); + const tree = new QuerySyntaxTree('{field} ge 1.5'); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ field: -1.501 })).to.equal(false); @@ -218,7 +216,7 @@ describe('Query Syntax Tree', () => { }); it('can compare string to number and return false', () => { - const tree = new QuerySyntaxTree('{a} eq num(1)'); + const tree = new QuerySyntaxTree('{a} eq 1'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -228,7 +226,7 @@ describe('Query Syntax Tree', () => { }); it('can compare strings', () => { - const tree = new QuerySyntaxTree('{a} eq str(1)'); + const tree = new QuerySyntaxTree('{a} eq "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); @@ -238,7 +236,7 @@ describe('Query Syntax Tree', () => { }); it('can compare string to number and return false', () => { - const tree = new QuerySyntaxTree('{c} eq str(1)'); + const tree = new QuerySyntaxTree('{c} eq "1"'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); diff --git a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts index 3413dc046..dacdcc140 100644 --- a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts @@ -2,7 +2,7 @@ import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; describe('Single Column Syntax Tree', () => { it('cannot have operand', () => { - const tree = new SingleColumnSyntaxTree('a', '{a} <= num(1)'); + const tree = new SingleColumnSyntaxTree('a', '{a} <= 1'); expect(tree.isValid).to.equal(false); }); @@ -27,13 +27,13 @@ describe('Single Column Syntax Tree', () => { }); it('can be binary + expression', () => { - const tree = new SingleColumnSyntaxTree('a', '<= num(1)'); + const tree = new SingleColumnSyntaxTree('a', '<= 1'); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 0 })).to.equal(true); expect(tree.evaluate({ a: 2 })).to.equal(false); - expect(tree.toQueryString()).to.equal('{a} <= num(1)'); + expect(tree.toQueryString()).to.equal('{a} <= 1'); }); it('can be unary', () => { @@ -47,12 +47,12 @@ describe('Single Column Syntax Tree', () => { }); it('can be expression', () => { - const tree = new SingleColumnSyntaxTree('a', 'num(1)'); + const tree = new SingleColumnSyntaxTree('a', '1'); expect(tree.isValid).to.equal(true); expect(tree.evaluate({ a: 1 })).to.equal(true); expect(tree.evaluate({ a: 2 })).to.equal(false); - expect(tree.toQueryString()).to.equal('{a} eq num(1)'); + expect(tree.toQueryString()).to.equal('{a} eq 1'); }); }); \ No newline at end of file diff --git a/tests/dash/app_dataframe_updating_graph_fe.py b/tests/dash/app_dataframe_updating_graph_fe.py index d2188bd19..beb8de26c 100644 --- a/tests/dash/app_dataframe_updating_graph_fe.py +++ b/tests/dash/app_dataframe_updating_graph_fe.py @@ -53,7 +53,7 @@ def layout(): > A quick note on filtering. We have defined our own > syntax for performing filtering operations. Here are some > examples for this particular dataset: - > - `lt num(50)` in the `lifeExp` column + > - `lt 50` in the `lifeExp` column > - `eq "Canada"` in the `country` column By default, these transformations are done clientside. diff --git a/tests/dash/app_styling.py b/tests/dash/app_styling.py index 9f9226817..a663ff3cd 100644 --- a/tests/dash/app_styling.py +++ b/tests/dash/app_styling.py @@ -443,10 +443,10 @@ def layout(): "width": "100%" }, style_data_conditional=[{ - "if": { "column_id": "Region", "filter": "Region eq str(Montreal)" }, + "if": { "column_id": "Region", "filter": "Region eq Montreal" }, "background_color": "yellow" }, { - "if": { "column_id": "Humidity", "filter": "Humidity eq num(20)" }, + "if": { "column_id": "Humidity", "filter": "Humidity eq 20" }, "background_color": "yellow" }] ) diff --git a/tests/visual/percy-storybook/Width.empty.percy.tsx b/tests/visual/percy-storybook/Width.empty.percy.tsx index 4c5659599..f99897846 100644 --- a/tests/visual/percy-storybook/Width.empty.percy.tsx +++ b/tests/visual/percy-storybook/Width.empty.percy.tsx @@ -46,16 +46,16 @@ storiesOf('DashTable/Empty', module) />)) .add('with column filters -- single query', () => ()) .add('with column filters -- multi query', () => ()) .add('with column filters -- multi query, no data', () => ()); \ No newline at end of file From 6e58593c401e4fdbbf3bf0bcfb1d5ece5d10742b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Mar 2019 19:32:38 -0400 Subject: [PATCH 26/54] rework regexp for lexemes, reuse between expression and operand, update query tests, add expression and operand tests --- src/core/syntax-tree/lexer.ts | 22 ++++++- src/core/syntax-tree/lexicon.ts | 2 +- .../syntax-tree/lexeme/expression.ts | 18 +++--- src/dash-table/syntax-tree/lexeme/operand.ts | 12 ++-- tests/cypress/tests/unit/lexeme_test.ts | 57 +++++++++++++++++++ .../tests/unit/query_syntactic_tree_test.ts | 14 +++-- 6 files changed, 99 insertions(+), 26 deletions(-) create mode 100644 tests/cypress/tests/unit/lexeme_test.ts diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts index 7e5db437f..60c429e4f 100644 --- a/src/core/syntax-tree/lexer.ts +++ b/src/core/syntax-tree/lexer.ts @@ -11,6 +11,22 @@ export interface ILexemeResult { value?: string; } +function findMatch(lexemes: ILexeme[], query: string): [ILexeme?, RegExp?] { + for (let lexeme of lexemes) { + if (Array.isArray(lexeme.regexp)) { + for (let regexp of lexeme.regexp) { + if (regexp.test(query)) { + return [lexeme, regexp]; + } + } + } else if (lexeme.regexp.test(query)) { + return [lexeme, lexeme.regexp]; + } + } + + return [undefined, undefined]; +} + export default function lexer(lexicon: Lexicon, query: string): ILexerResult { let result: ILexemeResult[] = []; @@ -29,12 +45,12 @@ export default function lexer(lexicon: Lexicon, query: string): ILexerResult { lexeme.if && lexeme.if.indexOf(undefined) !== -1)) ); - const next = lexemes.find(_lexeme => _lexeme.regexp.test(query)) || null; - if (!next) { + const [next, regexp] = findMatch(lexemes, query); + if (!next || !regexp) { return { lexemes: result, valid: false, error: query }; } - const value = (query.match(next.regexp) || [])[0]; + const value = (query.match(regexp) || [])[0]; result.push({ lexeme: next, value }); query = query.substring(value.length); diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index e7d05e7ee..30677c8b9 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -18,7 +18,7 @@ export interface IUnboundedLexeme { type: string; nesting?: number; priority?: number; - regexp: RegExp; + regexp: RegExp | RegExp[]; syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any; } diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index d32c4155a..d869ad26f 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -2,29 +2,27 @@ import isNumeric from 'fast-isnumeric'; import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; +import operand, { FIELD_REGEX } from './operand'; -const REGEX = /^(({(([^{}\\]|\\{|\\}|\\)+)})|('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|([^\s'"]+))/; - -const FIELD_REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; -const STRING_REGEX = /^(('([^'\\]|\\')+')|("([^"\\]|\\")+"))/; -const DIGITS_REGEX = /^[^\s'"]+/; +const STRING_REGEX = /^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))/; +const VALUE_REGEX = /^([^\s'"`{}\\]|\\\s|\\'|\\"|\\`|\\{|\\}|\\)+/; const expression: IUnboundedLexeme = { present: (tree: ISyntaxTree) => tree.value, resolve: (target: any, tree: ISyntaxTree) => { if (FIELD_REGEX.test(tree.value)) { - return target[tree.value.slice(1, tree.value.length - 1)]; + return operand.resolve && operand.resolve(target, tree); } else if (STRING_REGEX.test(tree.value)) { - return tree.value.slice(1, tree.value.length - 1).replace(/\\('|")/g, '$1'); - } else if (DIGITS_REGEX.test(tree.value)) { + return tree.value.slice(1, tree.value.length - 1).replace(/\\('|"|`)/g, '$1'); + } else if (VALUE_REGEX.test(tree.value)) { return isNumeric(tree.value) ? +tree.value : - tree.value; + tree.value.replace(/\\(\s|'|"|`|{|})/g, '$1'); } else { throw new Error(); } }, - regexp: REGEX, + regexp: [FIELD_REGEX, STRING_REGEX, VALUE_REGEX], type: LexemeType.Expression }; diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts index 53a7dad20..d5c9d52ec 100644 --- a/src/dash-table/syntax-tree/lexeme/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -1,24 +1,24 @@ import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -const REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; +export const FIELD_REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; -const getValue = ( +const getField = ( value: string ) => value .slice(1, value.length - 1) .replace(/\\([{}])/g, '$1'); const operand: IUnboundedLexeme = { - present: (tree: ISyntaxTree) => getValue(tree.value), + present: (tree: ISyntaxTree) => getField(tree.value), resolve: (target: any, tree: ISyntaxTree) => { - if (REGEX.test(tree.value)) { - return target[getValue(tree.value)]; + if (FIELD_REGEX.test(tree.value)) { + return target[getField(tree.value)]; } else { throw new Error(); } }, - regexp: REGEX, + regexp: FIELD_REGEX, type: LexemeType.Operand }; diff --git a/tests/cypress/tests/unit/lexeme_test.ts b/tests/cypress/tests/unit/lexeme_test.ts new file mode 100644 index 000000000..9ed818b0b --- /dev/null +++ b/tests/cypress/tests/unit/lexeme_test.ts @@ -0,0 +1,57 @@ +import expression from 'dash-table/syntax-tree/lexeme/expression'; +import operand from 'dash-table/syntax-tree/lexeme/operand'; +import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; + +describe('expression', () => { + it('resolves values', () => { + expect(!!expression.resolve).to.equal(true); + expect(typeof expression.resolve).to.equal('function'); + + if (expression.resolve) { + expect(expression.resolve(undefined, { value: 'abc' } as ISyntaxTree)).to.equal('abc'); + expect(expression.resolve(undefined, { value: 'a\\ bc' } as ISyntaxTree)).to.equal('a bc'); + expect(expression.resolve(undefined, { value: '\\' } as ISyntaxTree)).to.equal('\\'); + expect(expression.resolve(undefined, { value: 'abc\\' } as ISyntaxTree)).to.equal('abc\\'); + expect(expression.resolve(undefined, { value: '\'abc\'' } as ISyntaxTree)).to.equal('abc'); + expect(expression.resolve(undefined, { value: '"abc"' } as ISyntaxTree)).to.equal('abc'); + expect(expression.resolve(undefined, { value: '`abc`' } as ISyntaxTree)).to.equal('abc'); + expect(expression.resolve(undefined, { value: '123' } as ISyntaxTree)).to.equal(123); + expect(expression.resolve(undefined, { value: '123.45' } as ISyntaxTree)).to.equal(123.45); + expect(expression.resolve(undefined, { value: '1E6' } as ISyntaxTree)).to.equal(1000000); + expect(expression.resolve(undefined, { value: '0x100' } as ISyntaxTree)).to.equal(256); + + expect(expression.resolve(undefined, { value: '"\\""' } as ISyntaxTree)).to.equal('"'); + expect(expression.resolve(undefined, { value: `'\\''` } as ISyntaxTree)).to.equal(`'`); + expect(expression.resolve(undefined, { value: '`\\``' } as ISyntaxTree)).to.equal('`'); + + expect(expression.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3); + expect(expression.resolve({ ['"abc"']: 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3); + + expect(expression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); + expect(expression.resolve.bind(undefined, undefined, { value: '`' } as ISyntaxTree)).to.throw(Error); + expect(expression.resolve.bind(undefined, undefined, { value: `'` } as ISyntaxTree)).to.throw(Error); + expect(expression.resolve.bind(undefined, undefined, { value: '{' } as ISyntaxTree)).to.throw(Error); + expect(expression.resolve.bind(undefined, undefined, { value: '}' } as ISyntaxTree)).to.throw(Error); + } + }); +}); + +describe('operand', () => { + it('resolves values', () => { + expect(!!operand.resolve).to.equal(true); + expect(typeof operand.resolve).to.equal('function'); + + if (operand.resolve) { + expect(operand.resolve.bind(undefined, undefined, { value: 'abc' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, undefined, { value: '123' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, undefined, { value: '{abc' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, undefined, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, undefined, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); + + expect(operand.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3); + expect(operand.resolve({ ['a bc']: 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3); + expect(operand.resolve({ ['{abc}']: 3 }, { value: '{\\{abc\\}}' } as ISyntaxTree)).to.equal(3); + expect(operand.resolve({ ['"abc"']: 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3); + } + }); +}); \ No newline at end of file diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index 4342642c1..60fe7b552 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -1,10 +1,10 @@ import { QuerySyntaxTree } from 'dash-table/syntax-tree'; describe('Query Syntax Tree', () => { - const data0 = { a: '0', b: '0', c: 0, d: null, '\\\{': 0, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '{a:dot}': '0*dot*', '\'""\'': '0\'"dot' }; - const data1 = { a: '1', b: '0', c: 1, d: 0, '\\\{': 1, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot', '{a:dot}': '1*dot*', '\'""\'': '1\'"dot' }; - const data2 = { a: '2', b: '1', c: 2, d: '', '\\\{': 2, 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '{a:dot}': '2*dot*', '\'""\'': '2\'"dot' }; - const data3 = { a: '3', b: '1', c: 3, d: false, '\\\{': 3, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '{a:dot}': '3*dot*', '\'""\'': '3\'"dot' }; + const data0 = { a: '0', b: '0', c: 0, d: null, '\\{': 0, 'a.dot': '0.dot', 'a-dot': '0-dot', a_dot: '0_dot', 'a+dot': '0+dot', 'a dot': '0 dot', 'a:dot': '0:dot', '_-6.:+** *@$': '0*dot', '{a:dot}': '0*dot*', '\'""\'': '0\'"dot' }; + const data1 = { a: '1', b: '0', c: 1, d: 0, '\\{': 1, 'a.dot': '1.dot', 'a-dot': '1-dot', a_dot: '1_dot', 'a+dot': '1+dot', 'a dot': '1 dot', 'a:dot': '1:dot', '_-6.:+** *@$': '1*dot', '{a:dot}': '1*dot*', '\'""\'': '1\'"dot' }; + const data2 = { a: '2', b: '1', c: 2, d: '', '\\{': 2, 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '{a:dot}': '2*dot*', '\'""\'': '2\'"dot' }; + const data3 = { a: '3', b: '1', c: 3, d: false, '\\{': 3, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '{a:dot}': '3*dot*', '\'""\'': '3\'"dot' }; describe('operands', () => { it('does not support badly formed operands', () => { @@ -23,8 +23,10 @@ describe('Query Syntax Tree', () => { expect(new QuerySyntaxTree(`{myField} eq 'value'`).isValid).to.equal(true); expect(new QuerySyntaxTree(`{myField} eq "value"`).isValid).to.equal(true); expect(new QuerySyntaxTree('{myField} eq `value`').isValid).to.equal(true); - expect(new QuerySyntaxTree(`{myField} eq 'value\\'`).isValid).to.equal(false); - expect(new QuerySyntaxTree(`{myField} eq "value\\"`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq 'value\\'`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`{myField} eq 'value\\''`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`{myField} eq "value\\"`).isValid).to.equal(true); + expect(new QuerySyntaxTree('{myField} eq `value\\`').isValid).to.equal(true); expect(new QuerySyntaxTree(`{myField} eq \\'value'`).isValid).to.equal(false); expect(new QuerySyntaxTree(`{myField} eq \\"value"`).isValid).to.equal(false); }); From 747b9e6d5e6f0243f4c5d872a3afd4638e49e664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Mar 2019 19:35:11 -0400 Subject: [PATCH 27/54] same expression and operand tests for operand section --- tests/cypress/tests/unit/lexeme_test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/cypress/tests/unit/lexeme_test.ts b/tests/cypress/tests/unit/lexeme_test.ts index 9ed818b0b..198ecc96a 100644 --- a/tests/cypress/tests/unit/lexeme_test.ts +++ b/tests/cypress/tests/unit/lexeme_test.ts @@ -25,6 +25,8 @@ describe('expression', () => { expect(expression.resolve(undefined, { value: '`\\``' } as ISyntaxTree)).to.equal('`'); expect(expression.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3); + expect(expression.resolve({ ['a bc']: 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3); + expect(expression.resolve({ ['{abc}']: 3 }, { value: '{\\{abc\\}}' } as ISyntaxTree)).to.equal(3); expect(expression.resolve({ ['"abc"']: 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3); expect(expression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); From d85091acc42212254a0598bb12c226962a8cf43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Mar 2019 21:03:01 -0400 Subject: [PATCH 28/54] - fix expression value - add expression value / block test --- src/dash-table/syntax-tree/lexeme/expression.ts | 4 ++-- tests/cypress/tests/unit/query_syntactic_tree_test.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index d869ad26f..8fef12fac 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -5,7 +5,7 @@ import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; import operand, { FIELD_REGEX } from './operand'; const STRING_REGEX = /^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))/; -const VALUE_REGEX = /^([^\s'"`{}\\]|\\\s|\\'|\\"|\\`|\\{|\\}|\\)+/; +const VALUE_REGEX = /^([^\s'"`{}()\\]|\\[\s'"`{}()]?)+/; const expression: IUnboundedLexeme = { present: (tree: ISyntaxTree) => tree.value, @@ -17,7 +17,7 @@ const expression: IUnboundedLexeme = { } else if (VALUE_REGEX.test(tree.value)) { return isNumeric(tree.value) ? +tree.value : - tree.value.replace(/\\(\s|'|"|`|{|})/g, '$1'); + tree.value.replace(/\\([\s'"`{}()])/g, '$1'); } else { throw new Error(); } diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index 60fe7b552..c3377a917 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -288,6 +288,16 @@ describe('Query Syntax Tree', () => { expect(tree.evaluate(data2)).to.equal(false); expect(tree.evaluate(data3)).to.equal(false); }); + + it('behave correctly with value expressions', () => { + const tree = new QuerySyntaxTree('{a} eq 1 and ({a} eq 0 or ({b} eq 1 or {b} eq 0))'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 0, b: 0 })).to.equal(false); + expect(tree.evaluate({ a: 0, b: 1 })).to.equal(false); + expect(tree.evaluate({ a: 1, b: 0 })).to.equal(true); + expect(tree.evaluate({ a: 1, b: 1 })).to.equal(true); + }); }); describe('unary operators', () => { From d82b2e39ab37e53725b3c288952ea7eaa470f1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Mar 2019 21:08:39 -0400 Subject: [PATCH 29/54] - second filter for `filtering` app case --- demo/App.js | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/demo/App.js b/demo/App.js index ce51c534d..ae4d5e89d 100644 --- a/demo/App.js +++ b/demo/App.js @@ -14,6 +14,7 @@ class App extends Component { super(); this.state = AppState; + this.state.temp_filtering = '' const setProps = memoizeOne(() => { return newProps => { @@ -33,15 +34,30 @@ class App extends Component { const mode = Environment.searchParams.get('mode'); if (mode === AppMode.Filtering) { - return (); + return ( +
+ + this.setState({ temp_filtering: e.target.value }) + } + onBlur={e => { + const tableProps = R.clone(this.state.tableProps); + tableProps.filtering_settings = e.target.value; + + this.setState({ tableProps }); + }} /> +
); } } From 785373d19d3f10e42d63b3b894bf76f8006d2525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Mar 2019 21:19:20 -0400 Subject: [PATCH 30/54] fix lint --- demo/App.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/App.js b/demo/App.js index ae4d5e89d..b34975da3 100644 --- a/demo/App.js +++ b/demo/App.js @@ -14,7 +14,7 @@ class App extends Component { super(); this.state = AppState; - this.state.temp_filtering = '' + this.state.temp_filtering = ''; const setProps = memoizeOne(() => { return newProps => { From 1689b8669c24470ce1001cdf51821f440a788b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 29 Mar 2019 14:55:54 -0400 Subject: [PATCH 31/54] - break up expression into sub types - improve tests / fix issues found --- src/core/syntax-tree/index.ts | 42 ++++++- src/core/syntax-tree/lexer.ts | 24 +--- src/core/syntax-tree/lexicon.ts | 6 +- src/dash-table/dash/DataTable.js | 5 + .../syntax-tree/lexeme/expression.ts | 62 +++++++--- src/dash-table/syntax-tree/lexeme/operand.ts | 2 +- src/dash-table/syntax-tree/lexicon/column.ts | 16 ++- .../syntax-tree/lexicon/columnMulti.ts | 18 ++- src/dash-table/syntax-tree/lexicon/query.ts | 18 ++- tests/cypress/tests/unit/lexeme_test.ts | 116 +++++++++++++----- 10 files changed, 224 insertions(+), 85 deletions(-) diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index e3357d5fc..d9849d1a4 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -2,9 +2,43 @@ import * as R from 'ramda'; import Logger from 'core/Logger'; import lexer, { ILexerResult } from 'core/syntax-tree/lexer'; -import syntaxer, { ISyntaxerResult } from 'core/syntax-tree/syntaxer'; +import syntaxer, { ISyntaxerResult, ISyntaxTree } from 'core/syntax-tree/syntaxer'; import { Lexicon } from './lexicon'; +interface IStructure { + subType?: string; + type: string; + value: any; + + block?: IStructure; + left?: IStructure; + right?: IStructure; +} + +function toStructure(tree: ISyntaxTree): IStructure { + const { block, left, lexeme, right, value } = tree; + + const res: IStructure = { + subType: lexeme.subType, + type: lexeme.type, + value: lexeme.present ? lexeme.present(tree) : value + }; + + if (block) { + res.block = toStructure(block); + } + + if (left) { + res.left = toStructure(left); + } + + if (right) { + res.right = toStructure(right); + } + + return res; +} + export default class SyntaxTree { protected lexerResult: ILexerResult; protected syntaxerResult: ISyntaxerResult; @@ -48,4 +82,10 @@ export default class SyntaxTree { R.map(l => l.value, this.lexerResult.lexemes).join(' ') : ''; } + + toStructure() { + if (this.syntaxerResult.tree) { + return toStructure(this.syntaxerResult.tree); + } + } } \ No newline at end of file diff --git a/src/core/syntax-tree/lexer.ts b/src/core/syntax-tree/lexer.ts index 60c429e4f..c0fa303f5 100644 --- a/src/core/syntax-tree/lexer.ts +++ b/src/core/syntax-tree/lexer.ts @@ -1,3 +1,5 @@ +import * as R from 'ramda'; + import { ILexeme, Lexicon } from 'core/syntax-tree/lexicon'; export interface ILexerResult { @@ -11,22 +13,6 @@ export interface ILexemeResult { value?: string; } -function findMatch(lexemes: ILexeme[], query: string): [ILexeme?, RegExp?] { - for (let lexeme of lexemes) { - if (Array.isArray(lexeme.regexp)) { - for (let regexp of lexeme.regexp) { - if (regexp.test(query)) { - return [lexeme, regexp]; - } - } - } else if (lexeme.regexp.test(query)) { - return [lexeme, lexeme.regexp]; - } - } - - return [undefined, undefined]; -} - export default function lexer(lexicon: Lexicon, query: string): ILexerResult { let result: ILexemeResult[] = []; @@ -45,12 +31,12 @@ export default function lexer(lexicon: Lexicon, query: string): ILexerResult { lexeme.if && lexeme.if.indexOf(undefined) !== -1)) ); - const [next, regexp] = findMatch(lexemes, query); - if (!next || !regexp) { + const next = R.find(lexeme => lexeme.regexp.test(query), lexemes); + if (!next) { return { lexemes: result, valid: false, error: query }; } - const value = (query.match(regexp) || [])[0]; + const value = (query.match(next.regexp) || [])[next.regexpMatch || 0]; result.push({ lexeme: next, value }); query = query.substring(value.length); diff --git a/src/core/syntax-tree/lexicon.ts b/src/core/syntax-tree/lexicon.ts index 30677c8b9..6a26e1b77 100644 --- a/src/core/syntax-tree/lexicon.ts +++ b/src/core/syntax-tree/lexicon.ts @@ -13,12 +13,14 @@ export enum LexemeType { export interface IUnboundedLexeme { evaluate?: (target: any, tree: ISyntaxTree) => boolean; - present?: (tree: ISyntaxTree) => string; + present?: (tree: ISyntaxTree) => any; resolve?: (target: any, tree: ISyntaxTree) => any; + subType?: string; type: string; nesting?: number; priority?: number; - regexp: RegExp | RegExp[]; + regexp: RegExp; + regexpMatch?: number; syntaxer?: (lexs: any[], pivot: any, pivotIndex: number) => any; } diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 5be2d8c53..ecff2a255 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -982,6 +982,11 @@ export const propTypes = { */ virtualization: PropTypes.bool, + /** + * TBD. + */ + derived_filtering: PropTypes.object, + /** * This property represents the current state of `data` * on the current page. This property will be updated diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index 8fef12fac..6410b0600 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -2,28 +2,58 @@ import isNumeric from 'fast-isnumeric'; import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -import operand, { FIELD_REGEX } from './operand'; +import operand, { FIELD_REGEX, getField } from './operand'; const STRING_REGEX = /^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))/; -const VALUE_REGEX = /^([^\s'"`{}()\\]|\\[\s'"`{}()]?)+/; - -const expression: IUnboundedLexeme = { - present: (tree: ISyntaxTree) => tree.value, - resolve: (target: any, tree: ISyntaxTree) => { - if (FIELD_REGEX.test(tree.value)) { - return operand.resolve && operand.resolve(target, tree); - } else if (STRING_REGEX.test(tree.value)) { - return tree.value.slice(1, tree.value.length - 1).replace(/\\('|"|`)/g, '$1'); - } else if (VALUE_REGEX.test(tree.value)) { - return isNumeric(tree.value) ? - +tree.value : - tree.value.replace(/\\([\s'"`{}()])/g, '$1'); +const VALUE_REGEX = /^(([^\s'"`{}()\\]|\\[\s'"`{}()]?)+)(?:[\s)]|$)/; + +export const fieldExpression: IUnboundedLexeme = { + present: (tree: ISyntaxTree) => getField(tree.value), + resolve: operand.resolve, + regexp: FIELD_REGEX, + subType: 'field', + type: LexemeType.Expression +}; + +const getString = ( + value: string +) => value.slice(1, value.length - 1).replace(/\\(['"`])/g, '$1'); + +const getValue = ( + value: string +) => { + value = (value.match(VALUE_REGEX) as any)[1]; + + return isNumeric(value) ? + +value : + value.replace(/\\([\s'"`{}()])/g, '$1'); +}; + +export const stringExpression: IUnboundedLexeme = { + present: (tree: ISyntaxTree) => getString(tree.value), + resolve: (_target: any, tree: ISyntaxTree) => { + if (STRING_REGEX.test(tree.value)) { + return getString(tree.value); } else { throw new Error(); } }, - regexp: [FIELD_REGEX, STRING_REGEX, VALUE_REGEX], + regexp: STRING_REGEX, + subType: 'string', type: LexemeType.Expression }; -export default expression; \ No newline at end of file +export const valueExpression: IUnboundedLexeme = { + present: (tree: ISyntaxTree) => getValue(tree.value), + resolve: (_target: any, tree: ISyntaxTree) => { + if (VALUE_REGEX.test(tree.value)) { + return getValue(tree.value); + } else { + throw new Error(); + } + }, + regexp: VALUE_REGEX, + regexpMatch: 1, + subType: 'value', + type: LexemeType.Expression +}; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts index d5c9d52ec..68862526e 100644 --- a/src/dash-table/syntax-tree/lexeme/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -3,7 +3,7 @@ import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; export const FIELD_REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; -const getField = ( +export const getField = ( value: string ) => value .slice(1, value.length - 1) diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index 47cc0d805..e8911ca78 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -1,6 +1,10 @@ import * as R from 'ramda'; -import expression from '../lexeme/expression'; +import { + fieldExpression, + stringExpression, + valueExpression +} from '../lexeme/expression'; import { equal, greaterOrEqual, @@ -48,15 +52,19 @@ const lexicon: ILexeme[] = [ if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous, terminal: true })), - { - ...expression, + ...[ + fieldExpression, + stringExpression, + valueExpression + ].map(exp => ({ + ...exp, if: (_lexs: ILexemeResult[], previous: ILexemeResult) => !previous || R.contains( previous.lexeme.type, [LexemeType.RelationalOperator] ), terminal: true - } + })) ]; export default lexicon; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index dcc4dd035..75373c8d7 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -1,10 +1,14 @@ import * as R from 'ramda'; -import expression from '../lexeme/expression'; -import operand from '../lexeme/operand'; +import { + fieldExpression, + stringExpression, + valueExpression +} from '../lexeme/expression'; import { and } from '../lexeme/logical'; +import operand from '../lexeme/operand'; import { equal, greaterOrEqual, @@ -81,15 +85,19 @@ const lexicon: ILexeme[] = [ ), terminal: true })), - { - ...expression, + ...[ + fieldExpression, + stringExpression, + valueExpression + ].map(exp => ({ + ...exp, if: (_: ILexemeResult[], previous: ILexemeResult) => previous && R.contains( previous.lexeme.type, [LexemeType.RelationalOperator] ), terminal: true - } + })) ]; export default lexicon; \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index 93ade6caa..ffd43e66c 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -1,15 +1,19 @@ import * as R from 'ramda'; -import expression from '../lexeme/expression'; -import operand from '../lexeme/operand'; import { blockClose, blockOpen } from '../lexeme/block'; +import { + fieldExpression, + stringExpression, + valueExpression +} from '../lexeme/expression'; import { and, or } from '../lexeme/logical'; +import operand from '../lexeme/operand'; import { equal, greaterOrEqual, @@ -148,11 +152,15 @@ const lexicon: ILexeme[] = [ ), terminal: false }, - { - ...expression, + ...[ + fieldExpression, + stringExpression, + valueExpression + ].map(exp => ({ + ...exp, if: ifExpression, terminal: isTerminal - } + })) ]; export default lexicon; \ No newline at end of file diff --git a/tests/cypress/tests/unit/lexeme_test.ts b/tests/cypress/tests/unit/lexeme_test.ts index 198ecc96a..335d2bca9 100644 --- a/tests/cypress/tests/unit/lexeme_test.ts +++ b/tests/cypress/tests/unit/lexeme_test.ts @@ -1,39 +1,91 @@ -import expression from 'dash-table/syntax-tree/lexeme/expression'; +import { + fieldExpression, + stringExpression, + valueExpression +} from 'dash-table/syntax-tree/lexeme/expression'; import operand from 'dash-table/syntax-tree/lexeme/operand'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; describe('expression', () => { - it('resolves values', () => { - expect(!!expression.resolve).to.equal(true); - expect(typeof expression.resolve).to.equal('function'); - - if (expression.resolve) { - expect(expression.resolve(undefined, { value: 'abc' } as ISyntaxTree)).to.equal('abc'); - expect(expression.resolve(undefined, { value: 'a\\ bc' } as ISyntaxTree)).to.equal('a bc'); - expect(expression.resolve(undefined, { value: '\\' } as ISyntaxTree)).to.equal('\\'); - expect(expression.resolve(undefined, { value: 'abc\\' } as ISyntaxTree)).to.equal('abc\\'); - expect(expression.resolve(undefined, { value: '\'abc\'' } as ISyntaxTree)).to.equal('abc'); - expect(expression.resolve(undefined, { value: '"abc"' } as ISyntaxTree)).to.equal('abc'); - expect(expression.resolve(undefined, { value: '`abc`' } as ISyntaxTree)).to.equal('abc'); - expect(expression.resolve(undefined, { value: '123' } as ISyntaxTree)).to.equal(123); - expect(expression.resolve(undefined, { value: '123.45' } as ISyntaxTree)).to.equal(123.45); - expect(expression.resolve(undefined, { value: '1E6' } as ISyntaxTree)).to.equal(1000000); - expect(expression.resolve(undefined, { value: '0x100' } as ISyntaxTree)).to.equal(256); - - expect(expression.resolve(undefined, { value: '"\\""' } as ISyntaxTree)).to.equal('"'); - expect(expression.resolve(undefined, { value: `'\\''` } as ISyntaxTree)).to.equal(`'`); - expect(expression.resolve(undefined, { value: '`\\``' } as ISyntaxTree)).to.equal('`'); - - expect(expression.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3); - expect(expression.resolve({ ['a bc']: 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3); - expect(expression.resolve({ ['{abc}']: 3 }, { value: '{\\{abc\\}}' } as ISyntaxTree)).to.equal(3); - expect(expression.resolve({ ['"abc"']: 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3); - - expect(expression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); - expect(expression.resolve.bind(undefined, undefined, { value: '`' } as ISyntaxTree)).to.throw(Error); - expect(expression.resolve.bind(undefined, undefined, { value: `'` } as ISyntaxTree)).to.throw(Error); - expect(expression.resolve.bind(undefined, undefined, { value: '{' } as ISyntaxTree)).to.throw(Error); - expect(expression.resolve.bind(undefined, undefined, { value: '}' } as ISyntaxTree)).to.throw(Error); + it('resolves field expression', () => { + expect(!!fieldExpression.resolve).to.equal(true); + expect(typeof fieldExpression.resolve).to.equal('function'); + + if (fieldExpression.resolve) { + expect(fieldExpression.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3); + expect(fieldExpression.resolve({ ['a bc']: 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3); + expect(fieldExpression.resolve({ ['{abc}']: 3 }, { value: '{\\{abc\\}}' } as ISyntaxTree)).to.equal(3); + expect(fieldExpression.resolve({ ['"abc"']: 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3); + + expect(fieldExpression.resolve.bind(undefined, undefined, { value: '3' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: 'abc' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: '{abc' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: '`' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: `'` } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: '{' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, undefined, { value: '}' } as ISyntaxTree)).to.throw(Error); + } + }); + + it('resolves string expressions', () => { + expect(!!stringExpression.resolve).to.equal(true); + expect(typeof stringExpression.resolve).to.equal('function'); + + if (stringExpression.resolve) { + expect(stringExpression.resolve(undefined, { value: '\'abc\'' } as ISyntaxTree)).to.equal('abc'); + expect(stringExpression.resolve(undefined, { value: '"abc"' } as ISyntaxTree)).to.equal('abc'); + expect(stringExpression.resolve(undefined, { value: '`abc`' } as ISyntaxTree)).to.equal('abc'); + expect(stringExpression.resolve(undefined, { value: '"\\""' } as ISyntaxTree)).to.equal('"'); + expect(stringExpression.resolve(undefined, { value: `'\\''` } as ISyntaxTree)).to.equal(`'`); + expect(stringExpression.resolve(undefined, { value: '`\\``' } as ISyntaxTree)).to.equal('`'); + expect(stringExpression.resolve(undefined, { value: '\'\\\'' } as ISyntaxTree)).to.equal('\\'); + + expect(stringExpression.resolve.bind(undefined, undefined, { value: '3' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: 'abc' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: '{abc' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: '`' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: `'` } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: '{' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, undefined, { value: '}' } as ISyntaxTree)).to.throw(Error); + } + }); + + it('resolves value expressions', () => { + expect(!!valueExpression.resolve).to.equal(true); + expect(typeof valueExpression.resolve).to.equal('function'); + + if (valueExpression.resolve) { + expect(valueExpression.resolve(undefined, { value: 'abc' } as ISyntaxTree)).to.equal('abc'); + expect(valueExpression.resolve(undefined, { value: 'abc ' } as ISyntaxTree)).to.equal('abc'); + expect(valueExpression.resolve(undefined, { value: 'a\\ bc' } as ISyntaxTree)).to.equal('a bc'); + expect(valueExpression.resolve(undefined, { value: '\\' } as ISyntaxTree)).to.equal('\\'); + expect(valueExpression.resolve(undefined, { value: 'abc\\' } as ISyntaxTree)).to.equal('abc\\'); + expect(valueExpression.resolve(undefined, { value: '123' } as ISyntaxTree)).to.equal(123); + expect(valueExpression.resolve(undefined, { value: '123.45' } as ISyntaxTree)).to.equal(123.45); + expect(valueExpression.resolve(undefined, { value: '1E6' } as ISyntaxTree)).to.equal(1000000); + expect(valueExpression.resolve(undefined, { value: '0x100' } as ISyntaxTree)).to.equal(256); + expect(valueExpression.resolve(undefined, { value: '\\{abc' } as ISyntaxTree)).to.equal('{abc'); + expect(valueExpression.resolve(undefined, { value: 'abc\\}' } as ISyntaxTree)).to.equal('abc}'); + expect(valueExpression.resolve(undefined, { value: '\\}abc\\{' } as ISyntaxTree)).to.equal('}abc{'); + expect(valueExpression.resolve(undefined, { value: '\\{\\{abc\\}\\}' } as ISyntaxTree)).to.equal('{{abc}}'); + + expect(valueExpression.resolve.bind(undefined, undefined, { value: '{abc' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, undefined, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, undefined, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, undefined, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, undefined, { value: '`' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, undefined, { value: `'` } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, undefined, { value: '{' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, undefined, { value: '}' } as ISyntaxTree)).to.throw(Error); } }); }); From 1b3bf519e0fbb22821182ac84bb938e443eb19a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 1 Apr 2019 11:05:54 -0400 Subject: [PATCH 32/54] update subtype --- src/dash-table/syntax-tree/lexeme/block.ts | 2 +- src/dash-table/syntax-tree/lexeme/logical.ts | 4 ++-- src/dash-table/syntax-tree/lexeme/relational.ts | 12 ++++++------ src/dash-table/syntax-tree/lexeme/unary.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dash-table/syntax-tree/lexeme/block.ts b/src/dash-table/syntax-tree/lexeme/block.ts index 2131e22c4..3e804ba87 100644 --- a/src/dash-table/syntax-tree/lexeme/block.ts +++ b/src/dash-table/syntax-tree/lexeme/block.ts @@ -17,7 +17,7 @@ export const blockOpen: IUnboundedLexeme = { }, type: LexemeType.BlockOpen, nesting: 1, - present: () => '()', + subType: '()', priority: 1, regexp: /^\(/, syntaxer: (lexs: any[]) => { diff --git a/src/dash-table/syntax-tree/lexeme/logical.ts b/src/dash-table/syntax-tree/lexeme/logical.ts index 2477235ee..3fb4a0df3 100644 --- a/src/dash-table/syntax-tree/lexeme/logical.ts +++ b/src/dash-table/syntax-tree/lexeme/logical.ts @@ -18,7 +18,7 @@ export const and: IUnboundedLexeme = { type: LexemeType.LogicalOperator, priority: 2, regexp: /^(and\s|&&)/i, - present: () => LogicalOperator.And, + subType: LogicalOperator.And, syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { return Object.assign({ left: lexs.slice(0, pivotIndex), @@ -37,7 +37,7 @@ export const or: IUnboundedLexeme = { t.right.lexeme.evaluate(target, t.right); }, type: LexemeType.LogicalOperator, - present: () => LogicalOperator.Or, + subType: LogicalOperator.Or, priority: 3, regexp: /^(or\s|\|\|)/i, syntaxer: (lexs: any[], pivot: any, pivotIndex: number) => { diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts index 6dce950dd..f3c411079 100644 --- a/src/dash-table/syntax-tree/lexeme/relational.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -46,36 +46,36 @@ const LEXEME_BASE = { export const equal: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op === exp), - present: () => RelationalOperator.Equal, + subType: RelationalOperator.Equal, regexp: /^(=|eq)/i }, LEXEME_BASE); export const greaterOrEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op >= exp), - present: () => RelationalOperator.GreaterOrEqual, + subType: RelationalOperator.GreaterOrEqual, regexp: /^(>=|ge)/i }, LEXEME_BASE); export const greaterThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op > exp), - present: () => RelationalOperator.GreatherThan, + subType: RelationalOperator.GreatherThan, regexp: /^(>|gt)/i }, LEXEME_BASE); export const lessOrEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op <= exp), - present: () => RelationalOperator.LessOrEqual, + subType: RelationalOperator.LessOrEqual, regexp: /^(<=|le)/i }, LEXEME_BASE); export const lessThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op < exp), - present: () => RelationalOperator.LessThan, + subType: RelationalOperator.LessThan, regexp: /^(<|lt)/i }, LEXEME_BASE); export const notEqual: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op !== exp), - present: () => RelationalOperator.NotEqual, + subType: RelationalOperator.NotEqual, regexp: /^(!=|ne)/i }, LEXEME_BASE); \ No newline at end of file diff --git a/src/dash-table/syntax-tree/lexeme/unary.ts b/src/dash-table/syntax-tree/lexeme/unary.ts index 643d70075..aab1d5f52 100644 --- a/src/dash-table/syntax-tree/lexeme/unary.ts +++ b/src/dash-table/syntax-tree/lexeme/unary.ts @@ -55,7 +55,7 @@ export const not: IUnboundedLexeme = { return !t.right.lexeme.evaluate(target, t.right); }, type: LexemeType.UnaryOperator, - present: () => UnaryOperator.Not, + subType: UnaryOperator.Not, priority: 1.5, regexp: /^!/, syntaxer: (lexs: any[]) => { From 4fabf222c6d0013384537eb1f6afee2b255043a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 5 Apr 2019 13:16:19 -0400 Subject: [PATCH 33/54] add derived_query_structure prop --- src/core/syntax-tree/index.ts | 6 ++-- src/dash-table/components/Table/index.tsx | 11 +++++++ src/dash-table/components/Table/props.ts | 1 + src/dash-table/dash/DataTable.js | 31 +++++++++++++++++-- .../syntax-tree/lexeme/expression.ts | 2 +- src/dash-table/syntax-tree/lexeme/operand.ts | 1 + 6 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/core/syntax-tree/index.ts b/src/core/syntax-tree/index.ts index d9849d1a4..bf3e3568f 100644 --- a/src/core/syntax-tree/index.ts +++ b/src/core/syntax-tree/index.ts @@ -84,8 +84,10 @@ export default class SyntaxTree { } toStructure() { - if (this.syntaxerResult.tree) { - return toStructure(this.syntaxerResult.tree); + if (!this.isValid || !this.syntaxerResult.tree) { + return null; } + + return toStructure(this.syntaxerResult.tree); } } \ No newline at end of file diff --git a/src/dash-table/components/Table/index.tsx b/src/dash-table/components/Table/index.tsx index 38c61796b..e97a5e0ed 100644 --- a/src/dash-table/components/Table/index.tsx +++ b/src/dash-table/components/Table/index.tsx @@ -12,6 +12,8 @@ import derivedVirtualData from 'dash-table/derived/data/virtual'; import derivedVirtualizedData from 'dash-table/derived/data/virtualized'; import derivedVisibleColumns from 'dash-table/derived/column/visible'; +import QuerySyntaxTree from 'dash-table/syntax-tree/QuerySyntaxTree'; + import { ControlledTableProps, PropsWithDefaultsAndDerived, @@ -156,6 +158,8 @@ export default class Table extends Component = {}; + if (!derivedStructureCache.cached) { + newProps.derived_query_structure = derivedStructureCache.result; + } + if (!virtualCached) { newProps.derived_virtual_data = virtual.data; newProps.derived_virtual_indices = virtual.indices; @@ -261,4 +269,7 @@ export default class Table extends Component viewport); private readonly virtualCache = memoizeOneWithFlag(virtual => virtual); private readonly virtualSelectedRowsCache = memoizeOneWithFlag(virtual => virtual); + private readonly structuredQueryCache = memoizeOneWithFlag( + (query: string) => new QuerySyntaxTree(query).toStructure() + ); } \ No newline at end of file diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index dce15415b..a4dfeb7de 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -345,6 +345,7 @@ interface IDefaultProps { } interface IDerivedProps { + derived_query_structure: object | null; derived_viewport_data: Data; derived_viewport_indices: Indices; derived_viewport_selected_rows: Indices; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index ecff2a255..786537af3 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -983,9 +983,36 @@ export const propTypes = { virtualization: PropTypes.bool, /** - * TBD. + * This property represents the current structure of + * `filtering_settings` as a tree structure. Each node of the + * query structure have: + * - type (string; required) + * - 'open-block' + * - 'logical-operator' + * - 'relational-operator' + * - 'unary-operator' + * - 'expression' + * - 'operand' + * - subType (string; optional) + * - 'open-block': '()' + * - 'logical-operator': '&&', '||' + * - 'relational-operator': '=', '>=', '>', '<=', '<', '!=' + * - 'unary-operator': '!', 'is bool', 'is even', 'is nil', 'is num', 'is object', 'is odd', 'is prime', 'is str' + * - 'expression': 'value', 'field' + * - 'operand': 'field' + * - value (any) + * - 'expression, value': passed value + * - 'expression, field': the field/prop name + * - 'operand, field': the field/prop name + * + * - block (nested query structure; optional) + * - left (nested query structure; optional) + * - right (nested query structure; optional) + * + * If the query is invalid or empty, the `derived_query_structure` will + * be null. */ - derived_filtering: PropTypes.object, + derived_query_structure: PropTypes.object, /** * This property represents the current state of `data` diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index 6410b0600..482e30a07 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -39,7 +39,7 @@ export const stringExpression: IUnboundedLexeme = { } }, regexp: STRING_REGEX, - subType: 'string', + subType: 'value', type: LexemeType.Expression }; diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts index 68862526e..bfc18c66a 100644 --- a/src/dash-table/syntax-tree/lexeme/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -19,6 +19,7 @@ const operand: IUnboundedLexeme = { } }, regexp: FIELD_REGEX, + subType: 'field', type: LexemeType.Operand }; From d936a0e3225a076ff043410e6d4c023b103e6765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 5 Apr 2019 14:59:51 -0400 Subject: [PATCH 34/54] fix filtering tests --- src/dash-table/components/FilterFactory.tsx | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index f1418f102..713379e16 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -81,9 +81,30 @@ export default class FilterFactory { } private updateOps = memoizeOne((query: string) => { - const ast = new MultiColumnsSyntaxTree(query); + const multiQuery = new MultiColumnsSyntaxTree(query); - this.ops = getSingleColumnMap(ast) || this.ops; + const newOps = getSingleColumnMap(multiQuery); + if (!newOps) { + return; + } + + /* Mapping multi-column to single column queries will expand + * compressed forms. If the new ast query is equal to the + * old one, keep the old one instead. + * + * If the value was changed by the user, the current ast will + * have been modified already and the UI experience will also + * be consistent in that case. + */ + R.forEach(([key, ast]) => { + const newAst = newOps.get(key); + + if (newAst && newAst.toQueryString() === ast.toQueryString()) { + newOps.set(key, ast); + } + }, Array.from(this.ops.entries())); + + this.ops = newOps; }); public createFilters() { From edfd86579840531b3de71b36aabacc94f36ab3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 5 Apr 2019 16:42:38 -0400 Subject: [PATCH 35/54] fix filtering tests --- src/dash-table/components/FilterFactory.tsx | 33 +++++++++++++------ src/dash-table/derived/table/index.tsx | 19 ++++++++--- .../tests/standalone/filtering_test.ts | 13 +++++++- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index 713379e16..b4b960af3 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -3,6 +3,7 @@ import React from 'react'; import Logger from 'core/Logger'; import { arrayMap } from 'core/math/arrayZipMap'; +import memoizerCache from 'core/cache/memoizer'; import { memoizeOne } from 'core/memoizer'; import ColumnFilter from 'dash-table/components/Filter/Column'; @@ -107,6 +108,22 @@ export default class FilterFactory { this.ops = newOps; }); + private filter = memoizerCache<[ColumnId, number]>()(( + column: ColumnId, + index: number, + ast: SingleColumnSyntaxTree | undefined, + setFilter: SetFilter + ) => { + return (); + }); + public createFilters() { const { columns, @@ -142,16 +159,12 @@ export default class FilterFactory { ); const filters = R.addIndex(R.map)((column, index) => { - const ast = this.ops.get(column.id.toString()); - - return (); + return this.filter.get(column.id, index)( + column.id, + index, + this.ops.get(column.id.toString()), + setFilter + ); }, columns); const styledFilters = arrayMap( diff --git a/src/dash-table/derived/table/index.tsx b/src/dash-table/derived/table/index.tsx index e01765fe3..82d4fe4b7 100644 --- a/src/dash-table/derived/table/index.tsx +++ b/src/dash-table/derived/table/index.tsx @@ -1,5 +1,7 @@ import * as R from 'ramda'; +import { memoizeOne } from 'core/memoizer'; + import CellFactory from 'dash-table/components/CellFactory'; import FilterFactory from 'dash-table/components/FilterFactory'; import HeaderFactory from 'dash-table/components/HeaderFactory'; @@ -10,12 +12,10 @@ const handleSetFilter = (setProps: SetProps, setState: SetState, filtering_setti setState({ rawFilterQuery }); }; -function filterPropsFn(propsFn: () => ControlledTableProps) { +function filterPropsFn(propsFn: () => ControlledTableProps, setFilter: any) { const props = propsFn(); - return R.merge(props, { - setFilter: handleSetFilter.bind(undefined, props.setProps, props.setState) - }); + return R.merge(props, { setFilter }); } function getter( @@ -37,8 +37,17 @@ function getter( } export default (propsFn: () => ControlledTableProps) => { + const setFilter = memoizeOne(( + setProps: SetProps, + setState: SetState + ) => handleSetFilter.bind(undefined, setProps, setState)); + const cellFactory = new CellFactory(propsFn); - const filterFactory = new FilterFactory(() => filterPropsFn(propsFn)); + const filterFactory = new FilterFactory(() => { + const props = propsFn(); + + return filterPropsFn(propsFn, setFilter(props.setProps, props.setState)); + }); const headerFactory = new HeaderFactory(propsFn); return getter.bind(undefined, cellFactory, filterFactory, headerFactory); diff --git a/tests/cypress/tests/standalone/filtering_test.ts b/tests/cypress/tests/standalone/filtering_test.ts index dbe6b2ddd..fe2b2c856 100644 --- a/tests/cypress/tests/standalone/filtering_test.ts +++ b/tests/cypress/tests/standalone/filtering_test.ts @@ -47,6 +47,18 @@ describe('filter', () => { DashTable.toggleScroll(false); }); + it('handles hovering onto other filtering cells', () => { + DashTable.getFilterById('ccc').click(); + DOM.focused.type(`gt 100`); + DashTable.getFilterById('ddd').click(); + DOM.focused.type('lt 20000'); + + DashTable.getCellById(0, 'eee').trigger('mouseover'); + + DashTable.getFilterById('ccc').within(() => cy.get('input').should('have.value', 'gt 100')); + DashTable.getFilterById('ddd').within(() => cy.get('input').should('have.value', 'lt 20000')); + }); + it('handles invalid queries', () => { let cell_0; let cell_1; @@ -81,7 +93,6 @@ describe('filter', () => { DashTable.getFilterById('ccc').should('have.class', 'invalid'); DashTable.getFilterById('ddd').should('have.class', 'invalid'); DashTable.getFilterById('eee').should('have.class', 'invalid'); - }); it('reset updates results and filter fields', () => { From 6b9f73785913fcded260d573f020d057c17888b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Apr 2019 14:29:42 -0400 Subject: [PATCH 36/54] Updating Cypress --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43362cd38..ed2d50f89 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "babel-loader": "^8.0.5", "core-js": "^2.6.5", "css-loader": "^2.1.0", - "cypress": "^3.1.5", + "cypress": "^3.2.0", "d3-format": "^1.3.2", "fast-isnumeric": "^1.1.2", "file-loader": "^3.0.1", From fb894bfba98ac9c94eec7b557951588b27d1b0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Apr 2019 14:30:15 -0400 Subject: [PATCH 37/54] fix review comments --- .../syntax-tree/lexeme/expression.ts | 16 ++-- src/dash-table/syntax-tree/lexeme/operand.ts | 4 +- tests/cypress/tests/unit/lexeme_test.ts | 84 ++++++++++--------- 3 files changed, 55 insertions(+), 49 deletions(-) diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index 482e30a07..e6ec412a1 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -1,19 +1,19 @@ import isNumeric from 'fast-isnumeric'; +import * as R from 'ramda'; import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -import operand, { FIELD_REGEX, getField } from './operand'; +import operand from './operand'; const STRING_REGEX = /^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))/; const VALUE_REGEX = /^(([^\s'"`{}()\\]|\\[\s'"`{}()]?)+)(?:[\s)]|$)/; -export const fieldExpression: IUnboundedLexeme = { - present: (tree: ISyntaxTree) => getField(tree.value), - resolve: operand.resolve, - regexp: FIELD_REGEX, - subType: 'field', - type: LexemeType.Expression -}; +export const fieldExpression: IUnboundedLexeme = R.merge( + operand, { + subType: 'field', + type: LexemeType.Expression + } +); const getString = ( value: string diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts index bfc18c66a..b24fbf669 100644 --- a/src/dash-table/syntax-tree/lexeme/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -1,9 +1,9 @@ import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -export const FIELD_REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; +const FIELD_REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; -export const getField = ( +const getField = ( value: string ) => value .slice(1, value.length - 1) diff --git a/tests/cypress/tests/unit/lexeme_test.ts b/tests/cypress/tests/unit/lexeme_test.ts index 335d2bca9..320296cea 100644 --- a/tests/cypress/tests/unit/lexeme_test.ts +++ b/tests/cypress/tests/unit/lexeme_test.ts @@ -13,21 +13,25 @@ describe('expression', () => { if (fieldExpression.resolve) { expect(fieldExpression.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3); - expect(fieldExpression.resolve({ ['a bc']: 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3); - expect(fieldExpression.resolve({ ['{abc}']: 3 }, { value: '{\\{abc\\}}' } as ISyntaxTree)).to.equal(3); - expect(fieldExpression.resolve({ ['"abc"']: 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3); + expect(fieldExpression.resolve({ 'a bc': 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3); + expect(fieldExpression.resolve({ '{abc}': 3 }, { value: '{\\{abc\\}}' } as ISyntaxTree)).to.equal(3); + expect(fieldExpression.resolve({ '"abc"': 3 }, { value: '{"abc"}' } as ISyntaxTree)).to.equal(3); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: '3' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: 'abc' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: '{abc' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: '`' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: `'` } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: '{' } as ISyntaxTree)).to.throw(Error); - expect(fieldExpression.resolve.bind(undefined, undefined, { value: '}' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve({ abc: 3 }, { value: '{def}' } as ISyntaxTree) === undefined).to.equal(true); + expect(fieldExpression.resolve({ abc: 3 }, { value: '{a bc}' } as ISyntaxTree) === undefined).to.equal(true); + expect(fieldExpression.resolve({ abc: 3 }, { value: '{"abc"}' } as ISyntaxTree) === undefined).to.equal(true); + + expect(fieldExpression.resolve.bind(undefined, {}, { value: '3' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: 'abc' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: '{abc' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: '"' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: '`' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: `'` } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: '{' } as ISyntaxTree)).to.throw(Error); + expect(fieldExpression.resolve.bind(undefined, {}, { value: '}' } as ISyntaxTree)).to.throw(Error); } }); @@ -44,17 +48,17 @@ describe('expression', () => { expect(stringExpression.resolve(undefined, { value: '`\\``' } as ISyntaxTree)).to.equal('`'); expect(stringExpression.resolve(undefined, { value: '\'\\\'' } as ISyntaxTree)).to.equal('\\'); - expect(stringExpression.resolve.bind(undefined, undefined, { value: '3' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: 'abc' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: '{abc' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: '`' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: `'` } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: '{' } as ISyntaxTree)).to.throw(Error); - expect(stringExpression.resolve.bind(undefined, undefined, { value: '}' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: '3' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: 'abc' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: '{abc' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: '"' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: '`' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: `'` } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: '{' } as ISyntaxTree)).to.throw(Error); + expect(stringExpression.resolve.bind(undefined, {}, { value: '}' } as ISyntaxTree)).to.throw(Error); } }); @@ -65,6 +69,8 @@ describe('expression', () => { if (valueExpression.resolve) { expect(valueExpression.resolve(undefined, { value: 'abc' } as ISyntaxTree)).to.equal('abc'); expect(valueExpression.resolve(undefined, { value: 'abc ' } as ISyntaxTree)).to.equal('abc'); + expect(valueExpression.resolve(undefined, { value: 'abc\\ \\ \\ ' } as ISyntaxTree)).to.equal('abc '); + expect(valueExpression.resolve(undefined, { value: '\\ \\ \\ abc' } as ISyntaxTree)).to.equal(' abc'); expect(valueExpression.resolve(undefined, { value: 'a\\ bc' } as ISyntaxTree)).to.equal('a bc'); expect(valueExpression.resolve(undefined, { value: '\\' } as ISyntaxTree)).to.equal('\\'); expect(valueExpression.resolve(undefined, { value: 'abc\\' } as ISyntaxTree)).to.equal('abc\\'); @@ -77,15 +83,15 @@ describe('expression', () => { expect(valueExpression.resolve(undefined, { value: '\\}abc\\{' } as ISyntaxTree)).to.equal('}abc{'); expect(valueExpression.resolve(undefined, { value: '\\{\\{abc\\}\\}' } as ISyntaxTree)).to.equal('{{abc}}'); - expect(valueExpression.resolve.bind(undefined, undefined, { value: '{abc' } as ISyntaxTree)).to.throw(Error); - expect(valueExpression.resolve.bind(undefined, undefined, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); - expect(valueExpression.resolve.bind(undefined, undefined, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); - expect(valueExpression.resolve.bind(undefined, undefined, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); - expect(valueExpression.resolve.bind(undefined, undefined, { value: '"' } as ISyntaxTree)).to.throw(Error); - expect(valueExpression.resolve.bind(undefined, undefined, { value: '`' } as ISyntaxTree)).to.throw(Error); - expect(valueExpression.resolve.bind(undefined, undefined, { value: `'` } as ISyntaxTree)).to.throw(Error); - expect(valueExpression.resolve.bind(undefined, undefined, { value: '{' } as ISyntaxTree)).to.throw(Error); - expect(valueExpression.resolve.bind(undefined, undefined, { value: '}' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: '{abc' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: '}abc{' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: '"' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: '`' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: `'` } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: '{' } as ISyntaxTree)).to.throw(Error); + expect(valueExpression.resolve.bind(undefined, {}, { value: '}' } as ISyntaxTree)).to.throw(Error); } }); }); @@ -96,11 +102,11 @@ describe('operand', () => { expect(typeof operand.resolve).to.equal('function'); if (operand.resolve) { - expect(operand.resolve.bind(undefined, undefined, { value: 'abc' } as ISyntaxTree)).to.throw(Error); - expect(operand.resolve.bind(undefined, undefined, { value: '123' } as ISyntaxTree)).to.throw(Error); - expect(operand.resolve.bind(undefined, undefined, { value: '{abc' } as ISyntaxTree)).to.throw(Error); - expect(operand.resolve.bind(undefined, undefined, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); - expect(operand.resolve.bind(undefined, undefined, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, {}, { value: 'abc' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, {}, { value: '123' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, {}, { value: '{abc' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, {}, { value: 'abc}' } as ISyntaxTree)).to.throw(Error); + expect(operand.resolve.bind(undefined, {}, { value: '{{abc}}' } as ISyntaxTree)).to.throw(Error); expect(operand.resolve({ abc: 3 }, { value: '{abc}' } as ISyntaxTree)).to.equal(3); expect(operand.resolve({ ['a bc']: 3 }, { value: '{a bc}' } as ISyntaxTree)).to.equal(3); From 5bea8b5d1ac600dd7ba993acc3d0a3b63301e856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Apr 2019 14:32:59 -0400 Subject: [PATCH 38/54] update styling tests --- tests/dash/app_styling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/dash/app_styling.py b/tests/dash/app_styling.py index a663ff3cd..1b5afd481 100644 --- a/tests/dash/app_styling.py +++ b/tests/dash/app_styling.py @@ -443,10 +443,10 @@ def layout(): "width": "100%" }, style_data_conditional=[{ - "if": { "column_id": "Region", "filter": "Region eq Montreal" }, + "if": { "column_id": "Region", "filter": "{Region} eq Montreal" }, "background_color": "yellow" }, { - "if": { "column_id": "Humidity", "filter": "Humidity eq 20" }, + "if": { "column_id": "Humidity", "filter": "{Humidity} eq 20" }, "background_color": "yellow" }] ) From 2f05a3f400ea9319dac90811e15306a3032ff93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Apr 2019 14:36:07 -0400 Subject: [PATCH 39/54] add back test delay --- tests/dash/test_integration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/dash/test_integration.py b/tests/dash/test_integration.py index 73df6a3d1..6a27e878e 100644 --- a/tests/dash/test_integration.py +++ b/tests/dash/test_integration.py @@ -18,7 +18,9 @@ def test_review_app(self): def visit_and_snapshot(href): self.driver.get(href) - + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.ID, "waitfor")) + ) time.sleep(2) self.snapshot(href) self.driver.back() From 914089e737293096952e6a6656988c1252d907c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 08:28:31 -0400 Subject: [PATCH 40/54] fix py dropdown test --- tests/dash/app_dropdown.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/dash/app_dropdown.py b/tests/dash/app_dropdown.py index c047a1315..e9fbb6f89 100644 --- a/tests/dash/app_dropdown.py +++ b/tests/dash/app_dropdown.py @@ -97,7 +97,7 @@ def layout(): 'dropdowns': [ { - 'condition': 'City eq "NYC"', + 'condition': '{City} eq "NYC"', 'dropdown': [ {'label': i, 'value': i} for i in [ @@ -109,7 +109,7 @@ def layout(): }, { - 'condition': 'City eq "Montreal"', + 'condition': '{City} eq "Montreal"', 'dropdown': [ {'label': i, 'value': i} for i in [ @@ -121,7 +121,7 @@ def layout(): }, { - 'condition': 'City eq "Los Angeles"', + 'condition': '{City} eq "Los Angeles"', 'dropdown': [ {'label': i, 'value': i} for i in [ From 614b13862974c773364392202a043aa4a06a7f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 08:48:57 -0400 Subject: [PATCH 41/54] indentation --- tests/dash/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dash/test_integration.py b/tests/dash/test_integration.py index 6a27e878e..a19546cbf 100644 --- a/tests/dash/test_integration.py +++ b/tests/dash/test_integration.py @@ -19,7 +19,7 @@ def test_review_app(self): def visit_and_snapshot(href): self.driver.get(href) WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.ID, "waitfor")) + EC.presence_of_element_located((By.ID, "waitfor")) ) time.sleep(2) self.snapshot(href) From 40eb3283c65c316934a3fbc827c4a860eaad79ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 12:01:48 -0400 Subject: [PATCH 42/54] remove v1 tests (deprecated) --- .circleci/config.yml | 114 +++---------------------------------------- package.json | 6 +-- requirements-v1.txt | 4 -- requirements.txt | 2 +- 4 files changed, 9 insertions(+), 117 deletions(-) delete mode 100644 requirements-v1.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index f10b22159..619292432 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2 jobs: - "test-v0": + "test": docker: - image: circleci/python:3.6.7-node-browsers - image: cypress/base:10 @@ -30,67 +30,7 @@ jobs: sudo pip install --upgrade virtualenv python -m venv venv || virtualenv venv . venv/bin/activate - pip install -r requirements-base.txt --quiet - - - run: - name: Install dependencies (dash) - command: | - git clone git@github.com:plotly/dash.git - git clone git@github.com:plotly/dash-renderer.git - git clone git@github.com:plotly/dash-core-components.git - git clone git@github.com:plotly/dash-html-components.git - . venv/bin/activate - pip install -e ./dash --quiet - cd dash-renderer && npm install --ignore-scripts && npm run build && pip install -e . && cd .. - cd dash-core-components && npm install --ignore-scripts && npm run build && pip install -e . && cd .. - cd dash-html-components && npm install --ignore-scripts && npm run build && pip install -e . && cd .. - - - run: - name: Build - command: | - . venv/bin/activate - npm run private::build:js-test - npm run private::build:py - pip install -e . - - - run: - name: Run tests - command: | - . venv/bin/activate - npm run test-v0 - - - "test-v1": - docker: - - image: circleci/python:3.6.7-node-browsers - - image: cypress/base:10 - - steps: - - checkout - - restore_cache: - key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} - - run: - name: Install npm packages - command: npm install - - run: - name: Cypress Install - command: | - $(npm bin)/cypress install - - - save_cache: - key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} - paths: - - node_modules - - /home/circleci/.cache/Cypress - - - run: - name: Install requirements - command: | - sudo pip install --upgrade virtualenv - python -m venv venv || virtualenv venv - . venv/bin/activate - pip install -r requirements-base.txt --quiet - pip install -r requirements-v1.txt --quiet + pip install -r requirements.txt --quiet - run: name: Build @@ -104,7 +44,7 @@ jobs: name: Run tests command: | . venv/bin/activate - npm run test-v1 + npm run test "visual-test": @@ -172,7 +112,7 @@ jobs: when: always - "python-3.6-v0": + "python-3.6": docker: - image: circleci/python:3.6.7-stretch-node-browsers @@ -224,53 +164,11 @@ jobs: python -m unittest tests.dash.test_integration - "python-3.6-v1": - docker: - - image: circleci/python:3.6.7-stretch-node-browsers - - environment: - PERCY_ENABLED: True - PERCY_PROJECT: plotly/dash-table-python-v1 - - steps: - - checkout - - - run: - name: Inject Percy Environment variables - command: | - echo 'export PERCY_TOKEN="$PERCY_PYTHON_TOKEN_V1"' >> $BASH_ENV - - - run: - name: Install requirements - command: | - sudo pip install --upgrade virtualenv - python -m venv venv || virtualenv venv - . venv/bin/activate - pip install -r requirements-base.txt --quiet - pip install -r requirements-v1.txt --quiet - npm install - - - run: - name: Install test requirements - command: | - . venv/bin/activate - npm run build - pip install -e . - - - run: - name: Run integration tests - command: | - . venv/bin/activate - python -m unittest tests.dash.test_integration - - workflows: version: 2 build: jobs: - - "python-3.6-v0" - - "python-3.6-v1" + - "python-3.6" - "node" - - "test-v0" - - "test-v1" + - "test" - "visual-test" diff --git a/package.json b/package.json index 5f3905aeb..5ab5410d4 100644 --- a/package.json +++ b/package.json @@ -30,13 +30,11 @@ "private::runtests:unit": "cypress run --browser chrome --spec 'tests/cypress/tests/unit/**/*'", "private::runtests:standalone": "cypress run --browser chrome --spec 'tests/cypress/tests/standalone/**/*'", "private::runtests:server": "cypress run --browser chrome --spec 'tests/cypress/tests/server/**/*'", - "private::runtests-v0": "run-s private::runtests:server", - "private::runtests-v1": "run-s private::runtests:python private::runtests:unit private::runtests:standalone private::runtests:server", + "private::runtests": "run-s private::runtests:python private::runtests:unit private::runtests:standalone private::runtests:server", "build.watch": "webpack-dev-server --content-base dash_table --mode development", "build": "run-s private::build:js private::build:py", "lint": "run-s private::lint:*", - "test-v0": "run-p --race private::host* private::runtests-v0", - "test-v1": "run-p --race private::host* private::runtests-v1", + "test": "run-p --race private::host* private::runtests", "test.visual": "build-storybook && percy-storybook", "test.visual-local": "build-storybook", "test.watch": "run-p --race \"private::build:js-test-watch\" --race private::host* private::opentests" diff --git a/requirements-v1.txt b/requirements-v1.txt deleted file mode 100644 index 5d3d4a3a7..000000000 --- a/requirements-v1.txt +++ /dev/null @@ -1,4 +0,0 @@ -git+git://github.com/plotly/dash@release-v1#egg=dash -git+git://github.com/plotly/dash-core-components@release-v1#egg=dash_core_components -git+git://github.com/plotly/dash-html-components@release-v1#egg=dash_html_components -git+git://github.com/plotly/dash-renderer@release-v1#egg=dash_renderer \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 690080fe8..5d17344a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -r requirements-base.txt --r requirements-v1.txt \ No newline at end of file +-r requirements-v0.txt \ No newline at end of file From 96e1ee322b2e346de028e35c8418f61018bcaa8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 12:34:37 -0400 Subject: [PATCH 43/54] (test) super long delay on screenshot --- tests/dash/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dash/test_integration.py b/tests/dash/test_integration.py index a19546cbf..631b4b7d6 100644 --- a/tests/dash/test_integration.py +++ b/tests/dash/test_integration.py @@ -21,7 +21,7 @@ def visit_and_snapshot(href): WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.ID, "waitfor")) ) - time.sleep(2) + time.sleep(10) self.snapshot(href) self.driver.back() From ce18be8f45c1ee60aac046a815dbcd7af3888b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 13:40:35 -0400 Subject: [PATCH 44/54] rewrite some tests as storybook tests --- tests/dash/app_styling.py | 454 ------------------- tests/visual/percy-storybook/Style.percy.tsx | 103 +++++ 2 files changed, 103 insertions(+), 454 deletions(-) delete mode 100644 tests/dash/app_styling.py diff --git a/tests/dash/app_styling.py b/tests/dash/app_styling.py deleted file mode 100644 index 1b5afd481..000000000 --- a/tests/dash/app_styling.py +++ /dev/null @@ -1,454 +0,0 @@ -from collections import OrderedDict -from dash.dependencies import Input, Output -import dash_core_components as dcc -import dash_html_components as html -import pandas as pd -from textwrap import dedent - -import dash_table -from index import app -from .utils import html_table, section_title - - -def layout(): - data = OrderedDict( - [ - ("Date", ["2015-01-01", "2015-10-24", "2016-05-10"] * 2), - ("Region", ["Montreal", "Vermont", "New York City"] * 2), - ("Temperature", [1, -20, 3.512] * 2), - ("Humidity", [10, 20, 30] * 2), - ("Pressure", [2, 10924, 3912] * 2), - ] - ) - - df = pd.DataFrame(data) - - return html.Div( - style={"marginLeft": "auto", "marginRight": "auto", "width": "80%"}, - children=[ - html.H1("[WIP] - Styling the Table"), - - section_title("HTML Table - Gridded"), - - html.Div(""" - By default, the Dash table has grey headers and borders - around each cell. It resembles a spreadsheet with clearly defined - headers - """), - - html_table( - df, - cell_style={'border': 'thin lightgrey solid'}, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - header_style={'backgroundColor': 'rgb(235, 235, 235)'} - ), - - html.Hr(), - - section_title("HTML Table - Column Alignment and Column Fonts"), - dcc.Markdown(dedent( - """ - When displaying numerical data, it's a good practice to use - monospaced fonts, to right-align the data, and to provide the same - number of decimals throughout the column. - - Note that it's not possible to modify the number of decimal places - in css. `dash-table` will provide formatting options in the future, - until then you'll have to modify your data before displaying it. - - For textual data, left-aligning the data is usually easier to read. - - In both cases, the column headers should have the same alignment - as the cell content. - """ - )), - html_table( - df, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - cell_style={"paddingLeft": 5, "paddingRight": 5, 'border': 'thin lightgrey solid'}, - header_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - cell_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - header_style={'backgroundColor': 'rgb(235, 235, 235)'} - ), - - html.Hr(), - - section_title('HTML Table - Styling the Table as a List'), - - dcc.Markdown(dedent(''' - The gridded view is a good default view for an editable table, like a spreadsheet. - If your table isn't editable, then in many cases it can look cleaner without the - horizontal or vertical grid lines. - ''')), - - html_table( - df, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - cell_style={"paddingLeft": 5, "paddingRight": 5}, - header_style={'backgroundColor': 'rgb(235, 235, 235)', 'borderTop': 'thin lightgrey solid', 'borderBottom': 'thin lightgrey solid'}, - header_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - cell_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - ), - - html.Hr(), - - section_title('HTML Table - Row Padding'), - - dcc.Markdown(dedent(''' - By default, the gridded view is pretty tight. You can add some top and bottom row padding to - the rows to give your data a little bit more room to breathe. - ''')), - - html_table( - df, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - cell_style={"paddingLeft": 5, "paddingRight": 5}, - header_style={'backgroundColor': 'rgb(235, 235, 235)', 'borderTop': 'thin lightgrey solid', 'borderBottom': 'thin lightgrey solid'}, - header_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - cell_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - row_style={'paddingTop': 10, 'paddingBottom': 10} - ), - - html.Hr(), - - section_title('HTML Table - List Style with Minimal Headers'), - - dcc.Markdown(dedent(''' - In some contexts, the grey background can look a little heavy. - You can lighten this up by giving it a white background and - a thicker bottom border. - ''')), - - html_table( - df, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - cell_style={"paddingLeft": 5, "paddingRight": 5}, - header_style={'borderBottom': '2px lightgrey solid'}, - header_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - cell_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - row_style={'paddingTop': 10, 'paddingBottom': 10} - ), - - html.Hr(), - - section_title('HTML Table - List Style with Understated Headers'), - - dcc.Markdown(dedent(''' - When the data is obvious, sometimes you can de-emphasize the headers - as well, by giving them a lighter color than the cell text. - ''')), - - html_table( - df, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - cell_style={"paddingLeft": 5, "paddingRight": 5}, - header_style={'color': 'rgb(100, 100, 100)'}, - header_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - cell_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - row_style={'paddingTop': 10, 'paddingBottom': 10} - ), - - html.Hr(), - - section_title('HTML Table - Striped Rows'), - - dcc.Markdown(dedent(''' - When you're viewing datasets where you need to compare values within individual rows, it - can sometimes be helpful to give the rows alternating background colors. - We recommend using colors that are faded so as to not attract too much attention to the stripes. - ''')), - - html_table( - df, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - cell_style={"paddingLeft": 5, "paddingRight": 5}, - header_style={'backgroundColor': 'rgb(235, 235, 235)', 'borderTop': 'thin lightgrey solid', 'borderBottom': 'thin lightgrey solid'}, - header_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - cell_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - row_style={'paddingTop': 10, 'paddingBottom': 10}, - odd_row_style={'backgroundColor': 'rgb(248, 248, 248)'} - ), - - section_title('HTML Table - Dark Theme with Cells'), - - dcc.Markdown(dedent( - """ - You have full control over all of the elements in the table. - If you are viewing your table in an app with a dark background, - you can provide inverted background and font colors. - """ - )), - - html_table( - df, - table_style={ - 'backgroundColor': 'rgb(50, 50, 50)', - 'color': 'white', - 'width': '100%' - }, - cell_style={'border': 'thin white solid'}, - header_style={'backgroundColor': 'rgb(30, 30, 30)'}, - row_style={'padding': 10}, - header_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - cell_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - ), - - section_title('HTML Table - Dark Theme with Rows'), - - html_table( - df, - table_style={ - 'backgroundColor': 'rgb(50, 50, 50)', - 'color': 'white', - 'width': '100%' - }, - row_style={ - 'borderTop': 'thin white solid', - 'borderBottom': 'thin white solid', - 'padding': 10 - }, - header_style={'backgroundColor': 'rgb(30, 30, 30)'}, - header_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - cell_style_by_column={ - "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, - "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, - "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, - }, - ), - - section_title('HTML Table - Highlighting Certain Rows'), - - dcc.Markdown(dedent(''' - You can draw attention to certain rows by providing a unique - background color, bold text, or colored text. - ''')), - - html_table( - df, - cell_style={'border': 'thin lightgrey solid', 'color': 'rgb(60, 60, 60)'}, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - header_style={'backgroundColor': 'rgb(235, 235, 235)'}, - row_style_by_index={ - 4: { - 'backgroundColor': 'yellow', - } - } - ), - - section_title('HTML Table - Highlighting Certain Columns'), - - dcc.Markdown(dedent(''' - Similarly, certain columns can be highlighted. - ''')), - - html_table( - df, - cell_style={'border': 'thin lightgrey solid', 'color': 'rgb(60, 60, 60)'}, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - header_style={'backgroundColor': 'rgb(235, 235, 235)'}, - cell_style_by_column={ - "Temperature": { - "backgroundColor": "yellow" - }, - } - ), - - section_title('HTML Table - Highlighting Certain Cells'), - - dcc.Markdown(dedent(''' - You can also highlight certain cells. For example, you may want to - highlight certain cells that exceed a threshold or that match - a filter elsewhere in the app. - ''')), - - html_table( - df, - cell_style={'border': 'thin lightgrey solid', 'color': 'rgb(60, 60, 60)'}, - table_style={'width': '100%'}, - column_style={'width': '20%', 'paddingLeft': 20}, - header_style={'backgroundColor': 'rgb(235, 235, 235)'}, - conditional_cell_style=lambda cell, column: ( - {'backgroundColor': 'yellow'} - if ( - (column == 'Region' and cell == 'Montreal') - or - (cell == 20) - ) else {} - ) - ), - - html.Hr(), - - section_title('Dash Table - Styling the Table as a List'), - # ... - - section_title('Dash Table - Row Padding'), - dash_table.DataTable( - id="styling-2", - data=df.to_dict("rows"), - columns=[ - {"name": i, "id": i} for i in df.columns - ], - style_data_conditional=[{ "padding_bottom": 5, "padding_top": 5}] - ), - - section_title('Dash Table - List Style with Minimal Headers'), - # ... - - section_title('Dash Table - List Style with Understated Headers'), - # ... - - section_title('Dash Table - Striped Rows'), - # ... - - section_title('Dash Table - Dark Theme with Cells'), - dash_table.DataTable( - id="styling-6", - data=df.to_dict("rows"), - columns=[ - {"name": i, "id": i} for i in df.columns - ], - content_style="grow", - style_table={ - "width": "100%" - }, - style_data_conditional=[{ - "background_color": "rgb(50, 50, 50)", - "color": "white", - "font_family": "arial" - }, { - "if": { "column_id": "Humidity" }, - "font_family": "monospace", - "padding_left": 20, - "text_align": "left" - }, { - "if": { "column_id": "Pressure" }, - "font_family": "monospace", - "padding_left": 20, - "text_align": "left" - }, { - "if": { "column_id": "Temperature" }, - "font_family": "monospace", - "padding_left": 20, - "text_align": "left" - }] - ), - - section_title('Dash Table - Dark Theme with Rows'), - # ... - - section_title('Dash Table - Highlighting Certain Rows'), - # ... - - section_title('Dash Table - Highlighting Certain Columns'), - dash_table.DataTable( - id="styling-9", - data=df.to_dict("rows"), - columns=[ - {"name": i, "id": i} for i in df.columns - ], - content_style="grow", - style_table={ - "width": "100%" - }, - style_data_conditional=[{ - "color": "rgb(60, 60, 60)", - "padding_left": 20, - "text-align": "left", - "width": "20%" - }, { - "if": { "column_id": "Temperature" }, - "background_color": "yellow" - }] - ), - - section_title('Dash Table - Highlighting Certain Cells'), - dash_table.DataTable( - id="styling-10", - data=df.to_dict("rows"), - columns=[ - {"name": i, "id": i} for i in df.columns - ], - content_style="grow", - style_table={ - "width": "100%" - }, - style_data_conditional=[{ - "if": { "column_id": "Region", "filter": "{Region} eq Montreal" }, - "background_color": "yellow" - }, { - "if": { "column_id": "Humidity", "filter": "{Humidity} eq 20" }, - "background_color": "yellow" - }] - ) - ], - ) diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index b79e9c7cf..086956c12 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -1,3 +1,4 @@ +import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import DataTable from 'dash-table/dash/DataTable'; @@ -10,6 +11,23 @@ const setProps = () => { }; const fixtureStories = storiesOf('DashTable/Fixtures', module); fixtures.forEach(fixture => fixtureStories.add(fixture.name, () => ())); +const date = ['2015-01-01', '2015-10-24', '2016-05-10']; +const region = ['Montreal', 'Vermont', 'New York City']; +const temperature = [1, -20, 3.512]; +const humidity = [10, 20, 30]; +const pressure = [2, 10924, 3912]; + +const data: any[] = []; +for (let i = 0; i < 6; ++i) { + data.push({ + Date: date[i % date.length], + Region: region[i % region.length], + Temperature: temperature[i % temperature.length], + Humidity: humidity[i % humidity.length], + Pressure: pressure[i % pressure.length] + }); +} + storiesOf('DashTable/Style type condition', module) .add('with 1 column', () => ()) + .add('row padding', () => ( ({ name: i, id: i }), + R.keysIn(data[0])) + } + style_data_conditional={[{ + padding_bottom: 5, + padding_top: 5 + }]} + />)) + .add('dark theme with cells', () => ( ({ name: i, id: i }), + R.keysIn(data[0])) + } + content_style='grow' + style_table={{ + width: '100%' + }} + style_data_conditional={[{ + background_color: 'rgb(50, 50, 50)', + color: 'white', + font_family: 'arial' + }, { + if: { column_id: 'Humidity' }, + font_family: 'monospace', + padding_left: 20, + text_align: 'left' + }, { + if: { column_id: 'Pressure' }, + font_family: 'monospace', + padding_left: 20, + text_align: 'left' + }, { + if: { column_id: 'Temperature' }, + font_family: 'monospace', + padding_left: 20, + text_align: 'left' + }]} + />)) + .add('highlight columns', () => ( ({ name: i, id: i }), + R.keysIn(data[0])) + } + content_style='grow' + style_table={{ + width: '100%' + }} + style_data_conditional={[{ + color: 'rgb(60, 60, 60)', + padding_left: 20, + 'text-align': 'left', + width: '20%' + }, { + if: { column_id: 'Temperature' }, + background_color: 'yellow' + }]} + />)) + .add('highlight cells', () => ( ({ name: i, id: i }), + R.keysIn(data[0])) + } + content_style='grow' + style_table={{ + width: '100%' + }} + style_data_conditional={[{ + if: { column_id: 'Region', filter: '{Region} eq Montreal' }, + background_color: 'yellow' + }, { + if: { column_id: 'Humidity', filter: '{Humidity} eq 20' }, + background_color: 'yellow' + }]} + />)); \ No newline at end of file From 4e7ec15335d1cf1a60684621dc4ea740f4b2d2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 16:42:12 -0400 Subject: [PATCH 45/54] new virtualization test --- .config/webpack/base.js | 4 ++ package.json | 3 ++ tests/dash/app_virtualization.py | 27 ------------ .../percy-storybook/@Types/modules.d.ts | 6 +++ .../percy-storybook/Virtualization.percy.tsx | 43 +++++++++++++++++++ 5 files changed, 56 insertions(+), 27 deletions(-) delete mode 100644 tests/dash/app_virtualization.py create mode 100644 tests/visual/percy-storybook/Virtualization.percy.tsx diff --git a/.config/webpack/base.js b/.config/webpack/base.js index 32c6d61f0..362c631f9 100644 --- a/.config/webpack/base.js +++ b/.config/webpack/base.js @@ -43,6 +43,10 @@ module.exports = (preprocessor = {}, mode = 'development') => { test: /demo[/\\]index.html?$/, loader: 'file-loader?name=index.[ext]' }, + { + test: /\.csv$/, + loader: 'raw-loader' + }, { test: /\.ts(x?)$/, exclude: /node_modules/, diff --git a/package.json b/package.json index 5ab5410d4..93c1f639b 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@storybook/cli": "^5.0.5", "@storybook/react": "^5.0.5", "@types/d3-format": "^1.3.1", + "@types/papaparse": "^4.5.9", "@types/ramda": "^0.26.5", "@types/react": "^16.8.8", "@types/react-dom": "^16.8.3", @@ -69,7 +70,9 @@ "less-loader": "^4.1.0", "npm": "^6.9.0", "npm-run-all": "^4.1.5", + "papaparse": "^4.6.3", "ramda": "^0.26.1", + "raw-loader": "^2.0.0", "react": "16.8.5", "react-docgen": "^4.1.0", "react-dom": "16.8.5", diff --git a/tests/dash/app_virtualization.py b/tests/dash/app_virtualization.py deleted file mode 100644 index bc05b70f4..000000000 --- a/tests/dash/app_virtualization.py +++ /dev/null @@ -1,27 +0,0 @@ -import dash_table -import pandas as pd - -# Subset of https://www.irs.gov/pub/irs-soi/16zpallagi.csv -df = pd.read_csv("./datasets/16zpallagi-25cols-100klines.csv") -data = df.to_dict("rows") - -def layout(): - return dash_table.DataTable( - columns=[{"name": i, "id": i} for i in df.columns], - data=data, - pagination_mode=False, - virtualization=True, - editable=True, - n_fixed_rows=1, - style_table={ - "height": 800, - "max_height": 800, - "width": 1300, - "max_width": 1300 - }, - style_data={ - "width": 50, - "max_width": 50, - "min_width": 50 - } - ) diff --git a/tests/visual/percy-storybook/@Types/modules.d.ts b/tests/visual/percy-storybook/@Types/modules.d.ts index 287edccf7..7f343088b 100644 --- a/tests/visual/percy-storybook/@Types/modules.d.ts +++ b/tests/visual/percy-storybook/@Types/modules.d.ts @@ -1,5 +1,11 @@ declare var module: any; +declare module '*.csv' { + const value: any; + + export default value; +} + declare module '@storybook/react' { export const storiesOf: any; export const addDecorator: any; diff --git a/tests/visual/percy-storybook/Virtualization.percy.tsx b/tests/visual/percy-storybook/Virtualization.percy.tsx new file mode 100644 index 000000000..30d6d2b52 --- /dev/null +++ b/tests/visual/percy-storybook/Virtualization.percy.tsx @@ -0,0 +1,43 @@ +import parser from 'papaparse'; +import * as R from 'ramda'; +import React, { Fragment } from 'react'; +import { storiesOf } from '@storybook/react'; + +import dataset from './../../../datasets/16zpallagi-25cols-100klines.csv'; + +import DataTable from 'dash-table/dash/DataTable'; +import fixtures from './fixtures'; + +const setProps = () => { }; + +// Legacy: Tests previously run in Python +const fixtureStories = storiesOf('DashTable/Fixtures', module); +fixtures.forEach(fixture => fixtureStories.add(fixture.name, () => ())); + +const { data, meta } = parser.parse(dataset, { delimiter: ',', header: true }); +const columns = R.map(i => ({ name: i, id: i }), meta.fields); + +storiesOf('DashTable/Virtualization', module) + .add('default', () => ( + + )); \ No newline at end of file From 050fad7594350e566bc346b5997c5dcea0c74b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 16:48:47 -0400 Subject: [PATCH 46/54] rewrite header test --- tests/dash/app_multi_header.py | 32 ------------------- tests/visual/percy-storybook/Header.percy.tsx | 30 +++++++++++++++++ 2 files changed, 30 insertions(+), 32 deletions(-) delete mode 100644 tests/dash/app_multi_header.py create mode 100644 tests/visual/percy-storybook/Header.percy.tsx diff --git a/tests/dash/app_multi_header.py b/tests/dash/app_multi_header.py deleted file mode 100644 index bfa719584..000000000 --- a/tests/dash/app_multi_header.py +++ /dev/null @@ -1,32 +0,0 @@ -import dash_table -import pandas as pd - -sanitized_name = __name__.replace(".", "-") - -def layout(): - df = pd.read_csv("datasets/gapminder.csv") - return dash_table.DataTable( - id=sanitized_name, - columns=[ - {"name": ["Year", ""], "id": "year"}, - {"name": ["City", "Montreal"], "id": "montreal"}, - {"name": ["City", "Toronto"], "id": "toronto"}, - {"name": ["City", "Ottawa"], "id": "ottawa", "hidden": True}, - {"name": ["City", "Vancouver"], "id": "vancouver"}, - {"name": ["Climate", "Temperature"], "id": "temp"}, - {"name": ["Climate", "Humidity"], "id": "humidity"}, - ], - data=[ - { - "year": i, - "montreal": i * 10, - "toronto": i * 100, - "ottawa": i * -1, - "vancouver": i * -10, - "temp": i * -100, - "humidity": i * 0.1, - } - for i in range(100) - ], - merge_duplicate_headers=True, - ) diff --git a/tests/visual/percy-storybook/Header.percy.tsx b/tests/visual/percy-storybook/Header.percy.tsx new file mode 100644 index 000000000..e74780e43 --- /dev/null +++ b/tests/visual/percy-storybook/Header.percy.tsx @@ -0,0 +1,30 @@ +import * as R from 'ramda'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import DataTable from 'dash-table/dash/DataTable'; + +const setProps = () => { }; + +storiesOf('DashTable/Headers', module) + .add('multi header', () => ( ({ + year: i, + montreal: i * 10, + toronto: i * 100, + ottawa: i * -1, + vancouver: i * -10, + temp: i * -100, + humidity: i * 0.1 + }), R.range(0, 100))} + columns={[ + { name: ['Year', ''], id: 'year' }, + { name: ['City', 'Montreal'], id: 'montreal' }, + { name: ['City', 'Toronto'], id: 'toronto' }, + { name: ['City', 'Ottawa'], id: 'ottawa', hidden: true }, + { name: ['City', 'Vancouver'], id: 'vancouver' }, + { name: ['Climate', 'Temperature'], id: 'temp' }, + { name: ['Climate', 'Humidity'], id: 'humidity' } + ]} + />)); \ No newline at end of file From 491b586fecc9ac65c1379dc353fe1de7825e8f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 20:32:38 -0400 Subject: [PATCH 47/54] rewrite simple table test --- tests/dash/app_simple.py | 12 ------------ tests/visual/percy-storybook/DashTable.percy.tsx | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 12 deletions(-) delete mode 100644 tests/dash/app_simple.py diff --git a/tests/dash/app_simple.py b/tests/dash/app_simple.py deleted file mode 100644 index 134895151..000000000 --- a/tests/dash/app_simple.py +++ /dev/null @@ -1,12 +0,0 @@ -import dash_table -import pandas as pd - -sanitized_name = __name__.replace(".", "-") - -def layout(): - df = pd.read_csv("./datasets/gapminder.csv") - return dash_table.DataTable( - id=sanitized_name, - columns=[{"name": i, "id": i} for i in df.columns], - data=df.to_dict("rows"), - ) diff --git a/tests/visual/percy-storybook/DashTable.percy.tsx b/tests/visual/percy-storybook/DashTable.percy.tsx index c8e6fa184..a2ac626c1 100644 --- a/tests/visual/percy-storybook/DashTable.percy.tsx +++ b/tests/visual/percy-storybook/DashTable.percy.tsx @@ -1,6 +1,8 @@ +import parser from 'papaparse'; import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; + import random from 'core/math/random'; import DataTable from 'dash-table/dash/DataTable'; import fixtures from './fixtures'; @@ -11,6 +13,8 @@ const setProps = () => { }; const fixtureStories = storiesOf('DashTable/Fixtures', module); fixtures.forEach(fixture => fixtureStories.add(fixture.name, () => ())); +import dataset from './../../../datasets/gapminder.csv'; + storiesOf('DashTable/Without Data', module) .add('with 1 column', () => ()); storiesOf('DashTable/With Data', module) + .add('simple', () => { + const result = parser.parse(dataset, { delimiter: ',', header: true }); + + return ( ({ name: i, id: i }), + result.meta.fields) + } + />); + }) .add('with 3 columns and 3 rows, not actionable', () => ( Date: Mon, 15 Apr 2019 21:22:40 -0400 Subject: [PATCH 48/54] rewrite dropdown tests --- tests/dash/app_dropdown.py | 190 ------------------ .../visual/percy-storybook/Dropdown.percy.tsx | 105 ++++++++++ tests/visual/percy-storybook/Style.percy.tsx | 5 - .../percy-storybook/Virtualization.percy.tsx | 51 ++--- 4 files changed, 127 insertions(+), 224 deletions(-) delete mode 100644 tests/dash/app_dropdown.py create mode 100644 tests/visual/percy-storybook/Dropdown.percy.tsx diff --git a/tests/dash/app_dropdown.py b/tests/dash/app_dropdown.py deleted file mode 100644 index e9fbb6f89..000000000 --- a/tests/dash/app_dropdown.py +++ /dev/null @@ -1,190 +0,0 @@ -from collections import OrderedDict -from dash.dependencies import Input, Output -import dash_core_components as dcc -import dash_html_components as html -import pandas as pd -from textwrap import dedent - -import dash_table -from index import app -from .utils import section_title - - -ID_PREFIX = "app_dropdown" -IDS = { - "dropdown": ID_PREFIX, - "dropdown-by-cell": '{}-row-by-cell'.format(ID_PREFIX), - "dropdown-by-cell-deprecated": '{}-deprecated-row-by-cell'.format(ID_PREFIX) -} - - -df = pd.DataFrame(OrderedDict([ - ('climate', - ['Sunny', 'Snowy', 'Sunny', 'Rainy']), - ('temperature', - [13, 43, 50, 30]), - ('city', - ['NYC', 'Montreal', 'Miami', 'NYC']) -])) - -df_per_row_dropdown = pd.DataFrame(OrderedDict([ - ('City', - ['NYC', 'Montreal', 'Los Angeles']), - ('Neighborhood', - ['Brooklyn', 'Mile End', 'Venice']), - ('Temperature (F)', - [70, 60, 90]), -])) - - -def layout(): - return html.Div([ - - dcc.Markdown(dedent(''' - The Dash table includes support for per-column and - per-cell dropdowns. In future releases, this will - be tightly integrated with a more formal typing system. - - For now, use the dropdown renderer as a way to limit the - options available when editing the values with an editable table. - - ''')), - - section_title('Dash Table with Per-Column Dropdowns'), - - dash_table.DataTable( - id=IDS['dropdown'], - data=df.to_dict('rows'), - columns=[ - {'id': 'climate', 'name': 'climate'}, - {'id': 'temperature', 'name': 'temperature'}, - {'id': 'city', 'name': 'city'}, - ], - - editable=True, - column_static_dropdown=[ - { - 'id': 'climate', - 'dropdown': [ - {'label': i, 'value': i} - for i in df['climate'].unique() - ] - }, - { - 'id': 'city', - 'dropdown': [ - {'label': i, 'value': i} - for i in df['city'].unique() - ] - }, - ] - ), - - section_title('Dash Table with Per-Cell Dropdowns via Filtering UI'), - - dash_table.DataTable( - id=IDS['dropdown-by-cell'], - data=df_per_row_dropdown.to_dict('rows'), - columns=[ - {'id': c, 'name': c} - for c in df_per_row_dropdown.columns - ], - - editable=True, - column_conditional_dropdowns=[ - { - 'id': 'Neighborhood', - 'dropdowns': [ - - { - 'condition': '{City} eq "NYC"', - 'dropdown': [ - {'label': i, 'value': i} - for i in [ - 'Brooklyn', - 'Queens', - 'Staten Island' - ] - ] - }, - - { - 'condition': '{City} eq "Montreal"', - 'dropdown': [ - {'label': i, 'value': i} - for i in [ - 'Mile End', - 'Plateau', - 'Hochelaga' - ] - ] - }, - - { - 'condition': '{City} eq "Los Angeles"', - 'dropdown': [ - {'label': i, 'value': i} - for i in [ - 'Venice', - 'Hollywood', - 'Los Feliz' - ] - ] - } - - ] - } - ] - ), - - section_title('Dash Table with Per-Cell Dropdowns'), - - html.Div('This example uses a deprecated API, `dropdown_properties`.'), - - dash_table.DataTable( - id=IDS['dropdown-by-cell-deprecated'], - data=df_per_row_dropdown.to_dict('rows'), - columns=[ - {'id': c, 'name': c} - for c in df_per_row_dropdown.columns - ], - - editable=True, - dropdown_properties=[ - { - 'options': [ - {'label': i, 'value': i} - for i in [ - 'Brooklyn', - 'Queens', - 'Staten Island' - ] - ] - }, - - { - 'options': [ - {'label': i, 'value': i} - for i in [ - 'Mile End', - 'Plateau', - 'Hochelaga' - ] - ] - }, - - { - 'options': [ - {'label': i, 'value': i} - for i in [ - 'Venice', - 'Hollywood', - 'Los Feliz' - ] - ] - }, - ] - - ), - - ]) diff --git a/tests/visual/percy-storybook/Dropdown.percy.tsx b/tests/visual/percy-storybook/Dropdown.percy.tsx new file mode 100644 index 000000000..4770e5ada --- /dev/null +++ b/tests/visual/percy-storybook/Dropdown.percy.tsx @@ -0,0 +1,105 @@ +import * as R from 'ramda'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import DataTable from 'dash-table/dash/DataTable'; + +const setProps = () => { }; + +const data = [ + { climate: 'Sunny', temperature: 13, city: 'NYC' }, + { climate: 'Snowy', temperature: 43, city: 'Montreal' }, + { climate: 'Sunny', temperature: 50, city: 'Miami' }, + { climate: 'Rainy', temperature: 30, city: 'NYC' } +]; + +const data2 = [ + { City: 'NYC', Neighborhood: 'Brooklyn', 'Temperature (F)': 70 }, + { City: 'Montreal', Neighborhood: 'Mile End', 'Temperature (F)': 60 }, + { City: 'Los Angeles', Neighborhood: 'Venice', 'Temperature (F)': 90 } +]; + +const columns = R.map( + i => ({ name: i, id: i, presentation: 'dropdown' }), + ['climate', 'temperature', 'city'] +); + +const columns2 = R.map( + i => ({ name: i, id: i, presentation: 'dropdown' }), + ['City', 'Neighborhood', 'Temperature (F)'] +); + +storiesOf('DashTable/Dropdown', module) + .add('dropdown by column', () => ( ({ label: i, value: i }), + ['Sunny', 'Snowy', 'Rainy'] + ) + }, { + id: 'city', + dropdown: R.map( + i => ({ label: i, value: i }), + ['NYC', 'Montreal', 'Miami'] + ) + }]} + />)) + .add('dropdown by filtering', () => ( ({ label: i, value: i }), + ['Brooklyn', 'Queens', 'Staten Island'] + ) + }, { + condition: '{City} eq "Montreal"', + dropdown: R.map( + i => ({ label: i, value: i }), + ['Mile End', 'Plateau', 'Hochelaga'] + ) + }, { + condition: '{City} eq "Los Angeles"', + dropdown: R.map( + i => ({ label: i, value: i }), + ['Venice', 'Hollywood', 'Los Feliz'] + ) + }] + }]} + />)).add('dropdown by cell (deprecated)', () => ( ({ label: i, value: i }), + ['Brooklyn', 'Queens', 'Staten Island'] + ) + }, { + options: R.map( + i => ({ label: i, value: i }), + ['Mile End', 'Plateau', 'Hochelaga'] + ) + }, { + options: R.map( + i => ({ label: i, value: i }), + ['Venice', 'Hollywood', 'Los Feliz'] + ) + }] + }} + />)); \ No newline at end of file diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 086956c12..527dd3abe 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -2,15 +2,10 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import DataTable from 'dash-table/dash/DataTable'; -import fixtures from './fixtures'; import { ColumnType } from 'dash-table/components/Table/props'; const setProps = () => { }; -// Legacy: Tests previously run in Python -const fixtureStories = storiesOf('DashTable/Fixtures', module); -fixtures.forEach(fixture => fixtureStories.add(fixture.name, () => ())); - const date = ['2015-01-01', '2015-10-24', '2016-05-10']; const region = ['Montreal', 'Vermont', 'New York City']; const temperature = [1, -20, 3.512]; diff --git a/tests/visual/percy-storybook/Virtualization.percy.tsx b/tests/visual/percy-storybook/Virtualization.percy.tsx index 30d6d2b52..d197da826 100644 --- a/tests/visual/percy-storybook/Virtualization.percy.tsx +++ b/tests/visual/percy-storybook/Virtualization.percy.tsx @@ -1,43 +1,36 @@ import parser from 'papaparse'; import * as R from 'ramda'; -import React, { Fragment } from 'react'; +import React from 'react'; import { storiesOf } from '@storybook/react'; import dataset from './../../../datasets/16zpallagi-25cols-100klines.csv'; import DataTable from 'dash-table/dash/DataTable'; -import fixtures from './fixtures'; const setProps = () => { }; -// Legacy: Tests previously run in Python -const fixtureStories = storiesOf('DashTable/Fixtures', module); -fixtures.forEach(fixture => fixtureStories.add(fixture.name, () => ())); - const { data, meta } = parser.parse(dataset, { delimiter: ',', header: true }); const columns = R.map(i => ({ name: i, id: i }), meta.fields); storiesOf('DashTable/Virtualization', module) - .add('default', () => ( - - )); \ No newline at end of file + .add('default', () => ()); \ No newline at end of file From 15e3df4e0b12a0cadff6f3f96566580706f182c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Apr 2019 21:42:03 -0400 Subject: [PATCH 49/54] rewrite sizing tests --- tests/dash/app_sizing.py | 432 ------------------ tests/visual/percy-storybook/Sizing.percy.tsx | 83 ++++ 2 files changed, 83 insertions(+), 432 deletions(-) delete mode 100644 tests/dash/app_sizing.py create mode 100644 tests/visual/percy-storybook/Sizing.percy.tsx diff --git a/tests/dash/app_sizing.py b/tests/dash/app_sizing.py deleted file mode 100644 index 1657a2e10..000000000 --- a/tests/dash/app_sizing.py +++ /dev/null @@ -1,432 +0,0 @@ -from collections import OrderedDict -from dash.dependencies import Input, Output -import dash_core_components as dcc -import dash_html_components as html -import pandas as pd -from textwrap import dedent - -import dash_table -from index import app -from .utils import section_title, html_table - - -def layout(): - data = OrderedDict( - [ - ( - "Date", - [ - "July 12th, 2013 - July 25th, 2013", - "July 12th, 2013 - August 25th, 2013", - "July 12th, 2014 - August 25th, 2014", - ], - ), - ( - "Election Polling Organization", - ["The New York Times", "Pew Research", "The Washington Post"], - ), - ("Rep", [1, -20, 3.512]), - ("Dem", [10, 20, 30]), - ("Ind", [2, 10924, 3912]), - ( - "Region", - [ - "Northern New York State to the Southern Appalachian Mountains", - "Canada", - "Southern Vermont", - ], - ), - ] - ) - - df = pd.DataFrame(data) - df_long = pd.DataFrame( - OrderedDict([(name, col_data * 10) for (name, col_data) in data.items()]) - ) - - return [ - html.H1("Sizing Guide"), - html.Div( - style={ - "marginLeft": "auto", - "marginRight": "auto", - "width": "80%", - "borderLeft": "thin hotpink solid", - "borderRight": "thin hotpink solid", - }, - children=[ - html.H1("Background - HTML Tables"), - section_title("HTML Table - Default Styles"), - html.Div("By default, HTML tables expand to their contents"), - html_table(df, table_style={}, base_column_style={}), - section_title("HTML Table - Padding"), - html.Div( - """ - Since the table content is packed so tightly, - it's usually a good idea to place some left - on the columns. - """ - ), - html_table(df, table_style={}, cell_style={"paddingLeft": 10}), - section_title("HTML Table - Responsive Table"), - html.Div( - """ - With 100% width, the tables will expand to their - container. When the table gets small, the text will break into - multiple lines. - """ - ), - html_table(df, table_style={"width": "100%"}, base_column_style={}), - section_title("HTML Table - All Column Widths defined by Percent"), - html.Div( - """ - The column widths can be definied by percents rather than pixels. - """ - ), - html_table( - df, - table_style={"width": "100%"}, - column_style={ - "Date": {"width": "30%"}, - "Election Polling Organization": {"width": "25%"}, - "Dem": {"width": "5%"}, - "Rep": {"width": "5%"}, - "Ind": {"width": "5%"}, - "Region": {"width": "30%"}, - }, - ), - section_title("HTML Table - Single Column Width Defined by Percent"), - html.Div( - """ - The width of one column (Region=50%) can be definied by percent. - """ - ), - html_table( - df, - table_style={"width": "100%"}, - column_style={"Region": {"width": "50%"}}, - ), - section_title("HTML Table - Columns with min-width"), - html.Div( - "Here, the min-width for the first column is 130px, or about the width of this line: " - ), - html.Div( - style={"width": 130, "height": 10, "backgroundColor": "hotpink"} - ), - html_table( - df, - table_style={"width": "100%"}, - column_style={"Date": {"minWidth": "130"}}, - ), - section_title("HTML Table - Underspecified Widths"), - html.Div( - """ - The widths can be under-specified. Here, we're only setting the width for the three - columns in the middle, the rest of the columns are automatically sized to fit the rest of the container. - The columns have a width of 50px, or the width of this line: - """ - ), - html.Div( - style={"width": 50, "height": 10, "backgroundColor": "hotpink"} - ), - html_table( - df, - table_style={"width": "100%"}, - column_style={ - "Dem": {"width": 50}, - "Rep": {"width": 50}, - "Ind": {"width": 50}, - }, - ), - section_title("HTML Table - Widths that are smaller than the content"), - html.Div( - """ - In this case, we're setting the width to 20px, which is smaller - than the "10924" number in the "Ind" column. - The table does not allow it. - """ - ), - html.Div( - style={"width": 20, "height": 10, "backgroundColor": "hotpink"} - ), - html_table( - df, - table_style={"width": "100%"}, - column_style={ - "Dem": {"width": 20}, - "Rep": {"width": 20}, - "Ind": {"width": 20}, - }, - ), - section_title("HTML Table - Content with Ellipses"), - html.Div( - """ - With `max-width`, the content can collapse into - ellipses once the content doesn't fit. - - Here, `max-width` is set to 0. It could be any number, the only - important thing is that it is supplied. The behaviour will be - the same whether it is 0 or 50. - """ - ), - html_table( - df, - table_style={"width": "100%"}, - cell_style={ - "whiteSpace": "nowrap", - "overflow": "hidden", - "textOverflow": "ellipsis", - "maxWidth": 0, - }, - ), - section_title("HTML Table - Vertical Scrolling"), - html.Div( - """ - By supplying a max-height of the Table container and supplying - `overflow-y: scroll`, the table will become scrollable if the - table's contents are larger than the container. - """ - ), - html.Div( - style={"maxHeight": 300, "overflowY": "scroll"}, - children=html_table(df_long, table_style={"width": "100%"}), - ), - section_title("HTML Table - Vertical Scrolling with Max Height"), - html.Div( - """ - With `max-height`, if the table's contents are shorter than the - `max-height`, then the container will be shorter. - If you want a container with a constant height no matter the - contents, then use `height`. - - Here, we're setting max-height to 300, or the height of this line: - """ - ), - html.Div( - style={"width": 5, "height": 300, "backgroundColor": "hotpink"} - ), - html.Div( - style={"maxHeight": 300, "overflowY": "scroll"}, - children=html_table(df, table_style={"width": "100%"}), - ), - section_title("HTML Table - Vertical Scrolling with Height"), - html.Div("and here is `height` with the same content"), - html.Div( - style={"height": 300, "overflowY": "scroll"}, - children=html_table(df, table_style={"width": "100%"}), - ), - section_title("HTML Table - Horizontal Scrolling"), - html.Div( - """ - With HTML tables, we can set `min-width` to be 100%. - If the content is small, then the columns will have some extra - space. - But if the content of any of the cells is really large, then the - cells will expand beyond the container and a scrollbar will appear. - - In this way, `min-width` and `overflow-x: scroll` is an alternative - to `text-overflow: ellipses`. With scroll, the content that can't - fit in the container will get pushed out into a scrollable zone. - With text-overflow: ellipses, the content will get truncated by - ellipses. Both strategies work with or without line breaks on the - white spaces (`white-space: normal` or `white-space: nowrap`). - - These next two examples have the same styles applied: - - `min-width: 100%` - - `white-space: nowrap` (to keep the content on a single line) - - A parent with `overflow-x: scroll` - - """ - ), - section_title("HTML Table - Two Columns, 100% Min-Width"), - html.Div( - html_table( - pd.DataFrame({"Column 1": [1, 2], "Column 2": [3, 3]}), - table_style={"minWidth": "100%"}, - cell_style={"whiteSpace": "nowrap"}, - ), - style={"overflowX": "scroll"}, - ), - section_title("HTML Table - Long Columns, 100% Min-Width"), - html.Div( - """ - Here is a table with several columns with long titles, - 100% min-width, and `'white-space': 'nowrap'` - (to keep the text on a single line) - """ - ), - html.Div( - html_table( - pd.DataFrame( - { - "This is Column {} Data".format(i): [1, 2] - for i in range(10) - } - ), - table_style={"minWidth": "100%", "overflowX": "scroll"}, - cell_style={"whiteSpace": "nowrap"}, - ), - style={"overflowX": "scroll"}, - ), - html.Hr(), - html.H3("Dash Interactive Table"), - html.Div("These same styles can be applied to the dash table"), - section_title("Dash Table - Default Styles"), - dash_table.DataTable( - id="sizing-1", - data=df.to_dict("rows"), - columns=[{"name": i, "id": i} for i in df.columns], - ), - section_title("Dash Table - Padding"), - # ... - section_title("Dash Table - All Column Widths by Percent"), - html.Div( - """ - Here is a table with all columns having width equal to 16.67%, - the Region column additionally wraps text. The table will try and respect - the width of each column while allowing for the content to be displayed. - - Changing the browser's viewport width will help understand how the table - allocates space. - """ - ), - dash_table.DataTable( - id="sizing-2", - data=df.to_dict("rows"), - content_style="grow", - columns=[ - {"name": i, "id": i} for i in df.columns - ], - css=[ - {"selector": ".dash-spreadsheet", "rule": "width: 100%"}, - { - "selector": ".dash-cell[data-dash-column=Region]", - "rule": "white-space: normal", - }, - ], - style_data_conditional=[{"width": "16.67%"}] - ), - section_title("Dash Table - Single Column Width by Percent"), - html.Div( - """ - Here is a table with all columns having default (auto) width excepts for the - the Region column that has 50% width and wraps text. The table will try and respect - the width of each column while allowing for the content to be displayed. - - Changing the browser's viewport width will help understand how the table - allocates space. - """ - ), - dash_table.DataTable( - id="sizing-3", - data=df.to_dict("rows"), - content_style="grow", - columns=[ - {"name": i, "id": i } - for i in df.columns - ], - css=[ - {"selector": ".dash-spreadsheet", "rule": "width: 100%"}, - { - "selector": ".dash-cell[data-dash-column=Region]", - "rule": "white-space: normal", - }, - ], - style_data_conditional=[{ "if": { "column_id": "Region" }, "width": "50%" }] - ), - section_title("Dash Table - Underspecified Widths"), - html.Div( - """ - The widths can be under-specified. Here, we're only setting the width for the three - columns in the middle, the rest of the columns are automatically sized to fit the rest of the container. - The columns have a width/minWidth/maxWidth of 100px. - """ - ), - dash_table.DataTable( - id="sizing-4", - data=df.to_dict("rows"), - columns=[ - { - "name": i, - "id": i, - } - for i in df.columns - ], - style_data_conditional=[ - { "if": { "column_id": "Dem" }, "width": "100px", "min_width": "100px", "max_width": "100px" }, - { "if": { "column_id": "Rep" }, "width": "100px", "min_width": "100px", "max_width": "100px" }, - { "if": { "column_id": "Ind" }, "width": "100px", "min_width": "100px", "max_width": "100px" } - ] - ), - section_title("Dash Table - Widths that are smaller than the content"), - html.Div( - """ - Width for all columns is set to 100px. Columns whose content is smaller than the defined size will respect it. - Columns whose content is bigger than defined will grow to accomodate content. Region column wraps to show behavior - in that case - """ - ), - dash_table.DataTable( - id="sizing-5", - data=df.to_dict("rows"), - columns=[ - {"name": i, "id": i } for i in df.columns - ], - css=[ - { - "selector": ".dash-cell[data-dash-column=Region]", - "rule": "white-space: normal", - } - ], - style_data_conditional=[{ "width": "100px" }] - ), - section_title( - "Dash Table - Widths that are smaller than the content (forced)" - ), - html.Div( - """ - Width/minWidth/maxWidth for all columns is set to 100px. Columns whose content is smaller than the defined size will respect it. - Columns whose content is bigger than defined will respect it too. Region column wraps to show behavior - in that case - """ - ), - dash_table.DataTable( - id="sizing-6", - data=df.to_dict("rows"), - columns=[ - { - "name": i, - "id": i, - } - for i in df.columns - ], - css=[ - { - "selector": ".dash-cell[data-dash-column=Region]", - "rule": "white-space: normal", - } - ], - content_style="fit", - style_data_conditional=[ - { "width": "100px", "min_width": "100px", "max_width": "100px" } - ] - ), - section_title("Dash Table - Content with Ellipses"), - # ... - section_title("Dash Table - Vertical Scrolling"), - # ... - section_title("Dash Table - Vertical Scrolling with Max Height"), - # ... - section_title("Dash Table - Vertical Scrolling with Height"), - # ... - section_title("Dash Table - Horizontal Scrolling"), - # ... - section_title("Dash Table - Two Columns, 100% Min-Width"), - # ... - section_title("Dash Table - Long Columns, 100% Min-Width"), - # ... - section_title("Dash Table - Alignment"), - # ... - ], - ), - ] diff --git a/tests/visual/percy-storybook/Sizing.percy.tsx b/tests/visual/percy-storybook/Sizing.percy.tsx new file mode 100644 index 000000000..338ced890 --- /dev/null +++ b/tests/visual/percy-storybook/Sizing.percy.tsx @@ -0,0 +1,83 @@ +import * as R from 'ramda'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import DataTable from 'dash-table/dash/DataTable'; + +const setProps = () => { }; + +const data = [ + { Date: 'July 12th, 2013 - July 25th, 2013', Rep: 1, Dem: 10, Ind: 2, Region: 'Northern New York State to the Southern Appalachian Mountains', 'Election Polling Organization': 'The New York Times' }, + { Date: 'July 12th, 2013 - August 25th, 2013', Rep: -20, Dem: 20, Ind: 10924, Region: 'Canada', 'Election Polling Organization': 'Pew Research' }, + { Date: 'July 12th, 2014 - August 25th, 2014', Rep: 3.512, Dem: 30, Ind: 3912, Region: 'Southern Vermont', 'Election Polling Organization': 'The Washington Post' } +]; + +const columns = R.map( + i => ({ name: i, id: i }), + ['Date', 'Rep', 'Dem', 'Ind', 'Region', 'Election Polling Organization'] +); + +const props = { + setProps, + id: 'table', + data, + columns +}; + +storiesOf('DashTable/Sizing', module) + .add('default styles', () => ()) + .add('padding', () => ()) + .add('single column width by percentage', () => ()) + .add('underspecified widths', () => ()) + .add('widths smaller than content', () => ()) + .add('widths smaller than content (forced)', () => ()); \ No newline at end of file From 07185372c1b444871c37c9c3b1f1fb1e73eed849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 16 Apr 2019 10:27:54 -0400 Subject: [PATCH 50/54] add tests for alt whitespace characters trimming --- .../tests/unit/query_syntactic_tree_test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index c3377a917..1bf270648 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -1,3 +1,5 @@ +import * as R from 'ramda'; + import { QuerySyntaxTree } from 'dash-table/syntax-tree'; describe('Query Syntax Tree', () => { @@ -6,6 +8,27 @@ describe('Query Syntax Tree', () => { const data2 = { a: '2', b: '1', c: 2, d: '', '\\{': 2, 'a.dot': '2.dot', 'a-dot': '2-dot', a_dot: '2_dot', 'a+dot': '2+dot', 'a dot': '2 dot', 'a:dot': '2:dot', '_-6.:+** *@$': '2*dot', '{a:dot}': '2*dot*', '\'""\'': '2\'"dot' }; const data3 = { a: '3', b: '1', c: 3, d: false, '\\{': 3, 'a.dot': '3.dot', 'a-dot': '3-dot', a_dot: '3_dot', 'a+dot': '3+dot', 'a dot': '3 dot', 'a:dot': '3:dot', '_-6.:+** *@$': '3*dot', '{a:dot}': '3*dot*', '\'""\'': '3\'"dot' }; + describe('special whitespace characters are valid', () => { + const cases = [ + { name: 'suports new line', query: '{a}\neq\n"0"' }, + { name: 'suports carriage return', query: '{a}\req\r"0"' }, + { name: 'suports new line ad carriage return combination', query: '{a}\r\neq\r\n"0"' }, + { name: 'supports tab', query: '{a}\teq\t"0"' }, + // some random non-standard whitespace character from https://en.wikipedia.org/wiki/Whitespace_character + { name: 'supports ogham space mark', query: '{a}\u1680eq\u1680"0"' }, + { name: 'supports all', query: '{a}\r\n\t\u1680eq\r\n\t\u1680"0"' } + ]; + + R.forEach(c => { + it(c.name, () => { + const tree = new QuerySyntaxTree(c.query); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate(data0)).to.equal(true); + }); + }, cases); + }); + describe('operands', () => { it('does not support badly formed operands', () => { expect(new QuerySyntaxTree(`{myField} eq 0`).isValid).to.equal(true); From d69b98db03b55c7f0d669adbe426798830c66646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 16 Apr 2019 13:48:42 -0400 Subject: [PATCH 51/54] - fix typo (GreaterThan) - add `contains` relational operator to all lexicons - add `contains` test --- src/dash-table/syntax-tree/lexeme/relational.ts | 15 +++++++++++++-- src/dash-table/syntax-tree/lexicon/column.ts | 4 +++- src/dash-table/syntax-tree/lexicon/columnMulti.ts | 4 +++- src/dash-table/syntax-tree/lexicon/query.ts | 4 +++- .../tests/unit/query_syntactic_tree_test.ts | 8 ++++++++ 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts index f3c411079..df6fb9a78 100644 --- a/src/dash-table/syntax-tree/lexeme/relational.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -30,9 +30,10 @@ function relationalEvaluator( } export enum RelationalOperator { + Contains = 'contains', Equal = '=', GreaterOrEqual = '>=', - GreatherThan = '>', + GreaterThan = '>', LessOrEqual = '<=', LessThan = '<', NotEqual = '!=' @@ -44,6 +45,16 @@ const LEXEME_BASE = { type: LexemeType.RelationalOperator }; +export const contains: IUnboundedLexeme = R.merge({ + evaluate: relationalEvaluator(([op, exp]) => + typeof op === 'string' && + typeof exp === 'string' && + op.indexOf(exp) !== -1 + ), + subType: RelationalOperator.Contains, + regexp: /^(contains)/i +}, LEXEME_BASE); + export const equal: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op === exp), subType: RelationalOperator.Equal, @@ -58,7 +69,7 @@ export const greaterOrEqual: IUnboundedLexeme = R.merge({ export const greaterThan: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => op > exp), - subType: RelationalOperator.GreatherThan, + subType: RelationalOperator.GreaterThan, regexp: /^(>|gt)/i }, LEXEME_BASE); diff --git a/src/dash-table/syntax-tree/lexicon/column.ts b/src/dash-table/syntax-tree/lexicon/column.ts index e8911ca78..b6e42ec6d 100644 --- a/src/dash-table/syntax-tree/lexicon/column.ts +++ b/src/dash-table/syntax-tree/lexicon/column.ts @@ -6,6 +6,7 @@ import { valueExpression } from '../lexeme/expression'; import { + contains, equal, greaterOrEqual, greaterThan, @@ -28,7 +29,8 @@ import { LexemeType, ILexeme } from 'core/syntax-tree/lexicon'; import { ILexemeResult } from 'core/syntax-tree/lexer'; const lexicon: ILexeme[] = [ - ...[equal, + ...[contains, + equal, greaterOrEqual, greaterThan, lessOrEqual, diff --git a/src/dash-table/syntax-tree/lexicon/columnMulti.ts b/src/dash-table/syntax-tree/lexicon/columnMulti.ts index 75373c8d7..a8542b5bc 100644 --- a/src/dash-table/syntax-tree/lexicon/columnMulti.ts +++ b/src/dash-table/syntax-tree/lexicon/columnMulti.ts @@ -10,6 +10,7 @@ import { } from '../lexeme/logical'; import operand from '../lexeme/operand'; import { + contains, equal, greaterOrEqual, greaterThan, @@ -53,7 +54,8 @@ const lexicon: ILexeme[] = [ ), terminal: false }, - ...[equal, + ...[contains, + equal, greaterOrEqual, greaterThan, lessOrEqual, diff --git a/src/dash-table/syntax-tree/lexicon/query.ts b/src/dash-table/syntax-tree/lexicon/query.ts index ffd43e66c..8b23c1b63 100644 --- a/src/dash-table/syntax-tree/lexicon/query.ts +++ b/src/dash-table/syntax-tree/lexicon/query.ts @@ -15,6 +15,7 @@ import { } from '../lexeme/logical'; import operand from '../lexeme/operand'; import { + contains, equal, greaterOrEqual, greaterThan, @@ -116,7 +117,8 @@ const lexicon: ILexeme[] = [ ), terminal: false }, - ...[equal, + ...[contains, + equal, greaterOrEqual, greaterThan, lessOrEqual, diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index 1bf270648..9de623bd9 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -567,5 +567,13 @@ describe('Query Syntax Tree', () => { expect(tree.evaluate(data2)).to.equal(false); expect(tree.evaluate(data3)).to.equal(false); }); + + it('can do contains (contains) test', () => { + const tree = new QuerySyntaxTree('{a} contains v'); + + expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 'abc v' })).to.equal(true); + expect(tree.evaluate({ a: 'abc w' })).to.equal(false); + }); }); }); \ No newline at end of file From 3f96b5be4b85c521dae233c7a6ef58a893c07cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 16 Apr 2019 15:03:38 -0400 Subject: [PATCH 52/54] escape `\` --- src/dash-table/syntax-tree/lexeme/expression.ts | 8 ++++---- src/dash-table/syntax-tree/lexeme/operand.ts | 4 ++-- .../tests/unit/query_syntactic_tree_test.ts | 16 ++++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/dash-table/syntax-tree/lexeme/expression.ts b/src/dash-table/syntax-tree/lexeme/expression.ts index e6ec412a1..8dad633ee 100644 --- a/src/dash-table/syntax-tree/lexeme/expression.ts +++ b/src/dash-table/syntax-tree/lexeme/expression.ts @@ -5,8 +5,8 @@ import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; import operand from './operand'; -const STRING_REGEX = /^(('([^'\\]|\\'|\\)+')|("([^"\\]|\\"|\\)+")|(`([^`\\]|\\`|\\)+`))/; -const VALUE_REGEX = /^(([^\s'"`{}()\\]|\\[\s'"`{}()]?)+)(?:[\s)]|$)/; +const STRING_REGEX = /^(('([^'\\]|\\.)+')|("([^"\\]|\\.)+")|(`([^`\\]|\\.)+`))/; +const VALUE_REGEX = /^(([^\s'"`{}()\\]|\\.)+)(?:[\s)]|$)/; export const fieldExpression: IUnboundedLexeme = R.merge( operand, { @@ -17,7 +17,7 @@ export const fieldExpression: IUnboundedLexeme = R.merge( const getString = ( value: string -) => value.slice(1, value.length - 1).replace(/\\(['"`])/g, '$1'); +) => value.slice(1, value.length - 1).replace(/\\(.)/g, '$1'); const getValue = ( value: string @@ -26,7 +26,7 @@ const getValue = ( return isNumeric(value) ? +value : - value.replace(/\\([\s'"`{}()])/g, '$1'); + value.replace(/\\(.)/g, '$1'); }; export const stringExpression: IUnboundedLexeme = { diff --git a/src/dash-table/syntax-tree/lexeme/operand.ts b/src/dash-table/syntax-tree/lexeme/operand.ts index b24fbf669..5fe72feea 100644 --- a/src/dash-table/syntax-tree/lexeme/operand.ts +++ b/src/dash-table/syntax-tree/lexeme/operand.ts @@ -1,13 +1,13 @@ import { LexemeType, IUnboundedLexeme } from 'core/syntax-tree/lexicon'; import { ISyntaxTree } from 'core/syntax-tree/syntaxer'; -const FIELD_REGEX = /^{(([^{}\\]|\\{|\\}|\\)+)}/; +const FIELD_REGEX = /^{(([^{}\\]|\\.)+)}/; const getField = ( value: string ) => value .slice(1, value.length - 1) - .replace(/\\([{}])/g, '$1'); + .replace(/\\(.)/g, '$1'); const operand: IUnboundedLexeme = { present: (tree: ISyntaxTree) => getField(tree.value), diff --git a/tests/cypress/tests/unit/query_syntactic_tree_test.ts b/tests/cypress/tests/unit/query_syntactic_tree_test.ts index 9de623bd9..7fb011b0e 100644 --- a/tests/cypress/tests/unit/query_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/query_syntactic_tree_test.ts @@ -40,18 +40,22 @@ describe('Query Syntax Tree', () => { expect(new QuerySyntaxTree(`{myField eq 0`).isValid).to.equal(false); expect(new QuerySyntaxTree(`myField} eq 0`).isValid).to.equal(false); expect(new QuerySyntaxTree(`myField eq 0`).isValid).to.equal(false); + expect(new QuerySyntaxTree('{\\\\{myField\\\\}} eq 0').isValid).to.equal(false); }); it('does not support badly formed expression', () => { expect(new QuerySyntaxTree(`{myField} eq 'value'`).isValid).to.equal(true); expect(new QuerySyntaxTree(`{myField} eq "value"`).isValid).to.equal(true); expect(new QuerySyntaxTree('{myField} eq `value`').isValid).to.equal(true); - expect(new QuerySyntaxTree(`{myField} eq 'value\\'`).isValid).to.equal(true); + expect(new QuerySyntaxTree(`{myField} eq 'value\\\\'`).isValid).to.equal(true); expect(new QuerySyntaxTree(`{myField} eq 'value\\''`).isValid).to.equal(true); - expect(new QuerySyntaxTree(`{myField} eq "value\\"`).isValid).to.equal(true); - expect(new QuerySyntaxTree('{myField} eq `value\\`').isValid).to.equal(true); - expect(new QuerySyntaxTree(`{myField} eq \\'value'`).isValid).to.equal(false); - expect(new QuerySyntaxTree(`{myField} eq \\"value"`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq "value\\\\"`).isValid).to.equal(true); + expect(new QuerySyntaxTree('{myField} eq `value\\\\`').isValid).to.equal(true); + expect(new QuerySyntaxTree(`{myField} eq \\\\'value'`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq \\\\"value"`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq 'value\\'`).isValid).to.equal(false); + expect(new QuerySyntaxTree(`{myField} eq "value\\"`).isValid).to.equal(false); + expect(new QuerySyntaxTree('{myField} eq `value\\`').isValid).to.equal(false); }); it('support arbitrary quoted column name', () => { @@ -135,7 +139,7 @@ describe('Query Syntax Tree', () => { }); it('support column name with "\\"', () => { - const tree = new QuerySyntaxTree('{\\\\{} eq 1 || {\\\\{} eq 2'); + const tree = new QuerySyntaxTree('{\\\\\\{} eq 1 || {\\\\\\{} eq 2'); expect(tree.isValid).to.equal(true); expect(tree.evaluate(data0)).to.equal(false); From 8793613f4a7ec74d921cef4de791170902d1aafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 16 Apr 2019 15:32:22 -0400 Subject: [PATCH 53/54] fix lexeme tests --- tests/cypress/tests/unit/lexeme_test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cypress/tests/unit/lexeme_test.ts b/tests/cypress/tests/unit/lexeme_test.ts index 320296cea..5c878e82c 100644 --- a/tests/cypress/tests/unit/lexeme_test.ts +++ b/tests/cypress/tests/unit/lexeme_test.ts @@ -46,7 +46,7 @@ describe('expression', () => { expect(stringExpression.resolve(undefined, { value: '"\\""' } as ISyntaxTree)).to.equal('"'); expect(stringExpression.resolve(undefined, { value: `'\\''` } as ISyntaxTree)).to.equal(`'`); expect(stringExpression.resolve(undefined, { value: '`\\``' } as ISyntaxTree)).to.equal('`'); - expect(stringExpression.resolve(undefined, { value: '\'\\\'' } as ISyntaxTree)).to.equal('\\'); + expect(stringExpression.resolve(undefined, { value: '\'\\\\\'' } as ISyntaxTree)).to.equal('\\'); expect(stringExpression.resolve.bind(undefined, {}, { value: '3' } as ISyntaxTree)).to.throw(Error); expect(stringExpression.resolve.bind(undefined, {}, { value: 'abc' } as ISyntaxTree)).to.throw(Error); @@ -72,8 +72,8 @@ describe('expression', () => { expect(valueExpression.resolve(undefined, { value: 'abc\\ \\ \\ ' } as ISyntaxTree)).to.equal('abc '); expect(valueExpression.resolve(undefined, { value: '\\ \\ \\ abc' } as ISyntaxTree)).to.equal(' abc'); expect(valueExpression.resolve(undefined, { value: 'a\\ bc' } as ISyntaxTree)).to.equal('a bc'); - expect(valueExpression.resolve(undefined, { value: '\\' } as ISyntaxTree)).to.equal('\\'); - expect(valueExpression.resolve(undefined, { value: 'abc\\' } as ISyntaxTree)).to.equal('abc\\'); + expect(valueExpression.resolve(undefined, { value: '\\\\' } as ISyntaxTree)).to.equal('\\'); + expect(valueExpression.resolve(undefined, { value: 'abc\\\\' } as ISyntaxTree)).to.equal('abc\\'); expect(valueExpression.resolve(undefined, { value: '123' } as ISyntaxTree)).to.equal(123); expect(valueExpression.resolve(undefined, { value: '123.45' } as ISyntaxTree)).to.equal(123.45); expect(valueExpression.resolve(undefined, { value: '1E6' } as ISyntaxTree)).to.equal(1000000); From 45963e70f2f812a2935a16890da12bfa03b9ca6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 16 Apr 2019 16:20:33 -0400 Subject: [PATCH 54/54] - rename derived_query_structure to derived_filter_structure - rename filtering_settings to filter --- CHANGELOG.md | 16 ++++++++++ demo/App.js | 4 +-- .../components/ControlledTable/index.tsx | 8 ++--- src/dash-table/components/FilterFactory.tsx | 6 ++-- src/dash-table/components/Table/index.tsx | 12 +++---- src/dash-table/components/Table/props.ts | 6 ++-- src/dash-table/dash/DataTable.js | 32 +++++++++---------- src/dash-table/derived/data/virtual.ts | 4 +-- src/dash-table/derived/table/index.tsx | 4 +-- tests/dash/app_dataframe_backend_paging.py | 26 +++++++-------- .../percy-storybook/Width.empty.percy.tsx | 8 ++--- 11 files changed, 71 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a4885e8..4cf9c7646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] +### Added +[#397](https://github.com/plotly/dash-table/pull/397) +- Improve filtering syntax and capabilities + - new field syntax `{myField}` + - short form by-column filter + - implicit column and `eq` operator (e.g `"value"`) + - implicit column (e.g `ne "value"`) + - explicit form (e.g `{field} ne "value"`) + - new `contains` relational operator for strings + - new readonly `derived_filter_structure` prop exposing the query structure in a programmatically friendlier way + +### Changed +[#397](https://github.com/plotly/dash-table/pull/397) +- Rename `filtering_settings` to `filter` + ## [3.6.0] - 2019-03-04 ### Fixed [#189](https://github.com/plotly/dash-table/issues/189) diff --git a/demo/App.js b/demo/App.js index b34975da3..07dc6acc2 100644 --- a/demo/App.js +++ b/demo/App.js @@ -40,7 +40,7 @@ class App extends Component { className='clear-filters' onClick={() => { const tableProps = R.clone(this.state.tableProps); - tableProps.filtering_settings = ''; + tableProps.filter = ''; this.setState({ tableProps }); }} @@ -53,7 +53,7 @@ class App extends Component { } onBlur={e => { const tableProps = R.clone(this.state.tableProps); - tableProps.filtering_settings = e.target.value; + tableProps.filter = e.target.value; this.setState({ tableProps }); }} /> diff --git a/src/dash-table/components/ControlledTable/index.tsx b/src/dash-table/components/ControlledTable/index.tsx index 80619a915..a9d8dc57e 100644 --- a/src/dash-table/components/ControlledTable/index.tsx +++ b/src/dash-table/components/ControlledTable/index.tsx @@ -55,9 +55,9 @@ export default class ControlledTable extends PureComponent getLexerResult = memoizeOne(lexer.bind(undefined, queryLexicon)); get lexerResult() { - const { filtering_settings } = this.props; + const { filter } = this.props; - return this.getLexerResult(filtering_settings); + return this.getLexerResult(filter); } private updateStylesheet() { @@ -535,7 +535,7 @@ export default class ControlledTable extends PureComponent columns, data, editable, - filtering_settings, + filter, setProps, sorting_settings, viewport @@ -552,7 +552,7 @@ export default class ControlledTable extends PureComponent columns, data, true, - !sorting_settings.length || !filtering_settings.length + !sorting_settings.length || !filter.length ); if (result) { diff --git a/src/dash-table/components/FilterFactory.tsx b/src/dash-table/components/FilterFactory.tsx index b4b960af3..1aa02ecf5 100644 --- a/src/dash-table/components/FilterFactory.tsx +++ b/src/dash-table/components/FilterFactory.tsx @@ -18,8 +18,8 @@ type SetFilter = (filter: string, rawFilter: string) => void; export interface IFilterOptions { columns: VisibleColumns; + filter: string; filtering: Filtering; - filtering_settings: string; filtering_type: FilteringType; id: string; rawFilterQuery: string; @@ -127,8 +127,8 @@ export default class FilterFactory { public createFilters() { const { columns, + filter, filtering, - filtering_settings, filtering_type, row_deletable, row_selectable, @@ -143,7 +143,7 @@ export default class FilterFactory { return []; } - this.updateOps(filtering_settings); + this.updateOps(filter); if (filtering_type === FilteringType.Basic) { const filterStyles = this.relevantStyles( diff --git a/src/dash-table/components/Table/index.tsx b/src/dash-table/components/Table/index.tsx index dbf0efe2d..1ce9b95a2 100644 --- a/src/dash-table/components/Table/index.tsx +++ b/src/dash-table/components/Table/index.tsx @@ -70,8 +70,8 @@ export default class Table extends Component = {}; if (!derivedStructureCache.cached) { - newProps.derived_query_structure = derivedStructureCache.result; + newProps.derived_filter_structure = derivedStructureCache.result; } if (!virtualCached) { diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index a4dfeb7de..1a8f1e64d 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -266,8 +266,8 @@ interface IProps { data?: Data; dropdown_properties: any; // legacy editable?: boolean; + filter?: string; filtering?: Filtering; - filtering_settings?: string; filtering_type?: FilteringType; filtering_types?: FilteringType[]; locale_format: INumberLocale; @@ -309,8 +309,8 @@ interface IDefaultProps { css: IStylesheetRule[]; data: Data; editable: boolean; + filter: string; filtering: Filtering; - filtering_settings: string; filtering_type: FilteringType; filtering_types: FilteringType[]; merge_duplicate_headers: boolean; @@ -345,7 +345,7 @@ interface IDefaultProps { } interface IDerivedProps { - derived_query_structure: object | null; + derived_filter_structure: object | null; derived_viewport_data: Data; derived_viewport_indices: Indices; derived_viewport_selected_rows: Indices; diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index 786537af3..b0ab6b87b 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -40,8 +40,8 @@ export const defaultProps = { content_style: 'grow', css: [], + filter: '', filtering: false, - filtering_settings: '', filtering_type: 'basic', filtering_types: ['basic'], sorting: false, @@ -771,6 +771,16 @@ export const propTypes = { */ tooltip_duration: PropTypes.number, + /** + * If `filtering` is enabled, then the current filtering + * string is represented in this `filter` + * property. + * NOTE: The shape and structure of this property will + * likely change in the future. + * Stay tuned in [https://github.com/plotly/dash-table/issues/169](https://github.com/plotly/dash-table/issues/169) + */ + filter: PropTypes.string, + /** * The `filtering` property controls the behavior of the `filtering` UI. * If `False`, then the filtering UI is not displayed @@ -779,7 +789,7 @@ export const propTypes = { * that exists in the `data` property. * If `be`, then the filtering UI is displayed but it is the * responsibility of the developer to program the filtering - * through a callback (where `filtering_settings` would be the input + * through a callback (where `filter` would be the input * and `data` would be the output). * * NOTE - Several aspects of filtering may change in the future, @@ -788,16 +798,6 @@ export const propTypes = { */ filtering: PropTypes.oneOf(['fe', 'be', true, false]), - /** - * If `filtering` is enabled, then the current filtering - * string is represented in this `filtering_settings` - * property. - * NOTE: The shape and structure of this property will - * likely change in the future. - * Stay tuned in [https://github.com/plotly/dash-table/issues/169](https://github.com/plotly/dash-table/issues/169) - */ - filtering_settings: PropTypes.string, - /** * UNSTABLE * In the future, there may be several modes of the @@ -984,7 +984,7 @@ export const propTypes = { /** * This property represents the current structure of - * `filtering_settings` as a tree structure. Each node of the + * `filter` as a tree structure. Each node of the * query structure have: * - type (string; required) * - 'open-block' @@ -996,7 +996,7 @@ export const propTypes = { * - subType (string; optional) * - 'open-block': '()' * - 'logical-operator': '&&', '||' - * - 'relational-operator': '=', '>=', '>', '<=', '<', '!=' + * - 'relational-operator': '=', '>=', '>', '<=', '<', '!=', 'contains' * - 'unary-operator': '!', 'is bool', 'is even', 'is nil', 'is num', 'is object', 'is odd', 'is prime', 'is str' * - 'expression': 'value', 'field' * - 'operand': 'field' @@ -1009,10 +1009,10 @@ export const propTypes = { * - left (nested query structure; optional) * - right (nested query structure; optional) * - * If the query is invalid or empty, the `derived_query_structure` will + * If the query is invalid or empty, the `derived_filter_structure` will * be null. */ - derived_query_structure: PropTypes.object, + derived_filter_structure: PropTypes.object, /** * This property represents the current state of `data` diff --git a/src/dash-table/derived/data/virtual.ts b/src/dash-table/derived/data/virtual.ts index e8fd01c89..f46b597f1 100644 --- a/src/dash-table/derived/data/virtual.ts +++ b/src/dash-table/derived/data/virtual.ts @@ -14,7 +14,7 @@ import { QuerySyntaxTree } from 'dash-table/syntax-tree'; const getter = ( data: Data, filtering: Filtering, - filtering_settings: string, + filter: string, sorting: Sorting, sorting_settings: SortSettings = [], sorting_treat_empty_string_as_none: boolean @@ -25,7 +25,7 @@ const getter = ( }, data); if (filtering === 'fe' || filtering === true) { - const tree = new QuerySyntaxTree(filtering_settings); + const tree = new QuerySyntaxTree(filter); data = tree.isValid ? tree.filter(data) : diff --git a/src/dash-table/derived/table/index.tsx b/src/dash-table/derived/table/index.tsx index 82d4fe4b7..2aa87abf2 100644 --- a/src/dash-table/derived/table/index.tsx +++ b/src/dash-table/derived/table/index.tsx @@ -7,8 +7,8 @@ import FilterFactory from 'dash-table/components/FilterFactory'; import HeaderFactory from 'dash-table/components/HeaderFactory'; import { ControlledTableProps, SetProps, SetState } from 'dash-table/components/Table/props'; -const handleSetFilter = (setProps: SetProps, setState: SetState, filtering_settings: string, rawFilterQuery: string) => { - setProps({ filtering_settings }); +const handleSetFilter = (setProps: SetProps, setState: SetState, filter: string, rawFilterQuery: string) => { + setProps({ filter }); setState({ rawFilterQuery }); }; diff --git a/tests/dash/app_dataframe_backend_paging.py b/tests/dash/app_dataframe_backend_paging.py index f9ffe0973..93fb53bc2 100644 --- a/tests/dash/app_dataframe_backend_paging.py +++ b/tests/dash/app_dataframe_backend_paging.py @@ -141,7 +141,7 @@ def layout(): pagination_mode='be', filtering='be', - filtering_settings='' + filter='' ), section_title('Backend Paging with Filtering and Multi-Column Sorting'), @@ -158,7 +158,7 @@ def layout(): pagination_mode='be', filtering='be', - filtering_settings='', + filter='', sorting='be', sorting_type='multi', @@ -188,7 +188,7 @@ def layout(): pagination_mode='be', filtering='be', - filtering_settings='', + filter='', sorting='be', sorting_type='multi', @@ -269,10 +269,10 @@ def update_graph(pagination_settings, sorting_settings): @app.callback( Output(IDS["table-filtering"], "data"), [Input(IDS["table-filtering"], "pagination_settings"), - Input(IDS["table-filtering"], "filtering_settings")]) -def update_graph(pagination_settings, filtering_settings): - print(filtering_settings) - filtering_expressions = filtering_settings.split(' && ') + Input(IDS["table-filtering"], "filter")]) +def update_graph(pagination_settings, filter): + print(filter) + filtering_expressions = filter.split(' && ') dff = df for filter in filtering_expressions: if ' eq ' in filter: @@ -298,9 +298,9 @@ def update_graph(pagination_settings, filtering_settings): Output(IDS["table-sorting-filtering"], "data"), [Input(IDS["table-sorting-filtering"], "pagination_settings"), Input(IDS["table-sorting-filtering"], "sorting_settings"), - Input(IDS["table-sorting-filtering"], "filtering_settings")]) -def update_graph(pagination_settings, sorting_settings, filtering_settings): - filtering_expressions = filtering_settings.split(' && ') + Input(IDS["table-sorting-filtering"], "filter")]) +def update_graph(pagination_settings, sorting_settings, filter): + filtering_expressions = filter.split(' && ') dff = df for filter in filtering_expressions: if ' eq ' in filter: @@ -336,9 +336,9 @@ def update_graph(pagination_settings, sorting_settings, filtering_settings): Output(IDS["table-paging-with-graph"], "data"), [Input(IDS["table-paging-with-graph"], "pagination_settings"), Input(IDS["table-paging-with-graph"], "sorting_settings"), - Input(IDS["table-paging-with-graph"], "filtering_settings")]) -def update_table(pagination_settings, sorting_settings, filtering_settings): - filtering_expressions = filtering_settings.split(' && ') + Input(IDS["table-paging-with-graph"], "filter")]) +def update_table(pagination_settings, sorting_settings, filter): + filtering_expressions = filter.split(' && ') dff = df for filter in filtering_expressions: if ' eq ' in filter: diff --git a/tests/visual/percy-storybook/Width.empty.percy.tsx b/tests/visual/percy-storybook/Width.empty.percy.tsx index f99897846..2b59a5fc5 100644 --- a/tests/visual/percy-storybook/Width.empty.percy.tsx +++ b/tests/visual/percy-storybook/Width.empty.percy.tsx @@ -41,21 +41,21 @@ storiesOf('DashTable/Empty', module) />)) .add('with column filters -- invalid query', () => ()) .add('with column filters -- single query', () => ()) .add('with column filters -- multi query', () => ()) .add('with column filters -- multi query, no data', () => ()); \ No newline at end of file