Skip to content

Commit 31feade

Browse files
authored
Merge pull request #5 from shelfio/feature/v0.1.0-add-operators
Added operator contains and not_contains, adjusted naming, moved types into separate file
2 parents c780b71 + d8e8cd9 commit 31feade

File tree

7 files changed

+444
-301
lines changed

7 files changed

+444
-301
lines changed

src/evaluation.test.ts

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import type {Expression} from './types';
2+
import {expressionToRPN} from './evaluation';
3+
4+
const baseExpression: Expression = {
5+
joiner: 'AND',
6+
rules: [
7+
{
8+
joiner: 'OR',
9+
rules: [
10+
{
11+
variableId: 'variable-id-c',
12+
operator: 'eq',
13+
value: 'c',
14+
},
15+
{
16+
variableId: 'variable-id-b',
17+
operator: 'eq',
18+
value: 'b',
19+
},
20+
],
21+
},
22+
{
23+
variableId: 'variable-id-a',
24+
operator: 'eq',
25+
value: 'a',
26+
},
27+
],
28+
};
29+
30+
const notBinaryExpression: Expression = {
31+
joiner: 'AND',
32+
rules: [
33+
{
34+
joiner: 'OR',
35+
rules: [
36+
{
37+
variableId: 'variable-id-c',
38+
operator: 'eq',
39+
value: 'c',
40+
},
41+
{
42+
variableId: 'variable-id-b',
43+
operator: 'eq',
44+
value: 'b',
45+
},
46+
{
47+
variableId: 'variable-id-d',
48+
operator: 'eq',
49+
value: 'd',
50+
},
51+
],
52+
},
53+
{
54+
variableId: 'variable-id-e',
55+
operator: 'neq',
56+
value: 'e',
57+
},
58+
{
59+
variableId: 'variable-id-a',
60+
operator: 'eq',
61+
value: 'a',
62+
},
63+
],
64+
};
65+
66+
const singleRuleExpression: Expression = {
67+
rules: [
68+
{
69+
variableId: 'variable-id-a',
70+
operator: 'eq',
71+
value: 'a',
72+
},
73+
],
74+
};
75+
76+
const variablesValueMap = new Map(
77+
[
78+
{
79+
id: 'variable-id-a',
80+
value: 'a',
81+
},
82+
{
83+
id: 'variable-id-b',
84+
value: 'b',
85+
},
86+
{
87+
id: 'variable-id-c',
88+
value: 'wrong-value',
89+
},
90+
{
91+
id: 'variable-id-d',
92+
value: 'wrong-value',
93+
},
94+
{
95+
id: 'variable-id-e',
96+
value: 'wrong-value',
97+
},
98+
].map(({id, value}) => [id, value])
99+
);
100+
101+
describe('expressionToRPN', () => {
102+
it('should return `[true]`', () => {
103+
expect(expressionToRPN(baseExpression, variablesValueMap)).toEqual([true]);
104+
});
105+
106+
it('should return `[true]` for expression w/ non-binary operator', () => {
107+
expect(expressionToRPN(notBinaryExpression, variablesValueMap)).toEqual([true]);
108+
});
109+
110+
it('should return `[true]` for single rule when passed correct variable value', () => {
111+
expect(expressionToRPN(singleRuleExpression, variablesValueMap)).toEqual([true]);
112+
});
113+
114+
it('should return `[false]` for single rule when passed wrong variable value', () => {
115+
const variablesValueMap = new Map([['variable-id-a', 'wrong-value']]);
116+
117+
const expression: Expression = {
118+
rules: [
119+
{
120+
variableId: 'variable-id-a',
121+
operator: 'eq',
122+
value: 'a',
123+
},
124+
],
125+
};
126+
127+
expect(expressionToRPN(expression, variablesValueMap)).toEqual([false]);
128+
});
129+
130+
it('should return `[false]` when variables value not passed', () => {
131+
const variablesValueMap = new Map();
132+
133+
const expression: Expression = {
134+
rules: [
135+
{
136+
variableId: 'variable-id-a',
137+
operator: 'eq',
138+
value: 'a',
139+
},
140+
],
141+
};
142+
143+
expect(expressionToRPN(expression, variablesValueMap)).toEqual([false]);
144+
});
145+
146+
const variableValueSomething = new Map([['variable-id-a', 'something']]);
147+
148+
it('should return `[true]` for expression and `contains` rule', () => {
149+
const expression: Expression = {
150+
rules: [
151+
{
152+
variableId: 'variable-id-a',
153+
operator: 'contains',
154+
value: 'some',
155+
},
156+
],
157+
};
158+
159+
expect(expressionToRPN(expression, variableValueSomething)).toEqual([true]);
160+
});
161+
162+
it('should return `[false]` for expression and `contains` rule', () => {
163+
const expression: Expression = {
164+
rules: [
165+
{
166+
variableId: 'variable-id-a',
167+
operator: 'contains',
168+
value: 'other',
169+
},
170+
],
171+
};
172+
173+
expect(expressionToRPN(expression, variableValueSomething)).toEqual([false]);
174+
});
175+
176+
it('should return `[false]` for expression and `not_contains` rule', () => {
177+
const expression: Expression = {
178+
rules: [
179+
{
180+
variableId: 'variable-id-a',
181+
operator: 'not_contains',
182+
value: 'some',
183+
},
184+
],
185+
};
186+
187+
expect(expressionToRPN(expression, variableValueSomething)).toEqual([false]);
188+
});
189+
190+
it('should return `[true]` for expression and `not_contains` rule', () => {
191+
const expression: Expression = {
192+
rules: [
193+
{
194+
variableId: 'variable-id-a',
195+
operator: 'not_contains',
196+
value: 'other',
197+
},
198+
],
199+
};
200+
201+
expect(expressionToRPN(expression, variableValueSomething)).toEqual([true]);
202+
});
203+
204+
it('should return `[true]` for complex expression', () => {
205+
const complexExpression: Expression = {
206+
joiner: 'AND',
207+
rules: [
208+
baseExpression,
209+
singleRuleExpression,
210+
{
211+
joiner: 'OR',
212+
rules: [
213+
notBinaryExpression,
214+
{
215+
rules: [
216+
{
217+
variableId: 'variable-id-a',
218+
operator: 'eq',
219+
value: 'wrong-value',
220+
},
221+
],
222+
},
223+
],
224+
},
225+
],
226+
};
227+
228+
expect(expressionToRPN(complexExpression, variablesValueMap)).toEqual([true]);
229+
});
230+
231+
it('should return `[false]` for complex expression', () => {
232+
const complexExpression: Expression = {
233+
joiner: 'AND',
234+
rules: [
235+
baseExpression,
236+
singleRuleExpression,
237+
{
238+
joiner: 'AND',
239+
rules: [
240+
notBinaryExpression,
241+
{
242+
rules: [
243+
{
244+
variableId: 'variable-id-a',
245+
operator: 'eq',
246+
value: 'wrong-value',
247+
},
248+
],
249+
},
250+
],
251+
},
252+
],
253+
};
254+
255+
expect(expressionToRPN(complexExpression, variablesValueMap)).toEqual([false]);
256+
});
257+
});

src/evaluation.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type {Expression, Rule, StackElement} from './types';
2+
import type {JoinerParameters, RuleParameters} from './types';
3+
import {validateJoinerInvoke, validateRuleInvoke} from './validations';
4+
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),
9+
};
10+
11+
export const ruleHandlers = {
12+
eq: ({passedValue, comparedValue}: RuleParameters) => passedValue === comparedValue,
13+
neq: ({passedValue, comparedValue}: RuleParameters) => passedValue !== comparedValue,
14+
contains: ({passedValue, comparedValue}: RuleParameters) =>
15+
passedValue?.includes(comparedValue) ?? false,
16+
not_contains: ({passedValue, comparedValue}: RuleParameters) =>
17+
passedValue?.includes(comparedValue) === false,
18+
};
19+
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+
const passedValue = variableIdToValuesMap.get(variableId);
41+
42+
validateRuleInvoke({operator, comparedValue});
43+
44+
return [
45+
ruleHandlers[operator]({
46+
passedValue,
47+
comparedValue,
48+
}),
49+
];
50+
};
51+
52+
const transformToBinaryOperators = (expression: Expression): Expression => {
53+
if (expression.rules.length <= 2) {
54+
return expression;
55+
}
56+
57+
const [firstRule, ...restOfRules] = expression.rules;
58+
const equivalentExpression = transformToBinaryOperators({
59+
joiner: expression.joiner,
60+
rules: restOfRules,
61+
});
62+
63+
return {
64+
joiner: expression.joiner,
65+
rules: [firstRule, equivalentExpression],
66+
};
67+
};

0 commit comments

Comments
 (0)