Skip to content

Commit

Permalink
Merge pull request #14 from shelfio/feature/DECTREEAPI-235-fix-string…
Browse files Browse the repository at this point in the history
…-case

DECTREEAPI-235 Implemented case-insensitive string comparing
  • Loading branch information
DrewsAbuse authored Mar 14, 2023
2 parents 510a8e1 + ae1ba01 commit d462e13
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 68 deletions.
97 changes: 70 additions & 27 deletions src/evaluation.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {Expression} from './types';
import {expressionToRPN} from './evaluation';
import {evaluate} from './evaluation';

const baseExpression: Expression = {
joiner: 'and',
Expand Down Expand Up @@ -73,15 +73,15 @@ const singleRuleExpression: Expression = {
],
};

const variablesValueMap = new Map(
const variablesValueMap = new Map<string, string>(
[
{
id: 'variable-id-a',
value: 'a',
value: 'A',
},
{
id: 'variable-id-b',
value: 'b',
value: 'B',
},
{
id: 'variable-id-c',
Expand All @@ -98,20 +98,26 @@ const variablesValueMap = new Map(
].map(({id, value}) => [id, value])
);

describe('expressionToRPN', () => {
it('should return `[true]`', () => {
expect(expressionToRPN(baseExpression, variablesValueMap)).toEqual([true]);
describe('evaluate', () => {
it('should return `true`', () => {
expect(
evaluate({expression: baseExpression, variableIdToVariablesMap: variablesValueMap})
).toEqual(true);
});

it('should return `[true]` for expression w/ non-binary operator', () => {
expect(expressionToRPN(notBinaryExpression, variablesValueMap)).toEqual([true]);
it('should return `true` for expression w/ non-binary operator', () => {
expect(
evaluate({expression: notBinaryExpression, variableIdToVariablesMap: variablesValueMap})
).toEqual(true);
});

it('should return `[true]` for single rule when passed correct variable value', () => {
expect(expressionToRPN(singleRuleExpression, variablesValueMap)).toEqual([true]);
it('should return `true` for single rule when passed correct variable value', () => {
expect(
evaluate({expression: singleRuleExpression, variableIdToVariablesMap: variablesValueMap})
).toEqual(true);
});

it('should return `[false]` for single rule when passed wrong variable value', () => {
it('should return `false` for single rule when passed wrong variable value', () => {
const variablesValueMap = new Map([['variable-id-a', 'wrong-value']]);

const expression: Expression = {
Expand All @@ -124,10 +130,10 @@ describe('expressionToRPN', () => {
],
};

expect(expressionToRPN(expression, variablesValueMap)).toEqual([false]);
expect(evaluate({expression, variableIdToVariablesMap: variablesValueMap})).toEqual(false);
});

it('should return `[false]` when variables value not passed', () => {
it('should return `false` when variables value not passed', () => {
const variablesValueMap = new Map();

const expression: Expression = {
Expand All @@ -140,12 +146,12 @@ describe('expressionToRPN', () => {
],
};

expect(expressionToRPN(expression, variablesValueMap)).toEqual([false]);
expect(evaluate({expression, variableIdToVariablesMap: variablesValueMap})).toEqual(false);
});

const variableValueSomething = new Map([['variable-id-a', 'something']]);

it('should return `[true]` for expression and `contains` rule', () => {
it('should return `true` for expression and `contains` rule', () => {
const expression: Expression = {
rules: [
{
Expand All @@ -156,10 +162,10 @@ describe('expressionToRPN', () => {
],
};

expect(expressionToRPN(expression, variableValueSomething)).toEqual([true]);
expect(evaluate({expression, variableIdToVariablesMap: variableValueSomething})).toEqual(true);
});

it('should return `[false]` for expression and `contains` rule', () => {
it('should return `false` for expression and `contains` rule', () => {
const expression: Expression = {
rules: [
{
Expand All @@ -170,10 +176,10 @@ describe('expressionToRPN', () => {
],
};

expect(expressionToRPN(expression, variableValueSomething)).toEqual([false]);
expect(evaluate({expression, variableIdToVariablesMap: variableValueSomething})).toEqual(false);
});

it('should return `[false]` for expression and `not_contains` rule', () => {
it('should return `false` for expression and `not_contains` rule', () => {
const expression: Expression = {
rules: [
{
Expand All @@ -184,10 +190,10 @@ describe('expressionToRPN', () => {
],
};

expect(expressionToRPN(expression, variableValueSomething)).toEqual([false]);
expect(evaluate({expression, variableIdToVariablesMap: variableValueSomething})).toEqual(false);
});

it('should return `[true]` for expression and `not_contains` rule', () => {
it('should return `true` for expression and `not_contains` rule', () => {
const expression: Expression = {
rules: [
{
Expand All @@ -198,10 +204,10 @@ describe('expressionToRPN', () => {
],
};

expect(expressionToRPN(expression, variableValueSomething)).toEqual([true]);
expect(evaluate({expression, variableIdToVariablesMap: variableValueSomething})).toEqual(true);
});

it('should return `[true]` for complex expression', () => {
it('should return `true` for complex expression', () => {
const complexExpression: Expression = {
joiner: 'and',
rules: [
Expand All @@ -225,10 +231,12 @@ describe('expressionToRPN', () => {
],
};

expect(expressionToRPN(complexExpression, variablesValueMap)).toEqual([true]);
expect(
evaluate({expression: complexExpression, variableIdToVariablesMap: variablesValueMap})
).toEqual(true);
});

it('should return `[false]` for complex expression', () => {
it('should return `false` for complex expression', () => {
const complexExpression: Expression = {
joiner: 'and',
rules: [
Expand All @@ -252,6 +260,41 @@ describe('expressionToRPN', () => {
],
};

expect(expressionToRPN(complexExpression, variablesValueMap)).toEqual([false]);
expect(
evaluate({expression: complexExpression, variableIdToVariablesMap: variablesValueMap})
).toEqual(false);
});

it('should return `false` for case sensitive option', () => {
expect(
evaluate({
expression: baseExpression,
variableIdToVariablesMap: variablesValueMap,
options: {caseSensitive: true},
})
).toEqual(false);
});

it('should return `true` for case sensitive option', () => {
const variablesLowerCaseValueMap = new Map<string, string>(
[
{
id: 'variable-id-a',
value: 'a',
},
{
id: 'variable-id-b',
value: 'b',
},
].map(({id, value}) => [id, value])
);

expect(
evaluate({
expression: baseExpression,
variableIdToVariablesMap: variablesLowerCaseValueMap,
options: {caseSensitive: true},
})
).toEqual(true);
});
});
109 changes: 74 additions & 35 deletions src/evaluation.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,90 @@
import type {Expression, Rule, StackElement} from './types';
import type {JoinerParameters, RuleParameters} from './types';
import type {Expression} from './types';
import type {JoinerParameters} from './types';
import type {Rule, StackElement} from './types';
import type {RuleParameters} from './types';
import {validateJoinerInvoke, validateRuleInvoke} from './validations';

export const joinerHandlers = {
or: ({left, right}: JoinerParameters) => Boolean(left || right),
and: ({left, right}: JoinerParameters) => Boolean(left && right),
single: ({left}: JoinerParameters) => Boolean(left),
export const evaluate = ({
expression,
variableIdToVariablesMap,
options = {caseSensitive: false},
}: {
expression: Expression;
variableIdToVariablesMap: Map<string, string>;
options?: {caseSensitive: boolean};
}): StackElement => {
const {caseSensitive} = options;
const ruleHandlers = caseSensitive ? caseSensitiveRuleHandlers : caseInsensitiveRuleHandlers;

const evaluator = (
expression: Expression | Rule,
variableIdToValuesMap: Map<string, string>
): StackElement[] => {
if ('rules' in expression) {
const rpn = transformToBinaryOperators(expression).rules.flatMap(rule =>
evaluator(rule, variableIdToValuesMap)
);

const {joiner} = expression;
const [left, right] = rpn.splice(-2, 2);

validateJoinerInvoke({joiner, left, right});

rpn.push(joinerHandlers[joiner ?? 'single']({left, right}));

return rpn;
}

const {variableId, operator, value: comparedValue} = expression;

validateRuleInvoke({operator, comparedValue});

return [
ruleHandlers[operator]({
passedValue: variableIdToValuesMap.get(variableId),
comparedValue,
}),
];
};
const [result] = evaluator(expression, variableIdToVariablesMap);

return result;
};

export const ruleHandlers = {
type RuleHandlers = Record<
'eq' | 'neq' | 'contains' | 'not_contains',
(params: RuleParameters) => boolean
>;

const caseSensitiveRuleHandlers: RuleHandlers = {
eq: ({passedValue, comparedValue}: RuleParameters) => passedValue === comparedValue,
neq: ({passedValue, comparedValue}: RuleParameters) => passedValue !== comparedValue,
contains: ({passedValue, comparedValue}: RuleParameters) =>
passedValue?.includes(comparedValue) ?? false,
not_contains: ({passedValue, comparedValue}: RuleParameters) =>
passedValue?.includes(comparedValue) === false,
};
const applyRuleWithUpperCase = (
{passedValue, comparedValue}: RuleParameters,
rule: (params: RuleParameters) => boolean
): boolean =>
rule({passedValue: passedValue?.toUpperCase(), comparedValue: comparedValue?.toUpperCase()});

export const expressionToRPN = (
expression: Expression | Rule,
variableIdToValuesMap: Map<string, string>
): StackElement[] => {
if ('rules' in expression) {
const rpn = transformToBinaryOperators(expression).rules.flatMap(rule =>
expressionToRPN(rule, variableIdToValuesMap)
);

const {joiner} = expression;
const [left, right] = rpn.splice(-2, 2);

validateJoinerInvoke({joiner, left, right});

rpn.push(joinerHandlers[joiner ?? 'single']({left, right}));

return rpn;
}

const {variableId, operator, value: comparedValue} = expression;

validateRuleInvoke({operator, comparedValue});
const caseInsensitiveRuleHandlers: RuleHandlers = {
eq: ({passedValue, comparedValue}: RuleParameters) =>
applyRuleWithUpperCase({passedValue, comparedValue}, caseSensitiveRuleHandlers.eq),
neq: ({passedValue, comparedValue}: RuleParameters) =>
applyRuleWithUpperCase({passedValue, comparedValue}, caseSensitiveRuleHandlers.neq),
contains: ({passedValue, comparedValue}: RuleParameters) =>
applyRuleWithUpperCase({passedValue, comparedValue}, caseSensitiveRuleHandlers.contains),
not_contains: ({passedValue, comparedValue}: RuleParameters) =>
applyRuleWithUpperCase({passedValue, comparedValue}, caseSensitiveRuleHandlers.not_contains),
};

return [
ruleHandlers[operator]({
passedValue: variableIdToValuesMap.get(variableId),
comparedValue,
}),
];
const joinerHandlers: Record<'or' | 'and' | 'single', (params: JoinerParameters) => boolean> = {
or: ({left, right}: JoinerParameters) => Boolean(left || right),
and: ({left, right}: JoinerParameters) => Boolean(left && right),
single: ({left}: JoinerParameters) => Boolean(left),
};

const transformToBinaryOperators = (expression: Expression): Expression => {
Expand Down
6 changes: 3 additions & 3 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
jest.mock('./evaluation');

import {expressionToRPN} from './evaluation';
import {evaluate} from './evaluation';
import {evaluateExpression} from './index';

it('should return true', () => {
jest.mocked(expressionToRPN).mockReturnValue([true]);
jest.mocked(evaluate).mockReturnValue(true);

const expression = {
rules: [
Expand All @@ -20,7 +20,7 @@ it('should return true', () => {
});

it('should throw `Invalid expression result` if expressionToRPN return undefined', () => {
jest.mocked(expressionToRPN).mockReturnValue([undefined as any]);
jest.mocked(evaluate).mockReturnValue(undefined);

const expression = {
rules: [
Expand Down
7 changes: 4 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type {Expression, VariableWithValue} from './types';
import {expressionToRPN} from './evaluation';
import {evaluate} from './evaluation';

export const evaluateExpression = (
expression: Expression,
variableValues: VariableWithValue[]
variableValues: VariableWithValue[],
options: {caseSensitive: boolean} = {caseSensitive: false}
): boolean => {
const variableIdToVariablesMap = new Map(variableValues.map(({id, value}) => [id, value]));

const [result] = expressionToRPN(expression, variableIdToVariablesMap);
const result = evaluate({expression, variableIdToVariablesMap, options});

if (typeof result !== 'boolean') {
throw new Error('Invalid expression result', {
Expand Down

0 comments on commit d462e13

Please sign in to comment.