If you have the time, and you feel like learning something cool, you can create a very expression parser for this.
A good recipe to follow in a parser is to create the reading methods like this:
DadoDeRetorno LerAlgumaCoida(string codigo, ref int pos)
{
// lê algo do código e avança a posição de leitura
}
This DadoDeRetorno
depends on what you want to do.
-
interpret: in this case the output of most methods is the result of the interpretation. Since you will not know the type to be returned beforehand, then it will be object
same.
-
generate syntax tree: in this case the output will be a syntactic tree node. This is the case, for example, of wanting to use Expression Trees, from LINQ, which you can then compile, which will give you a lot of performance gain.
-
compile: Few languages can be compiled directly from code, but an expression parser is probably an exception. In this case, you will probably want to return a string / array already with the translation to the target language.
For every thing that is successfully handled, you should update the pos
variable.
In addition, the return of each function should indicate whether or not the method succeeded to make things easier.
You will need methods like this template to read spaces, to read variable names, to read numbers, among others:
bool LerEspacos(string codigo, ref int pos); // retorna false se não ler nada
string LerNomeDeVariavel(string codigo, ref int pos); // retorna null se não ler nada
int? LerNumeroInteiro(string codigo, ref int pos); // retorna null se não ler nada
Each one can also receive other values relevant to each type of parser.
For example, when making an interpreter, you must pass the values of the variables. If it's a compiler, you might need to pass a list of the registers in use and maybe even change that list.
You will also need a method to process the expressions. This goes into the same model, but within it, the implementation is a bit more complicated.
Expression LerExpressao(string codigo, ref int pos, Contexto contexto);
In this case you need two stacks, one for operators, and one for operands.
Whenever an operand is found, you stack it in the list of operands.
When an operator is found, you need to stack it on the operator stack, but only if it has higher precedence than the one already on the stack. Otherwise, it is necessary to perform the operation on the stack with the operands that are in the operand stack.
Example of the steps of an interpreter with two cells:
Entrada: 1 + 3 * 5 + 8
// Lê 1 da entrada e põe na pilha de operandos
Operandos: 1
Operadores:
Entrada: + 3 * 5 + 8
// Lê "+" da entrada e põe na pilha de operadores
Operandos: 1
Operadores: +
Entrada: 3 * 5 + 8
// Lê 3 da entrada e põe na pilha de operandos
Operandos: 1 3
Operadores: +
Entrada: * 5 + 8
// Lê "*" da entrada e põe na pilha de operadores
Operandos: 1 3
Operadores: + *
Entrada: 5 + 8
// Lê 5 da entrada e põe na pilha de operandos
Operandos: 1 3 5
Operadores: + *
Entrada: + 8
// Não pode empilhar + sobre *
// Executa a operação 3 * 5 = 15
// Remove o operador * da pilha
// Remove os operandos 3 e 5 da pilha
// Adiciona o 15 e o + nas respectivas pilhas
Operandos: 1 15
Operadores: + +
Entrada: 8
Operandos: 1 15 8
Operadores: + +
// Acabou, agora basta ir executando todos os itens das pilhas
// 15 + 8 = 23
Operandos: 1 23
Operadores: +
// 1 + 23 = 24
Operandos: 24
Operadores:
// o resultado é o que sobra na pilha de operandos = 24