I'm writing these pieces of code to parse expressions in the context of a dice rolling application for DnD. It's pretty much my first try using TypeScript
and I'm not that good in Javascript
. It is also the first time I write a syntax tree.
It's been tested using the Jasmine framework.
I'll split the question in two blocks :
Parsing the expression into tokens
a.k.a converting "1+21-314"
to ['1','+','21','-','314']
). This part of the code is fairly easy but I find there's lot of nesting for such a simple operation, is there maybe a way to take care of this problem using the map/reduce pattern? :
class ExpressionTokenizer {
public parse(expression : string) : Array<string> {
const emptyString : string = "";
let tokens: Array<string> = [];
let buffer : string = emptyString;
for(var i = 0;i < expression.length;i++) {
let character : string = expression[i];
if(isNaN(Number(character))) {
if(buffer !== emptyString) {
tokens.push(buffer);
}
buffer = emptyString;
tokens.push(character);
} else {
buffer += character;
}
}
if(buffer !== emptyString) {
tokens.push(buffer);
}
return tokens;
}
}
Creating a syntax tree from the tokens
The algorithm yields the following results in these test cases :
"1+2+3" => Add(Add(1,2),3)
"1*2+3" => Add(Multiply(1,2),3)
"3*(2+1)" => Multiply(3,Add(2,1))
Again, I don't like nesting. I feel like in such algorithms there's no real choice but maybe I'm wrong.. Is there a way to make the code look better/more explicit?
enum Operator {
None = 0,
Add,
Substract,
Multiply,
Divide,
LeftParenthesis,
RightParenthesis
}
class Node {
constructor(public value:number | Operator, public left?:Node, public right?:Node) {
}
}
class SyntaxParser {
private static map: { [symbol: string]: Operator } = {};
private static populateMap() {
if(Object.keys(SyntaxParser.map).length > 0) return;
SyntaxParser.map["+"] = Operator.Add;
SyntaxParser.map["-"] = Operator.Substract;
SyntaxParser.map["*"] = Operator.Multiply;
SyntaxParser.map["/"] = Operator.Divide;
SyntaxParser.map["("] = Operator.LeftParenthesis;
SyntaxParser.map[")"] = Operator.RightParenthesis;
}
constructor() {
SyntaxParser.populateMap();
}
public parseTokens(expression: string): Node {
if (SyntaxParser.isNumber(expression)) {
return new Node(Number(expression));
}
var tokens = new ExpressionTokenizer().parse(expression);
var stack: Array<Node> = [];
var current: Node = new Node(Operator.None);
for (var index = 0; index < tokens.length; index++) {
var element = tokens[index];
if (SyntaxParser.isNumber(element)) {
SyntaxParser.assignNumber(current, Number(element));
continue;
}
var symbol = SyntaxParser.map[element];
switch (symbol) {
case Operator.LeftParenthesis:
stack.push(current);
current = new Node(Operator.None);
break;
case Operator.RightParenthesis:
var head = stack.pop();
head.right = current;
current = head;
break;
default:
if (current.value == Operator.None) {
current.value = symbol;
} else {
var newNode = new Node(symbol, current, null);
current = newNode;
}
break;
}
}
while (stack.length > 0) {
current = stack.pop();
}
return current;
}
private static isNumber(value: any): boolean {
return !isNaN(Number(value));
}
private static assignNumber(current: Node, value: number) {
if (current.left == null) {
current.left = new Node(value);
} else {
current.right = new Node(value);
}
}
}