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 4 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
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
- The `contains` 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
19 changes: 13 additions & 6 deletions src/dash-table/syntax-tree/lexeme/relational.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,16 @@ const LEXEME_BASE = {
};

export const contains: IUnboundedLexeme = R.merge({
evaluate: relationalEvaluator(([op, exp]) =>
!R.isNil(exp) &&
!R.isNil(op) &&
(R.type(exp) === 'String' || R.type(op) === 'String') &&
op.toString().indexOf(exp.toString()) !== -1
),
evaluate: relationalEvaluator(([op, exp]) => {
if (R.isNil(op) || R.isNil(exp)) {
return false;
}

op = typeof op === 'number' ? op.toString() : op;
exp = typeof exp === 'number' ? exp.toString() : exp;

return op.toString().indexOf(exp.toString()) !== -1;
Copy link
Collaborator

Choose a reason for hiding this comment

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

isn't this double .toString?

But also: should we be trying to use the column formatting here? What if you have prices formatted with 2-digit decimals and you want to match all even-dollar values, with contains '.00'?

Copy link
Contributor Author

@Marc-Andre-Rivet Marc-Andre-Rivet Sep 18, 2019

Choose a reason for hiding this comment

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

Getting access to formatting from here would be difficult.

I think you're right, I got carried away by the number case for dates. Deviating too much from what was agreed upon when we last went through filtering. Will revert the contains changes and focus on (1) datestartswith and (2) the lexeme bug

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK - certainly understand that this would be tough, and it's a weird edge case so I'm happy to ignore for now. Let's make an issue for it though, if it doesn't already exist. If a user is filtering as a string, they would expect the string used in the filter to be the one they see.

}),
subType: RelationalOperator.Contains,
regexp: /^(contains)/i
}, LEXEME_BASE);
Expand Down Expand Up @@ -88,6 +92,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
10 changes: 5 additions & 5 deletions tests/cypress/tests/unit/dash_table_queries_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ 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: '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 },
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 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.

contains now does not distinguish between string 11 and number 11 in all (string|number, string|number) operand/expression combinations

{ 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 },
{ name: 'compares "1" to "1.0"', query: `${c.hideOperand ? '' : '{a} '}contains "1.0"`, target: { a: '1' }, valid: true, evaluate: false },
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
117 changes: 117 additions & 0 deletions 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 @@ -110,9 +117,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);
}
}
});
});