The open source project I work on uses Antlr4 pretty heavily, but I don't know much about it. I thought I would use April's Community Challenge as an opportunity to learn something about grammars, lexers, and parsers.
My calculator only handles integer multiplication, division, addition, and subtraction, but it does handle the order of operations correctly. Whitespace is stripped away, so it's optional, and the calculator will throw an ArgumentException
if it's passed an expression that it doesn't understand.
Is there anything I could have done better? I do think I want to go back and add support for exponentiation and parenthesis later, but that's later.
BasicMathGrammar.g4
grammar BasicMath;
/*
* Parser Rules
*/
compileUnit : expression+ EOF;
expression :
expression MULTIPLY expression #Multiplication
| expression DIVIDE expression #Division
| expression ADD expression #Addition
| expression SUBTRACT expression #Subtraction
| NUMBER #Number
;
/*
* Lexer Rules
*/
NUMBER : INT; //Leave room to extend what kind of math we can do.
INT : ('0'..'9')+;
MULTIPLY : '*';
DIVIDE : '/';
SUBTRACT : '-';
ADD : '+';
WS : [ \t\r\n] -> channel(HIDDEN);
IntegerMathVisitor.cs
class IntegerMathVisitor : BasicMathBaseVisitor<int>
{
public override int VisitCompileUnit(BasicMathParser.CompileUnitContext context)
{
// There can only ever be one expression in a compileUnit. The other node is EOF.
return Visit(context.expression(0));
}
public override int VisitNumber(BasicMathParser.NumberContext context)
{
return int.Parse(context.GetText());
}
public override int VisitAddition(BasicMathParser.AdditionContext context)
{
var left = WalkLeft(context);
var right = WalkRight(context);
return left + right;
}
public override int VisitSubtraction(BasicMathParser.SubtractionContext context)
{
var left = WalkLeft(context);
var right = WalkRight(context);
return left - right;
}
public override int VisitMultiplication(BasicMathParser.MultiplicationContext context)
{
var left = WalkLeft(context);
var right = WalkRight(context);
return left * right;
}
public override int VisitDivision(BasicMathParser.DivisionContext context)
{
var left = WalkLeft(context);
var right = WalkRight(context);
return left / right;
}
private int WalkLeft(BasicMathParser.ExpressionContext context)
{
return Visit(context.GetRuleContext<BasicMathParser.ExpressionContext>(0));
}
private int WalkRight(BasicMathParser.ExpressionContext context)
{
return Visit(context.GetRuleContext<BasicMathParser.ExpressionContext>(1));
}
ThrowExceptionErrorListener.cs (This lets me throw exceptions on bad input.)
class ThrowExceptionErrorListener : BaseErrorListener, IAntlrErrorListener<int>
{
//BaseErrorListener implementation
public override void SyntaxError(IRecognizer recognizer, IToken offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e)
{
throw new ArgumentException("Invalid Expression: {0}", msg, e);
}
//IAntlrErrorListener<int> implementation
public void SyntaxError(IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e)
{
throw new ArgumentException("Invalid Expression: {0}", msg, e);
}
}
Calculator.cs
public static class Calculator
{
public static int Evaluate(string expression)
{
var lexer = new BasicMathLexer(new AntlrInputStream(expression));
lexer.RemoveErrorListeners();
lexer.AddErrorListener(new ThrowExceptionErrorListener());
var tokens = new CommonTokenStream(lexer);
var parser = new BasicMathParser(tokens);
var tree = parser.compileUnit();
var visitor = new IntegerMathVisitor();
return visitor.Visit(tree);
}
}
Unit Tests
[TestClass]
public class EvaluateTests
{
[TestMethod]
public void OrderedOperation()
{
var expr = "1 + 6 - 2 * 3 / 2";
// 2 * 3 = 6
// 6 / 2 = 3
// 1 + 6 = 7
// 7 - 3 = 4
Assert.AreEqual(4, Calculator.Evaluate(expr));
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void BadInput()
{
var expr = "1 + 5 + 2(3)";
int value = Calculator.Evaluate(expr);
}
[TestMethod]
public void SimpleAddition()
{
Assert.AreEqual(2, Calculator.Evaluate("1 + 1"));
}
[TestMethod]
public void RepeatedAddition()
{
Assert.AreEqual(10, Calculator.Evaluate("1 + 2 + 3 + 4"));
}
[TestMethod]
public void SimpleSubtraction()
{
Assert.AreEqual(2, Calculator.Evaluate("4 - 2"));
}
[TestMethod]
public void RepeatedSubtraction()
{
Assert.AreEqual(5, Calculator.Evaluate("10 - 3 - 2"));
}
[TestMethod]
public void SimpleMultiplication()
{
Assert.AreEqual(4, Calculator.Evaluate("2 * 2"));
}
[TestMethod]
public void RepeatedMultiplication()
{
Assert.AreEqual(8, Calculator.Evaluate("2 * 2 * 2"));
}
[TestMethod]
public void SimpleDivision()
{
Assert.AreEqual(2, Calculator.Evaluate("4 / 2"));
}
[TestMethod]
public void RepeatedDivision()
{
Assert.AreEqual(2, Calculator.Evaluate("8 / 2 / 2"));
}
}
0.5 * 3 == 1 / 6
which is false. That is just evaluating the brackets portion as BEDMAS requires brackets be evaluated first. \$\endgroup\$