Skip to content

Commit

Permalink
feat(FeedbackRule): Add support for using parentheses in expression (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
hirokiterashima authored Dec 12, 2022
1 parent cd823dc commit 3b71c76
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 82 deletions.

This file was deleted.

56 changes: 3 additions & 53 deletions src/assets/wise5/components/common/feedbackRule/FeedbackRule.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FeedbackRuleExpression } from './FeedbackRuleExpression';

export class FeedbackRule {
id?: string;
expression: string;
feedback?: string | string[];
prompt?: string;
static operatorPrecedences = { '!': 2, '&&': 1, '||': 1 };

constructor(jsonObject: any = {}) {
for (const key of Object.keys(jsonObject)) {
Expand All @@ -25,58 +26,7 @@ export class FeedbackRule {
return feedbackRule.expression === 'isDefault';
}

// uses shunting-yard algorithm to get expression in postfix (reverse-polish) notation
getPostfixExpression(): string[] {
const result = [];
const operatorStack = [];
for (const symbol of this.getExpressionAsArray()) {
if (FeedbackRule.isOperator(symbol)) {
while (operatorStack.length > 0) {
const topOperatorOnStack = operatorStack[operatorStack.length - 1];
if (
FeedbackRule.hasGreaterPrecedence(topOperatorOnStack, symbol) ||
FeedbackRule.hasSamePrecedence(topOperatorOnStack, symbol)
) {
result.push(operatorStack.pop());
} else {
break;
}
}
operatorStack.push(symbol);
} else {
result.push(symbol);
}
}
while (operatorStack.length > 0) {
result.push(operatorStack.pop());
}
return result;
}

private getExpressionAsArray(): string[] {
return this.expression
.replace(/ /g, '')
.split(/(&&|\|\||!)/g)
.filter((el) => el !== '');
}

static isOperator(symbol: string): boolean {
return ['&&', '||', '!'].includes(symbol);
}

static isOperand(symbol: string): boolean {
return !this.isOperator(symbol);
}

static hasGreaterPrecedence(symbol1: string, symbol2: string): boolean {
return this.getPrecedence(symbol1) > this.getPrecedence(symbol2);
}

static hasSamePrecedence(symbol1: string, symbol2: string): boolean {
return this.getPrecedence(symbol1) === this.getPrecedence(symbol2);
}

static getPrecedence(symbol: string): number {
return FeedbackRule.operatorPrecedences[symbol] ?? 0;
return new FeedbackRuleExpression(this.expression).getPostfix();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FeedbackRuleComponent } from '../../feedbackRule/FeedbackRuleComponent';
import { CRaterResponse } from '../cRater/CRaterResponse';
import { FeedbackRule } from './FeedbackRule';
import { FeedbackRuleExpression } from './FeedbackRuleExpression';
import { TermEvaluator } from './TermEvaluator/TermEvaluator';
import { TermEvaluatorFactory } from './TermEvaluator/TermEvaluatorFactory';

Expand Down Expand Up @@ -104,10 +105,9 @@ export class FeedbackRuleEvaluator {
response: CRaterResponse | CRaterResponse[],
feedbackRule: FeedbackRule
): boolean {
const postfixExpression = feedbackRule.getPostfixExpression();
const termStack = [];
for (const term of postfixExpression) {
if (FeedbackRule.isOperand(term)) {
for (const term of feedbackRule.getPostfixExpression()) {
if (FeedbackRuleExpression.isOperand(term)) {
termStack.push(term);
} else {
this.evaluateOperator(term, termStack, response);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FeedbackRuleExpression } from './FeedbackRuleExpression';

describe('FeedbackRuleExpression', () => {
getPostfix();
});

function getPostfix() {
describe('getPostfix()', () => {
it('should convert text to array in postfix order', () => {
expectPostfixExpression('1 && 2 && 3', ['1', '2', '&&', '3', '&&']);
expectPostfixExpression('1 || 2 && 3', ['1', '2', '||', '3', '&&']);
expectPostfixExpression('!1 && !2', ['1', '!', '2', '!', '&&']);
expectPostfixExpression('1 && isSubmitNumber(2)', ['1', 'isSubmitNumber(2)', '&&']);
expectPostfixExpression('(1 && 2) || (3 && 4)', ['1', '2', '&&', '3', '4', '&&', '||']);
expectPostfixExpression('!(1 || 2) && 3', ['1', '2', '||', '!', '3', '&&']);
expectPostfixExpression('(!1 || (2 && 3)) || 4', ['1', '!', '2', '3', '&&', '||', '4', '||']);
});
});
}

function expectPostfixExpression(text: string, expectedResult: string[]) {
const expression = new FeedbackRuleExpression(text);
expect(expression.getPostfix()).toEqual(expectedResult);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
export class FeedbackRuleExpression {
static operatorPrecedences = { '!': 2, '&&': 1, '||': 1 };

constructor(private text: string) {}

// uses shunting-yard algorithm to get expression in postfix (reverse-polish) notation
getPostfix(): string[] {
const result = [];
const operatorStack = [];
for (const symbol of this.getExpressionAsArray()) {
if (FeedbackRuleExpression.isOperator(symbol)) {
this.processOperator(symbol, operatorStack, result);
} else if (FeedbackRuleExpression.isLeftParenthesis(symbol)) {
operatorStack.push(symbol);
} else if (FeedbackRuleExpression.isRightParenthesis(symbol)) {
this.processRightParenthesis(operatorStack, result);
} else {
result.push(symbol);
}
}
while (operatorStack.length > 0) {
result.push(operatorStack.pop());
}
return result;
}

private getExpressionAsArray(): string[] {
return this.text
.replace(/ /g, '')
.split(
/(hasKIScore\(\d\)|ideaCountEquals\(\d\)|ideaCountLessThan\(\d\)|ideaCountMoreThan\(\d\)|isSubmitNumber\(\d+\)|&&|\|\||!|\(|\))/g
)
.filter((el) => el !== '');
}

private processOperator(symbol: string, operatorStack: string[], result: string[]): void {
while (operatorStack.length > 0) {
const topOperatorOnStack = operatorStack[operatorStack.length - 1];
if (
FeedbackRuleExpression.hasGreaterPrecedence(topOperatorOnStack, symbol) ||
FeedbackRuleExpression.hasSamePrecedence(topOperatorOnStack, symbol)
) {
result.push(operatorStack.pop());
} else {
break;
}
}
operatorStack.push(symbol);
}

private processRightParenthesis(operatorStack: string[], result: string[]): void {
let topOperatorOnStack = operatorStack[operatorStack.length - 1];
while (!FeedbackRuleExpression.isLeftParenthesis(topOperatorOnStack)) {
result.push(operatorStack.pop());
topOperatorOnStack = operatorStack[operatorStack.length - 1];
}
operatorStack.pop(); // discard left parenthesis
}

static isLeftParenthesis(symbol: string): boolean {
return symbol === '(';
}

static isRightParenthesis(symbol: string): boolean {
return symbol === ')';
}

static isOperator(symbol: string): boolean {
return ['&&', '||', '!'].includes(symbol);
}

static isOperand(symbol: string): boolean {
return !this.isOperator(symbol);
}

static hasGreaterPrecedence(symbol1: string, symbol2: string): boolean {
return this.getPrecedence(symbol1) > this.getPrecedence(symbol2);
}

static hasSamePrecedence(symbol1: string, symbol2: string): boolean {
return this.getPrecedence(symbol1) === this.getPrecedence(symbol2);
}

static getPrecedence(symbol: string): number {
return this.operatorPrecedences[symbol] ?? 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ <h3 i18n>Introduction</h3>
#1 will be shown to the student.<br />
If the rule matches a second time (in a subsequent round), Feedback #2 will be shown, and so on.
</p>
<p i18n><b>Expressions</b> are made up of terms and operators:</p>
<p i18n><b>Expressions</b> are made up of terms, operators, and parentheses:</p>
<h3 i18n>Terms</h3>
<p i18n>Terms lets you specify what to look for in a student response.</p>
<table class="app-bg-bg">
Expand Down Expand Up @@ -111,6 +111,27 @@ <h3 i18n>Operators</h3>
</td>
</tr>
</table>
<h3 i18n>Parentheses</h3>
<p i18n>Parentheses let you group expressions and prioritize evaluation.</p>
<p i18n>You can add multiple parentheses in an expression. You can even nest parentheses.</p>
<table class="app-bg-bg">
<tr>
<th i18n>Example</th>
<th i18n>Description</th>
</tr>
<tr>
<td class="operator">!(4a || 12)</td>
<td i18n>Evaluates to true if neither idea 4a nor idea 12 were found.</td>
</tr>
<tr>
<td class="operator">5a && (4a || 12)</td>
<td i18n>Evaluates to true if idea 5a and either idea 4a or idea 12 was found.</td>
</tr>
<tr>
<td class="operator">(5a && (4a || 12)) && hasKIScore(3)</td>
<td i18n>Evaluates to true if idea 5a and either idea 4a or idea 12 was found, and the student received a KI score of 3.</td>
</tr>
</table>
</mat-dialog-content>
<mat-dialog-actions fxLayoutAlign="end">
<button mat-button mat-dialog-close cdkFocusRegionstart i18n>Close</button>
Expand Down
61 changes: 57 additions & 4 deletions src/messages.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">116</context>
<context context-type="linenumber">137</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/directives/dialog-with-close/dialog-with-close.component.html</context>
Expand Down Expand Up @@ -1800,6 +1800,10 @@ Click &quot;Cancel&quot; to keep the invalid JSON open so you can fix it.</sourc
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">87</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">120</context>
</context-group>
</trans-unit>
<trans-unit id="7491b1ebfc5dd457492a5c481e2d2254216dc1fc" datatype="html">
<source>Description required</source>
Expand Down Expand Up @@ -13137,7 +13141,7 @@ Are you ready to receive feedback on this answer?</source>
<source>Thanks for submitting your response.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/FeedbackRuleEvaluator.ts</context>
<context context-type="linenumber">8</context>
<context context-type="linenumber">9</context>
</context-group>
</trans-unit>
<trans-unit id="ac4a8ebf1347b29327308613dd307970ec899941" datatype="html">
Expand Down Expand Up @@ -13236,8 +13240,8 @@ Are you ready to receive feedback on this answer?</source>
<context context-type="linenumber">8,11</context>
</context-group>
</trans-unit>
<trans-unit id="f7b73495b143340f88fb636d660bde8a62016d55" datatype="html">
<source><x id="START_BOLD_TEXT" ctype="x-b" equiv-text="&lt;b&gt;"/>Expressions<x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="&lt;/b&gt;"/> are made up of terms and operators:</source>
<trans-unit id="d8b6142c3d02b165711db70e94cc65d49a2dfabe" datatype="html">
<source><x id="START_BOLD_TEXT" ctype="x-b" equiv-text="&lt;b&gt;"/>Expressions<x id="CLOSE_BOLD_TEXT" ctype="x-b" equiv-text="&lt;/b&gt;"/> are made up of terms, operators, and parentheses:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">12</context>
Expand Down Expand Up @@ -13376,6 +13380,55 @@ Are you ready to receive feedback on this answer?</source>
<context context-type="linenumber">108,111</context>
</context-group>
</trans-unit>
<trans-unit id="46b1ed215e53246db19c5bde05ea87e08a5c2705" datatype="html">
<source>Parentheses</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">114</context>
</context-group>
</trans-unit>
<trans-unit id="8df5a6f6a83598ccc0f22b86f8ca75661ef5a302" datatype="html">
<source>Parentheses let you group expressions and prioritize evaluation.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">115</context>
</context-group>
</trans-unit>
<trans-unit id="dae69adfd569cdbaa366470ec24471bf01c7db66" datatype="html">
<source>You can add multiple parentheses in an expression. You can even nest parentheses.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">116</context>
</context-group>
</trans-unit>
<trans-unit id="6db909a826f1878ad78bf853a62b6cc5cbda3d3f" datatype="html">
<source>Example</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">119</context>
</context-group>
</trans-unit>
<trans-unit id="f5d726bee0fd8a545258990d929f39b68549dd87" datatype="html">
<source>Evaluates to true if neither idea 4a nor idea 12 were found.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">124</context>
</context-group>
</trans-unit>
<trans-unit id="a8eebb4afda178aca31b3fdc61eb0f6d91837041" datatype="html">
<source>Evaluates to true if idea 5a and either idea 4a or idea 12 was found.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">128</context>
</context-group>
</trans-unit>
<trans-unit id="8dd00a754ebd92878be379ef601abcaccde63b44" datatype="html">
<source>Evaluates to true if idea 5a and either idea 4a or idea 12 was found, and the student received a KI score of 3.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/assets/wise5/components/common/feedbackRule/feedback-rule-help/feedback-rule-help.component.html</context>
<context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit id="dc34bee07a6ceed8f010d89d1fcbda15a5abee8c" datatype="html">
<source>Teaching Tips</source>
<context-group purpose="location">
Expand Down

0 comments on commit 3b71c76

Please sign in to comment.