From ae1ba01ccf7fced821824adc1490193effea2dc2 Mon Sep 17 00:00:00 2001 From: DrewsAbuse Date: Tue, 14 Mar 2023 11:53:22 +0100 Subject: [PATCH] feat: DECTREEAPI-235 implemented case insensitive string comparing --- src/evaluation.test.ts | 97 ++++++++++++++++++++++++++---------- src/evaluation.ts | 109 ++++++++++++++++++++++++++++------------- src/index.test.ts | 6 +-- src/index.ts | 7 +-- 4 files changed, 151 insertions(+), 68 deletions(-) diff --git a/src/evaluation.test.ts b/src/evaluation.test.ts index 07a6339..0ca430c 100644 --- a/src/evaluation.test.ts +++ b/src/evaluation.test.ts @@ -1,5 +1,5 @@ import type {Expression} from './types'; -import {expressionToRPN} from './evaluation'; +import {evaluate} from './evaluation'; const baseExpression: Expression = { joiner: 'and', @@ -73,15 +73,15 @@ const singleRuleExpression: Expression = { ], }; -const variablesValueMap = new Map( +const variablesValueMap = new Map( [ { id: 'variable-id-a', - value: 'a', + value: 'A', }, { id: 'variable-id-b', - value: 'b', + value: 'B', }, { id: 'variable-id-c', @@ -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 = { @@ -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 = { @@ -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: [ { @@ -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: [ { @@ -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: [ { @@ -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: [ { @@ -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: [ @@ -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: [ @@ -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( + [ + { + 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); }); }); diff --git a/src/evaluation.ts b/src/evaluation.ts index c593440..878d1bf 100644 --- a/src/evaluation.ts +++ b/src/evaluation.ts @@ -1,14 +1,62 @@ -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; + options?: {caseSensitive: boolean}; +}): StackElement => { + const {caseSensitive} = options; + const ruleHandlers = caseSensitive ? caseSensitiveRuleHandlers : caseInsensitiveRuleHandlers; + + const evaluator = ( + expression: Expression | Rule, + variableIdToValuesMap: Map + ): 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) => @@ -16,36 +64,27 @@ export const ruleHandlers = { 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 -): 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 => { diff --git a/src/index.test.ts b/src/index.test.ts index 1b3aedb..f839cec 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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: [ @@ -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: [ diff --git a/src/index.ts b/src/index.ts index 3ea655f..b404996 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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', {