Skip to content

Commit

Permalink
fix: remove custom rule parser code
Browse files Browse the repository at this point in the history
  • Loading branch information
9sneha-n committed Oct 10, 2024
1 parent 0d1e895 commit 8a065b8
Show file tree
Hide file tree
Showing 2 changed files with 5 additions and 229 deletions.
24 changes: 0 additions & 24 deletions src/data/utils/ruleHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,6 @@ export const getProgramRules = (
return attributeId;
}) || [];

const parsedCondition = condition.replace(
dataElementVariablePattern,
(match, programRuleVar) => {
const dataElementId = programRuleVariables?.find(
programRuleVariable => programRuleVariable.name === programRuleVar
)?.dataElement?.id;

return `#{${dataElementId}}`;
}
);

const attributeParsedCondition = parsedCondition.replace(
attributeVariablePattern,
(match, programRuleVar) => {
const attributeId = programRuleVariables?.find(
programRuleVariable => programRuleVariable.name === programRuleVar
)?.trackedEntityAttribute?.id;

return `#{${attributeId}}`;
}
);

const programRuleActionIds: string[] = actions.map(action => action.id);

const programRuleActions: D2ProgramRuleAction[] | undefined = programRuleActionsResponse
Expand All @@ -84,8 +62,6 @@ export const getProgramRules = (

return {
id: id,
condition: attributeParsedCondition.replace(/d2:/g, "fn:"), //replace d2: with fn: to decouple entity from DHIS
d2Condition: attributeParsedCondition,
originalCondition: condition,
dataElementIds: _(dataElementIds).uniq().compact().value(),
teAttributeIds: _(teaIds).uniq().compact().value(),
Expand Down
210 changes: 5 additions & 205 deletions src/domain/entities/Questionnaire/QuestionnaireRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,6 @@ import { Id } from "../Ref";
import _ from "../generic/Collection";
import { Question } from "./QuestionnaireQuestion";

const RULE_FUNCTIONS = ["fn:hasValue", "fn:daysBetween", "fn:yearsBetween"];
const RULE_OPERATORS = [
">" as const,
">=" as const,
"<" as const,
"<=" as const,
"==" as const,
"!=" as const,
];

export type QuestionnaireRuleActionType =
| "DISPLAYTEXT"
| "DISPLAYKEYVALUEPAIR"
Expand Down Expand Up @@ -52,8 +42,8 @@ export interface QuestionnaireRuleAction {
}
export interface QuestionnaireRule {
id: Id;
condition: string; //condition is parsed with dataelementId e.g: #{dataElementId} == 'Yes'
d2Condition: string; //SNEHA TO DO : remove above condition and use this condition after testing
// condition: string; //condition is parsed with dataelementId e.g: #{dataElementId} == 'Yes'
// d2Condition: string; //SNEHA TO DO : remove above condition and use this condition after testing
originalCondition: string;
dataElementIds: Id[]; // all dataElements in condition (there could be mutiple conditions)
teAttributeIds: Id[]; // all trackedEntityAttributes in condition (there could be mutiple conditions)
Expand All @@ -78,23 +68,8 @@ export const getApplicableRules = (

//2. Run the rule conditions and return rules with parsed results
const parsedApplicableRules = applicableRules.map(rule => {
const customParserResult = parseCondition(rule.condition, updatedQuestion, questions);

const expressionParserResult = parseConditionWithExpressionParser(rule, questions);

//SNEHA DEBUG
if (customParserResult !== expressionParserResult) {
console.debug(
`custom parser and expression parser give diffrent results for rule : ${
rule.id
}, condition : ${
rule.originalCondition
}, custom parser : ${expressionParserResult}, expression parser : ${customParserResult}, value: ${
questions.find(q => q.id === rule.dataElementIds[0])?.value
}`
);
}

return { ...rule, parsedResult: expressionParserResult };
});

Expand All @@ -120,183 +95,6 @@ export const getQuestionValueByType = (question: Question): string => {
}
};

const parseConditionValues = (
condition: string,
updatedQuestion: Question,
questions: Question[]
) => {
return condition.replace(/#\{(.*?)\}/g, (_i, dataElementId) => {
const isDataElementInUpdatedQuestion = updatedQuestion.id === dataElementId;
if (isDataElementInUpdatedQuestion) {
return getQuestionValueByType(updatedQuestion);
} else {
const currentQuestion = questions.find(
(question: Question) => question.id === dataElementId
);
return currentQuestion ? getQuestionValueByType(currentQuestion) : "";
}
});
};

const handleRuleFunctions = (condition: string): boolean => {
const ruleFunction = RULE_FUNCTIONS.find(rulefunc => condition.includes(rulefunc));

switch (ruleFunction) {
case "fn:hasValue": {
const match = condition.match(/fn:hasValue\((.*?)\)/);
if (match) {
const innerString = match[1];
if (innerString?.trim() === "") {
return false;
} else {
return true;
}
} else return false;
}

default:
console.debug(`Unkown rule function: ${ruleFunction}`);
return false;
}
};

const handleCondition = (condition: string): boolean => {
const operator = RULE_OPERATORS.find(ruleOperator => condition.includes(ruleOperator));

if (!operator || !RULE_OPERATORS.includes(operator))
throw new Error(`Operator ${operator} is either undefined or not handled`);

const leftOperand = condition
.substring(0, condition.indexOf(operator))
.replaceAll("'", "")
.trim();

const rightOperandStr = condition
.substring(condition.indexOf(operator))
.replace(operator, "")
.replaceAll("'", "")
.trim();

// Handle right operands boolean values of "1" and "0" for true and false
const rightOperand =
leftOperand === "true" && rightOperandStr === "1"
? "true"
: leftOperand === "false" && rightOperandStr === "0"
? "false"
: rightOperandStr;

switch (operator) {
case "!=": {
return leftOperand !== rightOperand;
}
case "==": {
return leftOperand === rightOperand;
}
case ">": {
try {
return parseFloat(leftOperand) > parseFloat(rightOperand);
} catch {
return false;
}
}
case ">=": {
try {
return parseFloat(leftOperand) >= parseFloat(rightOperand);
} catch {
return false;
}
}
case "<": {
try {
return parseFloat(leftOperand) < parseFloat(rightOperand);
} catch {
return false;
}
}
case "<=": {
try {
return parseFloat(leftOperand) <= parseFloat(rightOperand);
} catch {
return false;
}
}
default:
throw new Error(`Operator ${operator} not handled`);
}
};

const parseAndEvaluateSubCondition = (
subCondition: string,
updatedQuestion: Question,
questions: Question[]
): boolean => {
// Replace #{dataElementId} with actual value from questionnaire
const parsedConditionWithValues = parseConditionValues(
subCondition,
updatedQuestion,
questions
);

// Evaluate the condition
try {
if (RULE_FUNCTIONS.some(ruleFunction => parsedConditionWithValues.includes(ruleFunction))) {
return handleRuleFunctions(parsedConditionWithValues);
} else {
return handleCondition(parsedConditionWithValues);
}
} catch (error) {
console.error(
`Error evaluating condition: ${parsedConditionWithValues} with error : ${error}`
);
return false;
}
};

const parseCondition = (
condition: string,
updatedQuestion: Question,
questions: Question[]
): boolean => {
// Create a regular expression from RULE_FUNCTIONS array
const ruleFunctionsRegex = new RegExp(`(?<!${RULE_FUNCTIONS.join("|")})\\(`);

// Handle parentheses as long as they are not immediately preceded by a value in RULE_FUNCTIONS array
const newCondition =
condition.search(ruleFunctionsRegex) !== -1
? condition.replace(
new RegExp(`(?<!${RULE_FUNCTIONS.join("|")})\\(([^()]+)\\)`, "g"),
(_, subCondition) => {
return parseCondition(subCondition, updatedQuestion, questions)
? "true"
: "false";
}
)
: condition;

// Split condition into sub-conditions based on logical operators
const andConditions = newCondition.split("&&").map(subCondition1 => {
const orConditions = subCondition1.split("||").map(subCondition2 => {
const notCondition = subCondition2.trim().replace("(", "").startsWith("!");
const trimmedSubCondition = notCondition
? subCondition2.trim().substring(1)
: subCondition2;

const result =
trimmedSubCondition.replace(/\s/g, "") === "true"
? true
: trimmedSubCondition.replace(/\s/g, "") === "false"
? false
: parseAndEvaluateSubCondition(trimmedSubCondition, updatedQuestion, questions);

return notCondition ? !result : result;
});

return orConditions.some(condition => condition);
});

return andConditions.every(condition => condition);
};

function getProgramRuleVariableValues(
programRuleVariables: Maybe<D2ProgramRuleVariable[]>,
questions: Question[]
Expand Down Expand Up @@ -341,7 +139,9 @@ const parseConditionWithExpressionParser = (rule: QuestionnaireRule, questions:
programRuleVariableValues
);
} catch (error) {
console.error(`Error parsing rule condition: ${rule.condition} with error : ${error}`);
console.error(
`Error parsing rule condition: ${rule.originalCondition} with error : ${error}`
);
return false;
}
};

0 comments on commit 8a065b8

Please sign in to comment.