fun with GTK#
Recently I’ve resumed work on a GTK# project I started three years ago: SnazzyCalculator, hosted on Github. I originally started the project out of curiosity about GTK#, and it’s been fun to develop – I know no one needs another calculator tool. I do intend to develop it into what I’ve been calling a wordulator: type in something like “one plus one equals” and it’ll respond “two”. Feel free to laugh, Jon keeps teasing me about it certainly. :P Anyway, what’s been really fun since I’ve resumed development is getting the parser and equation solver to work. I found this awesome tutorial by Eric White on writing a recursive descent parser in C#, and that gave me the bulk of my parser code. I did discover two bugs in his code that I fixed; I’ll share my fixes with you.
ParserException
from white space generation
In his Expression.Produce
, he has the following:
1 2 3 4 |
return new Expression( Enumerable.Repeat(new WhiteSpace(), whiteSpaceBefore), n, Enumerable.Repeat(new WhiteSpace(), whiteSpaceAfter)); |
This caused a ParserException
for me because of the checks in the Symbol(params Object[] symbols)
constructor. Basically, Enumerable.Repeat
returned an Enumerable/<CreateRepeatIterator>
instead of either a Symbol
or IEnumerable<Symbol>
like the Symbol
constructor expected, so Symbol
wigged out and threw a ParserException
. I ended up adding the following to my WhiteSpace
class:
1 2 3 4 5 6 7 8 9 |
public static IEnumerable<Symbol> CreateWhiteSpace(int amount) { List<Symbol> ws = new List<Symbol>(); for (int i=0; i<amount; i++) { ws.Add(new WhiteSpace()); } return (IEnumerable<Symbol>)ws; } |
Then in Expression.Produce
, instead of Eric’s code, I had:
1 2 |
return new Expression(WhiteSpace.CreateWhiteSpace(whiteSpaceBefore), n, WhiteSpace.CreateWhiteSpace(whiteSpaceAfter)); |
Mismatched parentheses
Parsing an expression like (1+2)\*(2+3)
resulted in an exception because of the order in which his NospaceExpression.Produce
method constructs new NospaceExpression
instances. His code was looking for an opening parenthesis and a closing parenthesis first; then, if that didn’t match, checking for an Expression
followed by an InfixOperator
followed by another Expression
. That resulted in it extracting 1+2)\*(2+3
, i.e. ditching the first and last parentheses, and trying to treat that as an Expression
, which it isn’t. I just moved the following chunk of code down a bit, so it came after return new NospaceExpression(e1, io, e2);
:
1 2 3 4 5 6 7 8 9 10 |
if (symbols.First() is OpenParenthesis && symbols.Last() is CloseParenthesis) { Expression e = Expression.Produce(symbols.Skip(1).SkipLast(1)); if (e != null) { return new NospaceExpression(new OpenParenthesis(), e, new CloseParenthesis()); } } |
This results in first trying to find an expression, then some operator that works on two expressions, then a final expression. And since an Expression
consists of another NospaceExpression
, which can then consist of parentheses surrounding another Expression
, you wind up with (1+2)
being seen as its own Expression
, then \*
as an InfixOperator
, then finally (2+3)
as the last Expression
.
After I got the parser working as expected, I had to make it actually do something with its parse – that is, solve the equation. I did that by adding a Solve
method to several classes that returned a double
representing its value. That was easiest for the DecimalDigit
class, since it just returns its own numeric value. I worked up from there, determining what the symbols could be in each class, so I could determine how to take those symbols and produce some double
result. A fun part was adding the following method to InfixOperator
that returns a function that knows how to solve an equation involving two terms, depending on which operator the InfixOperator
contains:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public Func<double, double, double> GetSolver() { Symbol sym = ConstituentSymbols[0]; if (sym is Plus) { return (ad1, ad2) => ad1 + ad2; } if (sym is Minus) { return (t1, t2) => t1 - t2; } if (sym is Asterisk) { return (f1, f2) => f1 * f2; } if (sym is ForwardSlash) { return (dividend, divisor) => { if (0 == divisor) { throw new DivideByZeroException(); } return dividend / divisor; }; } if (sym is Caret) { return (t1, t2) => Math.Pow(t1, t2); } return (t1, t2) => { throw new ParserException("Don't know how to solve " + "expression with InfixOperator " + sym.GetType().ToString()); }; } |
So now my SnazzyCalculator calculates, and it seems to do so correctly, following PEMDAS. It correctly reports “ERROR” when you give it a crap equation like *1.32.34))
, or you divide by zero. The next step is to write some tests for it, to ensure I don’t accidentally break the parser or solver while tinkering with other bits. I’ve been fiddling with the GUI, and while it looks all right, I don’t like how I’m styling it. I’m applying the fonts and colors programmatically, instead of in the gui.stetic XML file where all the widgets are laid out. It seems like I ought to be able to describe the appearance of each button in the XML somewhere, but I haven’t found any example showing how.