Skip to content

Commit d462e13

Browse files
authored
Merge pull request #14 from shelfio/feature/DECTREEAPI-235-fix-string-case
DECTREEAPI-235 Implemented case-insensitive string comparing
2 parents 510a8e1 + ae1ba01 commit d462e13

File tree

4 files changed

+151
-68
lines changed

4 files changed

+151
-68
lines changed

src/evaluation.test.ts

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {Expression} from './types';
2-
import {expressionToRPN} from './evaluation';
2+
import {evaluate} from './evaluation';
33

44
const baseExpression: Expression = {
55
joiner: 'and',
@@ -73,15 +73,15 @@ const singleRuleExpression: Expression = {
7373
],
7474
};
7575

76-
const variablesValueMap = new Map(
76+
const variablesValueMap = new Map<string, string>(
7777
[
7878
{
7979
id: 'variable-id-a',
80-
value: 'a',
80+
value: 'A',
8181
},
8282
{
8383
id: 'variable-id-b',
84-
value: 'b',
84+
value: 'B',
8585
},
8686
{
8787
id: 'variable-id-c',
@@ -98,20 +98,26 @@ const variablesValueMap = new Map(
9898
].map(({id, value}) => [id, value])
9999
);
100100

101-
describe('expressionToRPN', () => {
102-
it('should return `[true]`', () => {
103-
expect(expressionToRPN(baseExpression, variablesValueMap)).toEqual([true]);
101+
describe('evaluate', () => {
102+
it('should return `true`', () => {
103+
expect(
104+
evaluate({expression: baseExpression, variableIdToVariablesMap: variablesValueMap})
105+
).toEqual(true);
104106
});
105107

106-
it('should return `[true]` for expression w/ non-binary operator', () => {
107-
expect(expressionToRPN(notBinaryExpression, variablesValueMap)).toEqual([true]);
108+
it('should return `true` for expression w/ non-binary operator', () => {
109+
expect(
110+
evaluate({expression: notBinaryExpression, variableIdToVariablesMap: variablesValueMap})
111+
).toEqual(true);
108112
});
109113

110-
it('should return `[true]` for single rule when passed correct variable value', () => {
111-
expect(expressionToRPN(singleRuleExpression, variablesValueMap)).toEqual([true]);
114+
it('should return `true` for single rule when passed correct variable value', () => {
115+
expect(
116+
evaluate({expression: singleRuleExpression, variableIdToVariablesMap: variablesValueMap})
117+
).toEqual(true);
112118
});
113119

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

117123
const expression: Expression = {
@@ -124,10 +130,10 @@ describe('expressionToRPN', () => {
124130
],
125131
};
126132

127-
expect(expressionToRPN(expression, variablesValueMap)).toEqual([false]);
133+
expect(evaluate({expression, variableIdToVariablesMap: variablesValueMap})).toEqual(false);
128134
});
129135

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

133139
const expression: Expression = {
@@ -140,12 +146,12 @@ describe('expressionToRPN', () => {
140146
],
141147
};
142148

143-
expect(expressionToRPN(expression, variablesValueMap)).toEqual([false]);
149+
expect(evaluate({expression, variableIdToVariablesMap: variablesValueMap})).toEqual(false);
144150
});
145151

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

148-
it('should return `[true]` for expression and `contains` rule', () => {
154+
it('should return `true` for expression and `contains` rule', () => {
149155
const expression: Expression = {
150156
rules: [
151157
{
@@ -156,10 +162,10 @@ describe('expressionToRPN', () => {
156162
],
157163
};
158164

159-
expect(expressionToRPN(expression, variableValueSomething)).toEqual([true]);
165+
expect(evaluate({expression, variableIdToVariablesMap: variableValueSomething})).toEqual(true);
160166
});
161167

162-
it('should return `[false]` for expression and `contains` rule', () => {
168+
it('should return `false` for expression and `contains` rule', () => {
163169
const expression: Expression = {
164170
rules: [
165171
{
@@ -170,10 +176,10 @@ describe('expressionToRPN', () => {
170176
],
171177
};
172178

173-
expect(expressionToRPN(expression, variableValueSomething)).toEqual([false]);
179+
expect(evaluate({expression, variableIdToVariablesMap: variableValueSomething})).toEqual(false);
174180
});
175181

176-
it('should return `[false]` for expression and `not_contains` rule', () => {
182+
it('should return `false` for expression and `not_contains` rule', () => {
177183
const expression: Expression = {
178184
rules: [
179185
{
@@ -184,10 +190,10 @@ describe('expressionToRPN', () => {
184190
],
185191
};
186192

187-
expect(expressionToRPN(expression, variableValueSomething)).toEqual([false]);
193+
expect(evaluate({expression, variableIdToVariablesMap: variableValueSomething})).toEqual(false);
188194
});
189195

190-
it('should return `[true]` for expression and `not_contains` rule', () => {
196+
it('should return `true` for expression and `not_contains` rule', () => {
191197
const expression: Expression = {
192198
rules: [
193199
{
@@ -198,10 +204,10 @@ describe('expressionToRPN', () => {
198204
],
199205
};
200206

201-
expect(expressionToRPN(expression, variableValueSomething)).toEqual([true]);
207+
expect(evaluate({expression, variableIdToVariablesMap: variableValueSomething})).toEqual(true);
202208
});
203209

204-
it('should return `[true]` for complex expression', () => {
210+
it('should return `true` for complex expression', () => {
205211
const complexExpression: Expression = {
206212
joiner: 'and',
207213
rules: [
@@ -225,10 +231,12 @@ describe('expressionToRPN', () => {
225231
],
226232
};
227233

228-
expect(expressionToRPN(complexExpression, variablesValueMap)).toEqual([true]);
234+
expect(
235+
evaluate({expression: complexExpression, variableIdToVariablesMap: variablesValueMap})
236+
).toEqual(true);
229237
});
230238

231-
it('should return `[false]` for complex expression', () => {
239+
it('should return `false` for complex expression', () => {
232240
const complexExpression: Expression = {
233241
joiner: 'and',
234242
rules: [
@@ -252,6 +260,41 @@ describe('expressionToRPN', () => {
252260
],
253261
};
254262

255-
expect(expressionToRPN(complexExpression, variablesValueMap)).toEqual([false]);
263+
expect(
264+
evaluate({expression: complexExpression, variableIdToVariablesMap: variablesValueMap})
265+
).toEqual(false);
266+
});
267+
268+
it('should return `false` for case sensitive option', () => {
269+
expect(
270+
evaluate({
271+
expression: baseExpression,
272+
variableIdToVariablesMap: variablesValueMap,
273+
options: {caseSensitive: true},
274+
})
275+
).toEqual(false);
276+
});
277+
278+
it('should return `true` for case sensitive option', () => {
279+
const variablesLowerCaseValueMap = new Map<string, string>(
280+
[
281+
{
282+
id: 'variable-id-a',
283+
value: 'a',
284+
},
285+
{
286+
id: 'variable-id-b',
287+
value: 'b',
288+
},
289+
].map(({id, value}) => [id, value])
290+
);
291+
292+
expect(
293+
evaluate({
294+
expression: baseExpression,
295+
variableIdToVariablesMap: variablesLowerCaseValueMap,
296+
options: {caseSensitive: true},
297+
})
298+
).toEqual(true);
256299
});
257300
});

src/evaluation.ts

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,90 @@
1-
import type {Expression, Rule, StackElement} from './types';
2-
import type {JoinerParameters, RuleParameters} from './types';
1+
import type {Expression} from './types';
2+
import type {JoinerParameters} from './types';
3+
import type {Rule, StackElement} from './types';
4+
import type {RuleParameters} from './types';
35
import {validateJoinerInvoke, validateRuleInvoke} from './validations';
46

5-
export const joinerHandlers = {
6-
or: ({left, right}: JoinerParameters) => Boolean(left || right),
7-
and: ({left, right}: JoinerParameters) => Boolean(left && right),
8-
single: ({left}: JoinerParameters) => Boolean(left),
7+
export const evaluate = ({
8+
expression,
9+
variableIdToVariablesMap,
10+
options = {caseSensitive: false},
11+
}: {
12+
expression: Expression;
13+
variableIdToVariablesMap: Map<string, string>;
14+
options?: {caseSensitive: boolean};
15+
}): StackElement => {
16+
const {caseSensitive} = options;
17+
const ruleHandlers = caseSensitive ? caseSensitiveRuleHandlers : caseInsensitiveRuleHandlers;
18+
19+
const evaluator = (
20+
expression: Expression | Rule,
21+
variableIdToValuesMap: Map<string, string>
22+
): StackElement[] => {
23+
if ('rules' in expression) {
24+
const rpn = transformToBinaryOperators(expression).rules.flatMap(rule =>
25+
evaluator(rule, variableIdToValuesMap)
26+
);
27+
28+
const {joiner} = expression;
29+
const [left, right] = rpn.splice(-2, 2);
30+
31+
validateJoinerInvoke({joiner, left, right});
32+
33+
rpn.push(joinerHandlers[joiner ?? 'single']({left, right}));
34+
35+
return rpn;
36+
}
37+
38+
const {variableId, operator, value: comparedValue} = expression;
39+
40+
validateRuleInvoke({operator, comparedValue});
41+
42+
return [
43+
ruleHandlers[operator]({
44+
passedValue: variableIdToValuesMap.get(variableId),
45+
comparedValue,
46+
}),
47+
];
48+
};
49+
const [result] = evaluator(expression, variableIdToVariablesMap);
50+
51+
return result;
952
};
1053

11-
export const ruleHandlers = {
54+
type RuleHandlers = Record<
55+
'eq' | 'neq' | 'contains' | 'not_contains',
56+
(params: RuleParameters) => boolean
57+
>;
58+
59+
const caseSensitiveRuleHandlers: RuleHandlers = {
1260
eq: ({passedValue, comparedValue}: RuleParameters) => passedValue === comparedValue,
1361
neq: ({passedValue, comparedValue}: RuleParameters) => passedValue !== comparedValue,
1462
contains: ({passedValue, comparedValue}: RuleParameters) =>
1563
passedValue?.includes(comparedValue) ?? false,
1664
not_contains: ({passedValue, comparedValue}: RuleParameters) =>
1765
passedValue?.includes(comparedValue) === false,
1866
};
67+
const applyRuleWithUpperCase = (
68+
{passedValue, comparedValue}: RuleParameters,
69+
rule: (params: RuleParameters) => boolean
70+
): boolean =>
71+
rule({passedValue: passedValue?.toUpperCase(), comparedValue: comparedValue?.toUpperCase()});
1972

20-
export const expressionToRPN = (
21-
expression: Expression | Rule,
22-
variableIdToValuesMap: Map<string, string>
23-
): StackElement[] => {
24-
if ('rules' in expression) {
25-
const rpn = transformToBinaryOperators(expression).rules.flatMap(rule =>
26-
expressionToRPN(rule, variableIdToValuesMap)
27-
);
28-
29-
const {joiner} = expression;
30-
const [left, right] = rpn.splice(-2, 2);
31-
32-
validateJoinerInvoke({joiner, left, right});
33-
34-
rpn.push(joinerHandlers[joiner ?? 'single']({left, right}));
35-
36-
return rpn;
37-
}
38-
39-
const {variableId, operator, value: comparedValue} = expression;
40-
41-
validateRuleInvoke({operator, comparedValue});
73+
const caseInsensitiveRuleHandlers: RuleHandlers = {
74+
eq: ({passedValue, comparedValue}: RuleParameters) =>
75+
applyRuleWithUpperCase({passedValue, comparedValue}, caseSensitiveRuleHandlers.eq),
76+
neq: ({passedValue, comparedValue}: RuleParameters) =>
77+
applyRuleWithUpperCase({passedValue, comparedValue}, caseSensitiveRuleHandlers.neq),
78+
contains: ({passedValue, comparedValue}: RuleParameters) =>
79+
applyRuleWithUpperCase({passedValue, comparedValue}, caseSensitiveRuleHandlers.contains),
80+
not_contains: ({passedValue, comparedValue}: RuleParameters) =>
81+
applyRuleWithUpperCase({passedValue, comparedValue}, caseSensitiveRuleHandlers.not_contains),
82+
};
4283

43-
return [
44-
ruleHandlers[operator]({
45-
passedValue: variableIdToValuesMap.get(variableId),
46-
comparedValue,
47-
}),
48-
];
84+
const joinerHandlers: Record<'or' | 'and' | 'single', (params: JoinerParameters) => boolean> = {
85+
or: ({left, right}: JoinerParameters) => Boolean(left || right),
86+
and: ({left, right}: JoinerParameters) => Boolean(left && right),
87+
single: ({left}: JoinerParameters) => Boolean(left),
4988
};
5089

5190
const transformToBinaryOperators = (expression: Expression): Expression => {

src/index.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
jest.mock('./evaluation');
22

3-
import {expressionToRPN} from './evaluation';
3+
import {evaluate} from './evaluation';
44
import {evaluateExpression} from './index';
55

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

99
const expression = {
1010
rules: [
@@ -20,7 +20,7 @@ it('should return true', () => {
2020
});
2121

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

2525
const expression = {
2626
rules: [

src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type {Expression, VariableWithValue} from './types';
2-
import {expressionToRPN} from './evaluation';
2+
import {evaluate} from './evaluation';
33

44
export const evaluateExpression = (
55
expression: Expression,
6-
variableValues: VariableWithValue[]
6+
variableValues: VariableWithValue[],
7+
options: {caseSensitive: boolean} = {caseSensitive: false}
78
): boolean => {
89
const variableIdToVariablesMap = new Map(variableValues.map(({id, value}) => [id, value]));
910

10-
const [result] = expressionToRPN(expression, variableIdToVariablesMap);
11+
const result = evaluate({expression, variableIdToVariablesMap, options});
1112

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

0 commit comments

Comments
 (0)