Skip to content
This repository was archived by the owner on Jun 4, 2024. It is now read-only.

Issue 460 - datestartswith relational operator behavior on number expression #589

Merged
merged 6 commits into from
Sep 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 15 additions & 9 deletions src/dash-table/syntax-tree/SingleColumnSyntaxTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
}

Expand Down Expand Up @@ -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
];
}
Expand Down
3 changes: 3 additions & 0 deletions src/dash-table/syntax-tree/lexeme/relational.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
8 changes: 4 additions & 4 deletions tests/cypress/tests/unit/dash_table_queries_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

{ 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 },
Expand All @@ -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 },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

{ name: 'compares "ab c" to "b "', query: `${c.hideOperand ? '' : '{a} '}contains "b "`, target: { a: 'ab c' }, valid: true, evaluate: true }
]);
});

Expand All @@ -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 },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the relational operator will now cast number to string for datestartswith

{ 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 },
Expand Down
121 changes: 120 additions & 1 deletion tests/cypress/tests/unit/single_column_syntactic_tree_test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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);
}
}
});
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the change to the single column syntax tree
https://circleci.com/gh/plotly/dash-table/13136, 04bbe1d
and passing again afterwards
https://circleci.com/gh/plotly/dash-table/13143, 8087b40


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);
}
}
});
});