Building an Expression Validator Using React

Building an Expression Validator Using React

In this article we are going to design a simple expression validator that can validate user entered expression using some set of rules.

Sometimes writing an expression can be difficult. Generally an expression consists of components like operators, identifiers, constants and functions. So, we are going to create an expression validator for validating the expressions. It can use data like a set of variables or functions coming from an API to make our work easier as it help us to efficiently create an expression by providing the features like hinting the available variable name, highlighting the error in case of an invalid expression, etc.

Implementation

Basically there are two components of expression validator:-

  • Mentions dropdown
  • Parser

Mentions Dropdown :-

In the expression builder, we show the list of available variables using react-mentions . In the current implementation the list of available options will get triggered whenever the user is entering the '$' character. We can change this using the custom regular expression(regex) as shown:

in.gif Here is code snippet of the react-mentions part :-

<MentionsInput
          className="customMentionInput"
          id="logicTextArea"
          value={value}
          onChange={(
            event: { target: { value: string } },
            newValue: string,
            newPlainTextValue: string
          ) => {
            hanldeAreaInputValue(newPlainTextValue);
          }}
          style={defaultMentionStyles}
        >
          <Mention
            trigger={/(?:^|)(\$([^\s$]*))$/} // with space = /(?:^|\s)(\$([^\s\$]*))$/
            markup={`$[____display____]`}
            data={dataSetForLogic}
            displayTransform={(id, display) => `$'${display}'`}
            appendSpaceOnAdd={true}
          />
    </MentionsInput>

MentionsInput is like a normal textInput component which will only accept Mention as a child component. Therefore, we have provided a regex for the trigger property /(?:^|)(\$([^\s$]*))$/. It will cause the Mention component to trigger querying the data source whenever it encounter's the '$' character.

displayTransform property accepts a function for customising the string that is displayed for a mention.

Parser :-

Parser takes expression as an input, tokenises the data and classifies the tokens into different categories. Here we are classifying the tokens into four different categories:

  • Variables
  • Identifiers
  • Operators
  • Numbers

Here is the code snippet to tokenise the data:-

const tokenList = [];
    const numberRegex = /[0-9]+(\.[0-9]*)?([eE][+-]?[0-9]+)?/;
    const operatorRegex = /AND|OR|and|or|XOR|&&|<=|>=|<|>|!=|==|&|OR*|!|\|{1,2}/;
    const variableRegex = /\$'[A-Za-z_][A-Za-z_0-9]+'+((\[([0-9]+)\])*)?/;
    const identifierRegex = /[A-Za-z_][A-Za-z_0-9]*/;
    const otherCharRegex = /\S/g;

    const reToken = new RegExp(
      numberRegex.source +
        "|" +
        operatorRegex.source +
        "|" +
        variableRegex.source +
        "|" +
        identifierRegex.source +
        "|" +
        otherCharRegex.source,
      otherCharRegex.flags
    );

    for (;;) {
      const match = reToken.exec(text);
      if (match === null) {
        break;
      }
    tokenList.push(new Token(match[0], match.index));
    }

Here, we are creating the regex reToken which will execute the text and return an array containing the result of that search. If the value of match is not null, a token is inserted into the tokenList. Classification of token is done using the following logic :-

export class Token {
  private text: string;
  private index: number;
  private kind: "identifier" | "number" | "operator" | "variable";
  constructor(text: string, index: number) {
    this.text = text;
    this.index = index;

    // Classify the token.
    if (/^\$'[A-Za-z_][A-Za-z_0-9]+'/.test(text)) {
      this.kind = "variable";
    } else if (/^[A-Za-z_]/.test(text)) {
      this.kind = "identifier";
    } else if (/^[0-9]/.test(text)) {
      this.kind = "number";
    } else {
      this.kind = "operator";
    }
  }
}

After classifying the tokens, we will parse the token list using the recursive descent parsing technique. For parser implementation, I have taken the references from the following article .

There are some set of rules that need to be satisfied for generating the parse tree :-

  • There must be one operator between the two operands.
  • The small parenthesis must be balanced.
  • Only the available variable names can be used to create the expression.

If the entered expression satisfies the above rules a parse tree will be generated successfully. An example of this is shown below:

Screenshot 2021-02-23 at 11.55.20 PM.png Here is the json for parse tree corresponding to the input text $'item1'+ $'item2':

SplitWise Model - Page 2 (1).png

To Conclude

If the entered expression does not satisfy the required set of rules, then the parser will generate an error message along with the token data wherever the error is found. Here is the sample example of the error message caused due to the wrong variable name and unbalanced parenthesis:

expressionValidator.gif You can play around with the expression editor by visiting the following link:- https://aashishgtbit.github.io/expression-editor/

Github : github.com/Aashishgtbit/expression-editor.

Hope you all liked the article.

Thank you