diff --git a/CHANGELOG.md b/CHANGELOG.md index 56af8a9ab..5537fff0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] +### Fixed +[#460](https://github.com/plotly/dash-table/issues/460) +- The `datestartswith` relational operator now supports number comparison +- Fixed a bug where the implicit operator for columns was `equal` instead of the expected default for the column type + ## [4.3.0] - 2019-09-17 ### Added [#566](https://github.com/plotly/dash-table/pull/566) diff --git a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts index f5ecd81c6..9c024ec5d 100644 --- a/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts +++ b/src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts @@ -6,19 +6,28 @@ import { LexemeType, boundLexeme } from 'core/syntax-tree/lexicon'; import { ColumnType, IColumn } from 'dash-table/components/Table/props'; import { fieldExpression } from './lexeme/expression'; -import { equal, RelationalOperator } from './lexeme/relational'; +import { equal, RelationalOperator, contains, dateStartsWith } from './lexeme/relational'; import columnLexicon from './lexicon/column'; -function getDefaultRelationalOperator(type: ColumnType = ColumnType.Any): RelationalOperator { +function getImplicitLexeme(type: ColumnType = ColumnType.Any): ILexemeResult { switch (type) { case ColumnType.Any: case ColumnType.Text: - return RelationalOperator.Contains; + return { + lexeme: boundLexeme(contains), + value: RelationalOperator.Contains + }; case ColumnType.Datetime: - return RelationalOperator.DateStartsWith; + return { + lexeme: boundLexeme(dateStartsWith), + value: RelationalOperator.DateStartsWith + }; case ColumnType.Numeric: - return RelationalOperator.Equal; + return { + lexeme: boundLexeme(equal), + value: RelationalOperator.Equal + }; } } @@ -49,10 +58,7 @@ function modifyLex(config: SingleColumnConfig, res: ILexerResult) { } else if (isExpression(res.lexemes)) { res.lexemes = [ { lexeme: boundLexeme(fieldExpression), value: `{${config.id}}` }, - { - lexeme: boundLexeme(equal), - value: getDefaultRelationalOperator(config.type) - }, + getImplicitLexeme(config.type), ...res.lexemes ]; } diff --git a/src/dash-table/syntax-tree/lexeme/relational.ts b/src/dash-table/syntax-tree/lexeme/relational.ts index 1c9a92ea9..06eec9129 100644 --- a/src/dash-table/syntax-tree/lexeme/relational.ts +++ b/src/dash-table/syntax-tree/lexeme/relational.ts @@ -88,6 +88,9 @@ const DATE_OPTIONS: IDateValidation = { export const dateStartsWith: IUnboundedLexeme = R.merge({ evaluate: relationalEvaluator(([op, exp]) => { + op = typeof op === 'number' ? op.toString() : op; + exp = typeof exp === 'number' ? exp.toString() : exp; + const normalizedOp = normalizeDate(op, DATE_OPTIONS); const normalizedExp = normalizeDate(exp, DATE_OPTIONS); diff --git a/tests/cypress/tests/unit/dash_table_queries_test.ts b/tests/cypress/tests/unit/dash_table_queries_test.ts index d811e5731..c6288ff32 100644 --- a/tests/cypress/tests/unit/dash_table_queries_test.ts +++ b/tests/cypress/tests/unit/dash_table_queries_test.ts @@ -74,7 +74,7 @@ describe('Dash Table Queries', () => { describe('contains', () => { processCases(c.syntaxer, [ - { name: 'cannot compare "11" to 1', query: `${c.hideOperand ? '' : '{a} '}contains 1`, target: { a: '11' }, valid: true, evaluate: true }, + { name: 'compares "11" to 1', query: `${c.hideOperand ? '' : '{a} '}contains 1`, target: { a: '11' }, valid: true, evaluate: true }, { name: 'cannot compare 11 to 1', query: `${c.hideOperand ? '' : '{a} '}contains 1`, target: { a: 11 }, valid: true, evaluate: false }, { name: 'compares "11" to "1"', query: `${c.hideOperand ? '' : '{a} '}contains "1"`, target: { a: '11' }, valid: true, evaluate: true }, { name: 'compares 11 to "1"', query: `${c.hideOperand ? '' : '{a} '}contains "1"`, target: { a: 11 }, valid: true, evaluate: true }, @@ -84,8 +84,8 @@ describe('Dash Table Queries', () => { { name: 'compares "abc" to "b"', query: `${c.hideOperand ? '' : '{a} '}contains "b"`, target: { a: 'abc' }, valid: true, evaluate: true }, { name: 'compares "abc" to " b"', query: `${c.hideOperand ? '' : '{a} '}contains " b"`, target: { a: 'abc' }, valid: true, evaluate: false }, { name: 'compares "abc" to "b "', query: `${c.hideOperand ? '' : '{a} '}contains "b "`, target: { a: 'abc' }, valid: true, evaluate: false }, - { name: 'compares "abc" to " b"', query: `${c.hideOperand ? '' : '{a} '}contains " b"`, target: { a: 'a bc' }, valid: true, evaluate: true }, - { name: 'compares "abc" to "b "', query: `${c.hideOperand ? '' : '{a} '}contains "b "`, target: { a: 'ab c' }, valid: true, evaluate: true } + { name: 'compares "a bc" to " b"', query: `${c.hideOperand ? '' : '{a} '}contains " b"`, target: { a: 'a bc' }, valid: true, evaluate: true }, + { name: 'compares "ab c" to "b "', query: `${c.hideOperand ? '' : '{a} '}contains "b "`, target: { a: 'ab c' }, valid: true, evaluate: true } ]); }); @@ -96,7 +96,7 @@ describe('Dash Table Queries', () => { { name: '0yyy in "0yyy"', query: `${c.hideOperand ? '' : '{a} '}datestartswith "0987"`, target: { a: '0987' }, valid: true, evaluate: true }, { name: 'yyyy in "yyyy"', query: `${c.hideOperand ? '' : '{a} '}datestartswith "2006"`, target: { a: '2005' }, valid: true, evaluate: false }, { name: 'yyyy in "yyyy"', query: `${c.hideOperand ? '' : '{a} '}datestartswith "2005"`, target: { a: '2005' }, valid: true, evaluate: true }, - { name: 'yyyy in yyyy', query: `${c.hideOperand ? '' : '{a} '}datestartswith 2005`, target: { a: '2005' }, valid: true, evaluate: false }, + { name: 'yyyy in yyyy', query: `${c.hideOperand ? '' : '{a} '}datestartswith 2005`, target: { a: '2005' }, valid: true, evaluate: true }, { name: 'yyyy-mm in "yyyy"', query: `${c.hideOperand ? '' : '{a} '}datestartswith "2005"`, target: { a: '2005-01' }, valid: true, evaluate: true }, { name: 'yyyy-mm-dd in "yyyy"', query: `${c.hideOperand ? '' : '{a} '}datestartswith "2005"`, target: { a: '2005-01-01' }, valid: true, evaluate: true }, { name: 'yyyy-mm-dd hh in "yyyy"', query: `${c.hideOperand ? '' : '{a} '}datestartswith "2005"`, target: { a: '2005-01-01T10' }, valid: true, evaluate: 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 d98108f00..8582c1e10 100644 --- a/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts +++ b/tests/cypress/tests/unit/single_column_syntactic_tree_test.ts @@ -1,12 +1,19 @@ import { SingleColumnSyntaxTree } from 'dash-table/syntax-tree'; import { ColumnType } from 'dash-table/components/Table/props'; import { SingleColumnConfig } from 'dash-table/syntax-tree/SingleColumnSyntaxTree'; +import { RelationalOperator } from 'dash-table/syntax-tree/lexeme/relational'; +import { LexemeType } from 'core/syntax-tree/lexicon'; const COLUMN_ANY: SingleColumnConfig = { id: 'a', type: ColumnType.Any }; +const COLUMN_DATE: SingleColumnConfig = { + id: 'a', + type: ColumnType.Datetime +}; + const COLUMN_NUMERIC: SingleColumnConfig = { id: 'a', type: ColumnType.Numeric @@ -72,7 +79,9 @@ describe('Single Column Syntax Tree', () => { const tree = new SingleColumnSyntaxTree('1', COLUMN_UNDEFINED); expect(tree.isValid).to.equal(true); - expect(tree.evaluate({ a: 1 })).to.equal(true); + expect(tree.evaluate({ a: '1' })).to.equal(true); + expect(tree.evaluate({ a: '2' })).to.equal(false); + expect(tree.evaluate({ a: 1 })).to.equal(false); expect(tree.evaluate({ a: 2 })).to.equal(false); expect(tree.toQueryString()).to.equal('{a} contains 1'); @@ -110,9 +119,119 @@ describe('Single Column Syntax Tree', () => { const tree = new SingleColumnSyntaxTree('"1"', COLUMN_TEXT); expect(tree.isValid).to.equal(true); + expect(tree.evaluate({ a: 1 })).to.equal(true); + expect(tree.evaluate({ a: 2 })).to.equal(false); expect(tree.evaluate({ a: '1' })).to.equal(true); expect(tree.evaluate({ a: '2' })).to.equal(false); expect(tree.toQueryString()).to.equal('{a} contains "1"'); }); + + ['1975', '"1975"'].forEach(value => { + it(`can be expression '${value}' with datetime column type`, () => { + const tree = new SingleColumnSyntaxTree(value, COLUMN_DATE); + + expect(tree.evaluate({ a: 1975 })).to.equal(true); + expect(tree.evaluate({ a: '1975' })).to.equal(true); + expect(tree.evaluate({ a: '1975-01' })).to.equal(true); + expect(tree.evaluate({ a: '1975-01-01' })).to.equal(true); + expect(tree.evaluate({ a: '1975-01-01 01:01:01' })).to.equal(true); + + expect(tree.evaluate({ a: 1976 })).to.equal(false); + expect(tree.evaluate({ a: '1976' })).to.equal(false); + expect(tree.evaluate({ a: '1976-01' })).to.equal(false); + expect(tree.evaluate({ a: '1976-01-01' })).to.equal(false); + expect(tree.evaluate({ a: '1976-01-01 01:01:01' })).to.equal(false); + }); + }); + + [ + { type: COLUMN_UNDEFINED, name: 'undefined' }, + { type: COLUMN_ANY, name: 'any' }, + { type: COLUMN_TEXT, name: 'text' } + ].forEach(({ type, name }) => { + it(`returns the correct relational operator lexeme for '${name}' column type`, () => { + const tree = new SingleColumnSyntaxTree('1', type); + const structure = tree.toStructure(); + + expect(tree.toQueryString()).to.equal('{a} contains 1'); + expect(structure).to.not.equal(null); + + if (structure) { + expect(structure.value).to.equal(RelationalOperator.Contains); + expect(structure.subType).to.equal(RelationalOperator.Contains); + expect(structure.type).to.equal(LexemeType.RelationalOperator); + + expect(structure.left).to.not.equal(null); + if (structure.left) { + expect(structure.left.type).to.equal(LexemeType.Expression); + expect(structure.left.subType).to.equal('field'); + expect(structure.left.value).to.equal('a'); + } + + expect(structure.right).to.not.equal(null); + if (structure.right) { + expect(structure.right.type).to.equal(LexemeType.Expression); + expect(structure.right.subType).to.equal('value'); + expect(structure.right.value).to.equal(1); + } + } + }); + }); + + it(`returns the correct relational operator lexeme for 'date' column type`, () => { + const tree = new SingleColumnSyntaxTree('1975', COLUMN_DATE); + const structure = tree.toStructure(); + + expect(tree.toQueryString()).to.equal('{a} datestartswith 1975'); + expect(structure).to.not.equal(null); + + if (structure) { + expect(structure.value).to.equal(RelationalOperator.DateStartsWith); + expect(structure.subType).to.equal(RelationalOperator.DateStartsWith); + expect(structure.type).to.equal(LexemeType.RelationalOperator); + + expect(structure.left).to.not.equal(null); + if (structure.left) { + expect(structure.left.type).to.equal(LexemeType.Expression); + expect(structure.left.subType).to.equal('field'); + expect(structure.left.value).to.equal('a'); + } + + expect(structure.right).to.not.equal(null); + if (structure.right) { + expect(structure.right.type).to.equal(LexemeType.Expression); + expect(structure.right.subType).to.equal('value'); + expect(structure.right.value).to.equal(1975); + } + } + }); + + it(`returns the correct relational operator lexeme for 'numeric' column type`, () => { + const tree = new SingleColumnSyntaxTree('1', COLUMN_NUMERIC); + const structure = tree.toStructure(); + + expect(tree.toQueryString()).to.equal('{a} = 1'); + expect(structure).to.not.equal(null); + + if (structure) { + expect(structure.value).to.equal(RelationalOperator.Equal); + expect(structure.subType).to.equal(RelationalOperator.Equal); + expect(structure.type).to.equal(LexemeType.RelationalOperator); + + expect(structure.left).to.not.equal(null); + if (structure.left) { + expect(structure.left.type).to.equal(LexemeType.Expression); + expect(structure.left.subType).to.equal('field'); + expect(structure.left.value).to.equal('a'); + } + + expect(structure.right).to.not.equal(null); + if (structure.right) { + expect(structure.right.type).to.equal(LexemeType.Expression); + expect(structure.right.subType).to.equal('value'); + expect(structure.right.value).to.equal(1); + } + } + }); }); \ No newline at end of file