|
What's the difference
between the other calculator and this one?
As you know, my other
calculator tutorial is a basic calculator, similar to MS Calculator.
For example:
1
<Press the + button>
2
<Press the = button>
<Output: 3>
This new calculator, however, will put your skills on the line,
because by the end of this tutorial, we'll be looking at a
calculator where you can type in anything you want into the text
area, and then press enter. It'll be able to perform complex
operations like this (with order of operations) :
((1 + 2) / (14.5 * 10 ^ (2 * Sin (Cos (12 +
99) + 14) - 2))) ^ 3 * 3
Heh. Hell yeah.
Rules
Rules? Yep. Rules.
Since we're parsing the text, the text will need to come in exactly
the way we want it. For example in VB.NET you can't really say EndIf
You have to have spaces in between End and If. Similarly, our
calculator will have to have spaces in between the numbers.
1+2 <--- Not valid
1 + 2 <--- Valid
Sin(1+2) <-- Not valid
Sin(1 + 2) <-- STILL not valid
Sin (1 + 2) <-- Valid
This is the reason I opted to use a Label as the calculator
display as opposed a Textbox. If we had a textbox the user would
type in something silly such as "The Answer to Life The Universe and
Everything Else." Which, for the misinformed, is 42. Ask Google,
search "Answer to Life The Universe And Everything Else =" and it'll
tell you.
Got off track a bit, sorry.
The Basic Code
Ok, as you may (or may not) know, I'm not very good when it comes to
aesthetics. I'm more of a practical kind of guy, I'm pretty bad when
it comes to graphics. Create an interface similar to this one
(Doesn't matter how it looks or how everything's arranged, just
create the buttons)

I named all the buttons like so:
Left side of the screen:
btn0........btn9
btnDot
btnPlusOrMinus <-- makes the number negative
btnToggleMode <--- this toggles between Degrees and Radians
btnEquals
Right side of the screen:
btnPlus ............. btnDivide
btnOpenParentheseese....btnCloseParentheseese
btnSin.......btnATan
btnStore.....btnRecall
btnClear...btnDelete
btnSquare, btnPower, btnSqrt, btnXRt
Output
lblDisplay - The big thing at the top
lblStore - The thing to the right of it
lblHelp - The thing at the bottom
Pretty basic, and pretty practical.
One quick announcement: You can download
Part I of the source code, which contains the layout already
made, and the buttons displaying things on lblDisplay. Part
II is the finished calculator, and that will be up for download
as well.
Alright, now it's time to make the
buttons display things on the screen. I'm assuming you have a solid
knowledge of Visual Basic and you should be able to do the
following. This is the code for the click events of all the buttons:
For buttons 0 - 9, and btnDot use the following code:
lblDisplay.Text += "0" (replace 0 with the
number, or a dot(period))
For btnPi:
lblDisplay.Text += Math.PI.ToString
btnPlusOrMinus (this button will make a number negative)
lblDisplay.Text += "-"
For all the operators:
lblDisplay.Text += " + " (Replace this with
the appropriate symbol and NOTE THE SPACING)
Opening and closing parentheseese:
lblDisplay.Text += "("
lblDisplay.Text += ")"
Trignometry Functions:
lblDisplay.Text += "Sin ("
lblDisplay.Text += "ArcSin ("
Replace the above with the appropriate function name and NOTE THE
SPACING)
btnSquare:
lblDisplay.Text += " ^ 2"
btnPower:
lblDisplay.Text += " ^ "
btnSqrt:
lblDisplay.Text += "Sqrt ("
btnXRt:
lblDisplay.Text += " ^ (1 / "
'Ex - Cube root of 4: 4 ^ (1 / 3) <-- just a little algebra
btnClear:
lblDisplay.Text = ""
btnDelete:
lblDisplay.Text =
lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1) 'Remove 1
character
Again. You don't have to type all this in. You
can download Part I of the source code for everything we've
done up to this point (see end of tutorial for links to source
code).
The ExpressionParser Class: v1.0 (Sort
between Numbers and Operators)
The program is already ~700
lines of code in Form1 because of all the buttons and the code
(although it doesn't accomplish much).
The most useful bulk of code for this tutorial is the
ExpressionParser class. This class will parse (read and understand)
what you type in, and it'll give back the answer. In my mind, this
class cannot be done in '1 try'. I originally programmed this class
in Java for school (we had an entire week to do a calculator, and
since I've done it several times and written a tutorial on it
for vb, I finished in about a day..... so then I decided to make an
"Advanced Expression Parsing Calculator" which takes in the equation
all at once.... and so I did! Now I'm writing a tutorial for it in
VB).
That "1 week" that I programmed this ExpressionParser class in was
full of trial and error, and I believe that Trial and Error is
the best way to learn. Although in this tutorial you're
generally doing what I tell you.... I'll give you older versions of
my class (that have fatal mistakes in them) on purpose, so that when
we do the next version, it gets revised and we figure out what we're
doing wrong, and as you get to the end of this tutorial you'll have
a pretty good sense of the development process. In my mind this
calculator can't be done perfectly in one try. What we're going to
do is work on a basic calculator, and then start adding features to
it instead of doing it all at once.
Alright. The first thing I decided to work on was BASIC
operations, without Order of Operations. By basic operations
I mean 1 + 2 - 3 type. If you type in something like 4 + 4 / 8 you'd
get 1 instead of 4.5 (because Order of Operations isn't implemented
in this version).
Create a new class called
ExpressionParser.
Imports System.IO
Public Class ExpressionParser
End Class
Since our expressionparser will return an answer back to the
user, we'll have to ... well... return a value! Create a new
function
Public Function ParseEquation(ByVal
Equation As String) As Double
End Function
First things first. We'll need to distinguish between the
numbers and the operators.
1 + 2 - 3
1st item is a number(1)
2nd item is an operator(+)
3rd item is a number(2)
4th item is an operator(-)
5th item is a number(3)
Basically every other item is an operator, and every other item is a
number. The thing that's in between the numbers and the
operators is a space. We're going to use String.Split(" ") to
split up the numbers.
Brief oveview of String.Split:
Dim String1 as String
Dim String2() as String 'note: String2 is an array
String1 = "1,99,3"
String2 = String1.Split(",") 'The splitter char is a comma
MessageBox.Show(String2(0)) 'Messageboxes "1"
MessageBox.Show(String2(1)) 'Messageboxes "99"
MessageBox.Show(String2(2)) 'Messageboxes "2"
MessageBox.Show(String2(3)) 'Error. There is no 4th element
in the array.
Alright, so we're basically doing the same thing, except splitting
by spaces instead of commas. Every other item is an operator,
and every other item is a number. If you remember from the TicTacToe
tutorial, we use Mod 2 to do "Every other one". For example:
0 Mod 2 <-- this is 0
1 Mod 2 <---this is 1
2 Mod 2 <-- this is 0
3 Mod 2 <-- this is 1
4 Mod 2 <-- this is 0
5 Mod 2 <-- this is 1
Mod just returns the remainder (ex: 5/2, the remainder is 1). So we
can use it to check for "every other one" in this case.
Your ParseEquation sub should look like this:
Public Function ParseEquation(ByVal Equation As String) As Double
Dim Numbers As ArrayList
Dim Operators As ArrayList
Dim SplitEquation() As String
Dim x As Integer
SplitEquation = Equation.Split(" ")
For x = 0 To SplitEquation.Length - 1
If x Mod 2 = 0 Then
MessageBox.Show("Number: " & SplitEquation(x))
Numbers.Add(SplitEquation(x))
End If
If x Mod 2 = 1 Then
MessageBox.Show("Operator: " & SplitEquation(x))
Operators.Add(SplitEquation(x))
End If
Next
End Function
Look over the code. It's pretty
straightforward. If you're not sure what an ArrayList is, please
check the Alternative To Arrays tutorial.
Now in your Form1: Dim Parser As New
ExpressionParser
In the Click event for the btnEquals:
Parser.ParseEquation(lblDisplay.Text)
Run it. Type in "1 + 2 - 3" and it should tell you what's a
number and what's an operator. Go back to the EquationParser class,
and delete the MessageBox lines (they're annoying anyways).
The ExpressionParser Class: v2.0
(Perform Multiple Calculations)
Now it's time to take the given numbers, and the given operators,
and actually make it calculate something!
Take my example again:
1 + 2 - 3
Since we're not doing order of operations now, obviously we'll have
to read the expression from left to right.
Number 1: 1
Operator 1: +
Number 2: 2
Operator 2: -
Number 3: 3
As you know, calculations are
performed by grouping. In this case: Groups of 2 numbers. Instead of
"Doing it all at once", we're going to split it up.
We're going to do it from left to right in groups of 2 numbers:
1 + 2 first
3 - 3 last
Take a look at my numbers/operators list again. In between number X
and number (X + 1), lies the operator (X), where X is the current
operation. Sounded confusing. Let's do an example.
For the first operation (X = 1):
Number 1:
1
Operator 1: +
Number 2: 2
In between Number X (which is Number 1... which is equal to 1) and
number (X + 1) (which is number 2.... which is equal to 2) lies the
operator X (which is Operator 1... which is +). So for Number 1 and
Number 2, the operation you're going to perform is Operator 1.
Create a new sub:
Private Function CalculateAnswer(ByVal Numbers As ArrayList, ByVal Operators As ArrayList) As Double
Dim x As Integer
Dim answer As Double
For x = 0 To Operators.Count - 1
If Operators(x) = "+" Then
answer = Numbers(x) + Numbers(x + 1)
End If
If Operators(x) = "-" Then
answer = Numbers(x) - Numbers(x + 1)
End If
If Operators(x) = "*" Then
answer = Numbers(x) * Numbers(x + 1)
End If
If Operators(x) = "/" Then
answer = Numbers(x) / Numbers(x + 1)
End If
If Operators(x) = "^" Then
answer = Numbers(x) ^ Numbers(x + 1)
End If
MessageBox.Show("Performing Operation: " & Numbers(x) & Operators(x) & Numbers(x + 1) & " = " & answer)
Next
End Function
Now in your ParseEquation Sub, say CalculateAnswer(numbers,Operators).
Run the program: Type in 1 + 2
You get 12???
Why? The problem lies in this line
answer = Numbers(x) + Numbers(x + 1)
It's concatenating the two STRINGS. This problem is easy to fix:
answer = Convert.ToDouble(Numbers(x)) + Convert.ToDouble(Numbers(x + 1))
Do that for all the operations.
Now type in 1 + 2 and you'll get 3.
But type in 1 + 2 - 3
You'll get; "1 + 2 = 3"
And : "2 - 3 = -1"
Obviously. The reason being is that we haven't done anything with the answer. We have to put the answer back into the equation.
Explanation time:
1 + 2 - 3 ------------> 3 - 3 -------------> 0
So we know that Numbers(x), numbers (x + 1) and Operators (x) must be deleted. In this case:
1 + 2 - 3
Numbers(0) = 1
Operators(0) = +
Numbers(1) = 2
We delete all the above, and replace them with 3.
Although this might seem weird, ArrayLists are automatically bumped down.
Take this example:
You have an arraylist with 3 elements
myAList(0) = 99
myAList(1) = "Red"
myAList(2) = "Balloons"
Got it? (ArrayLists can store anything: numbers, strings, objects, even arraylists... you name it)
Say you delete myAList(0). The arraylist now looks like this:
myAList(0) = "Red"
myAList(1) = 'Balloons"
By deleting myAList(0), it does NOT look like this:
myAList(0) = <Nothing/Null/Nada>
myAList(1) = "Red"
myAList(2) = "Balloons"
The ArrayList is automatically bumped down.
This is important. Because when we delete number(0), to delete the next number, we delete number(0) AGAIN.
For example:
1 + 2 - 3
To delete the "1", the "+", and the "2" we'd do:
Numbers.RemoveAt(0)
Operators.RemoveAt(0)
Numbers.RemoveAt(0)
And to add the answer back into the array:
Numbers.Insert(0, answer) 'The 0 argument makes it get inserted in the beginning of the arraylist.
In terms of X:
Numbers.RemoveAt(X)
Operators.RemoveAt(X)
Numbers.RemoveAt(X)
Numbers.Insert(X, answer) 'The X argument makes it get inserted in the X Position of the arraylist.
Great. Here's your new code:
Private Function CalculateAnswer(ByVal Numbers As ArrayList, ByVal Operators As ArrayList) As Double
Dim x As Integer
Dim answer As Double
For x = 0 To Operators.Count - 1
'If there are no more operators...
If Operators.Count = 0 Then Return answer 'Then exit! We've got the answer!
If Operators(x) = "+" Then
answer = Convert.ToDouble(Numbers(x)) + Convert.ToDouble(Numbers(x + 1))
End If
If Operators(x) = "-" Then
answer = Convert.ToDouble(Numbers(x)) - Convert.ToDouble(Numbers(x + 1))
End If
If Operators(x) = "*" Then
answer = Convert.ToDouble(Numbers(x)) * Convert.ToDouble(Numbers(x + 1))
End If
If Operators(x) = "/" Then
answer = Convert.ToDouble(Numbers(x)) / Convert.ToDouble(Numbers(x + 1))
End If
If Operators(x) = "^" Then
answer = Convert.ToDouble(Numbers(x)) ^ Convert.ToDouble(Numbers(x + 1))
End If
MessageBox.Show("Performing Operation: " & Numbers(x) & Operators(x) & Numbers(x + 1) & " = " & answer)
'Remove 2 numbers and an operator...
Numbers.RemoveAt(x)
Operators.RemoveAt(x)
Numbers.RemoveAt(x)
'... and replace them with an answer
Numbers.Insert(x, answer)
'Back up a bit. We just added a number, we need to backtrack
x -= 1
Next
End Function
One line that I added was
x -= 1
I'll explain why you have to do that, with an example
1 + 2 - 3 'Our starting equation
'First loop. X = 0
Numbers(X) = 1
Numbers(X + 1) = 2
1 + 2 - 3 -------------> 3 - 3
'Second loop. X = 1
Numbers(X) = 3 (the second 3)
Numbers(X + 1) = <Nothing>
If we backtrack, our 2nd loop would look like this:
1 + 2 - 3 -------------> 3 - 3
'Second loop. X = 0
Numbers(X) = 3 (the first 3)
Numbers(X + 1) = 3 (the second 3)
That's the reasoning behind that.
Go back to your ParseEquation sub. The very last line should be:
Return CalculateAnswer(Numbers, Operators)
I added the return statement. If you have an existing "CalculateAnswer(Numbers, Operators)" line, just delete it.
Go back to form1, and in the Equals click button:
lblDisplay.Text = Parser.ParseEquation(lblDisplay.Text)
Run your program! You can type in whatever you want in the display (besides the Parantheseese, Square Root, and the Trig functions).
Just note 1 thing. If you wanted to say -1 + 2, you'd type it in like so:
<PlusOrMinus key> 1 <Plus Key> 2
The PlusOrMinus is used to make something negative
Another interesting feature:
1 + 2 - 5
<Press Equals, it'll give you -2>
Without pressing clear, type this in:
* 5
<Press Equals, It'll give you -10>
Pretty awesome huh? Multiple operations work!
Now, do 4 + 4 / 8. It gives you 1 instead of 4.5.......... so it's time for ORDER OF OPERATIONS!
The ExpressionParser Class: v3.0 (Order of Operations)
Order of operations is actually pretty simple. Remember your PEMDAS:
P - Parentheseese
E - Exponents
M - Multiplication
D - Division
A - Addition
S - Subtraction
v3.0 of this class won't do Parentheseese yet. So we're stuck with EMDAS for now =).
It's actually pretty simple. The logic:
1) Sort through the operators and find what we're looking for
2) Find what position it's at and record it
3) Perform the required operation
4) Insert the answer back into the arraylist.
5) Repeat
4 + 4 / 8
Number(0) = 4
Number(1) = 4
Number(2) = 8
Operator(0) = +
Operator(1) = /
The first operation we have to do is 4 / 8 ..... which is Number(1) Operator(1) Number(2).
Step 1: Sort through the opeartors and find what we're looking for
This is a simple for loop:
For x = 0 to Operators.Count - 1
If Operators(x) = "/" Then ..... <Do Something>
Next
Step 2: Find what position it's at and record it
In the for loop:
If Operators(x) = "/" Then
Index = X
End If
Step 3: Perform the required operation
So if we're searching through the operator and we come across the index of the operator "/", we know it's (in this case) 1. Call 1 "X" since everything will be in
terms of X for loops.
The operands (numbers) are Numbers(X), and Numbers(X + 1).
So our code would look like this:
If Operators(x) = "/" Then
Index = X
Answer = answer = Numbers(x) / Numbers(x + 1) 'The exact same line we have!
End If
Step 4: Insert the answer back into the arraylist.
'Remove 2 numbers and an operator...
Numbers.RemoveAt(x)
Operators.RemoveAt(x)
Numbers.RemoveAt(x)
'... and replace them with an answer
Numbers.Insert(x, answer)
Step 5: Repeat
That's what the loop is for.
HELLO? This is EXACTLY what we've
been doing all along! The ONLY difference is that now, we're
SEARCHING for an operator and performing an operation instead of
going from left to right!
Private Function CalculateAnswer(ByVal Numbers As ArrayList, ByVal Operators As ArrayList, ByVal OperationToPerform As String) As Double
Dim x As Integer
Dim answer As Double
For x = 0 To Operators.Count - 1
'If there are no more operators...
If Operators.Count = 0 Then Return answer 'Then exit! We've got the answer!
'For some odd reason, EVEN THOUGH the loop goes
'from 0 to operators.count - 1... it ends up BEING operators.count
If x = Operators.Count Then Exit Function
If Operators(x) = OperationToPerform Then
If Operators(x) = "+" Then
answer = Convert.ToDouble(Numbers(x)) + Convert.ToDouble(Numbers(x + 1))
End If
If Operators(x) = "-" Then
answer = Numbers(x) - Numbers(x + 1)
End If
If Operators(x) = "*" Then
answer = Numbers(x) * Numbers(x + 1)
End If
If Operators(x) = "/" Then
answer = Numbers(x) / Numbers(x + 1)
End If
If Operators(x) = "^" Then
answer = Numbers(x) ^ Numbers(x + 1)
End If
MessageBox.Show("Performing Operation: " & Numbers(x) & Operators(x) & Numbers(x + 1) & " = " & answer)
'Remove 2 numbers and an operator...
Numbers.RemoveAt(x)
Operators.RemoveAt(x)
Numbers.RemoveAt(x)
'... and replace them with an answer
Numbers.Insert(x, answer)
'Back up a bit. We just added a number, we need to backtrack
x -= -1
End If
Next
End Function
Note the OperationToPerform argument.
You should be aware of this new
line:
If x = Operators.Count Then Exit
Function
I don't know why, but even though the loop says:
For x = 0 To Operators.Count - 1
X still gets set to
Operators.Count - Not too sure why though.
Alright.
Now go back to ParseEquation, and delete your Return
CalculateAnswer(Numbers,Operators) line.
Replace it with:
'PEMDAS.... or in this case: EMDAS
CalculateAnswer(Numbers, Operators, "^")
CalculateAnswer(Numbers, Operators, "*")
CalculateAnswer(Numbers, Operators, "/")
CalculateAnswer(Numbers, Operators, "+")
CalculateAnswer(Numbers, Operators, "-")
What do we return? Well there's going to be only 1 number remaining
in the Numbers ArrayList, so that's the answer of course!
Return Numbers(0)
Well there you have it: Order of operations.
4 + 4 / 8 will give you 4.5. Give it a shot.
Sweet. Order of Operations wasn't so hard was it? I thought it was
the hardest thing in the world when I programmed this last month. I
stopped to pseudocode, and planned things out (stepwise) and ended
up realizing that it's the exact same thing as left-to-right, with a
few modifications involved.
Now it's time to get
Parentheseese working!
The ExpressionParser Class: v4.0 (Parentheseese)
Actually, at first I told
myself I wouldn't be doing parentheses (The calculator was for my
Java final exam). But I ended up sitting down for 3 hours and
figuring it out.
The concept is WAY easier than I had imagined.
(I learned this the hard way): BEFORE JUMPING TO CODE. BE SURE TO
PSEUDOCODE!!
As you know, Parentheses work from "Innermost to Outermost".
(4 + (2 - 3)) + 1
What is the innermost expression? "2 - 3" of course. So we'd do that
first. The equation would then be:
(4 + -1) + 1
The innermost expression would then be "4 + -1" and the equation
would be
3 + 1
Which would be 4.
In our MINDS we can figure out what the innermost expression
is. But in CODE how will the compiler know what it is? We
have to point it in the right direction.
(4 + (2 - 3)) + 1
In this case, the innermost expression is between the LAST
OPENING Parentheses and the FIRST CLOSING PARENTHESES.
And from here on, we'd simply parse the 2 - 3 (with the existing
code we have).
Alright, I learned through trial and error (it took me at least an
hour to figure this out) that I was wrong.
Take this expression:
(1 + 2) / (3 - 2)
Between the last opening parentheses and the first closing
parentheses...... well........ it doesn't really exist (if you're
reading right to left it would be the division sign, but obviously
you don't do anything right to left in math).
I had these crazy ideas in my head about parentheses grouping,
parentheses counting, how many parentheses there are, asking the
user what the innermost parentheses was..... WOAH I got
confused.
After an hour, I was about to shoot myself. The answer was
extremely simple.
Get this:
The innermost expression is the expression that contains the
FIRST CLOSING parentheses. It has nothing to do with the last
opening parentheses.
Once you close the parentheses, you're out of the innermost
block.
What we have to do is loop through the equation and find the
parentheses, and record the indices.
Private Function EvaluateAndSimplyParentheticalExpressions(ByVal Equation As String) As String
Dim PositionOfInnerMostOpen As Integer
Dim PositionOfInnerMostClosed As Integer
Dim x As Integer
'Find and retreive the innermost parenthesese
For x = 0 To Equation.Length - 1
If Equation.Chars(x) = "(" Then
PositionOfInnerMostOpen = x
End If
If Equation.Chars(x) = ")" Then
PositionOfInnerMostClosed = x
Exit For
End If
Next
MsgBox(PositionOfInnerMostOpen & "," & PositionOfInnerMostClosed)
End Function
That method works every time.
Do this
Equation = EvaluateAndSimplyParentheticalExpressions(Equation)
in ParseEquation BEFORE SplitEquation = Equation.Split(" ")
Give it a shot by typing in a complex parenthetical expression:
(4 + (2 - 3)) + 1
and then
(1 + 2) / (3 - 2)
<-- "Innermost" from Left to Right though
Obviously after giving you the position of the parentheses it'll
error out because it doesn't understand parentheses yet, but we'll
fix that :P.
Type in an expression without
parentheses. It'll tell you that the parentheses are at position 0,
0.
--------
--------
Here's what we're going to do next:
-After getting the position of the parentheses that contain the
innermost expression, we're going to store the innermost expression
in a string.
-we'll take that string, and call ParseEquation to simplify
that string (sounds very confusing I know)
Just remember that the inner most parenthetical expression is
an expression WITHOUT parentheses.
Instead of sounding so cryptic, let me give you the answer directly.
Create a new sub:
Private Sub TokenizeEquation(ByVal Numbers As ArrayList, ByVal Operators As ArrayList, ByVal Equation As String)
Dim SplitEquation() As String
Dim x As Integer
SplitEquation = Equation.Split(" ")
For x = 0 To SplitEquation.Length - 1
If x Mod 2 = 0 Then
' MessageBox.Show("Number: " & SplitEquation(x))
Numbers.Add(SplitEquation(x))
End If
If x Mod 2 = 1 Then
' MessageBox.Show("Operator: " & SplitEquation(x))
Operators.Add(SplitEquation(x))
End If
Next
End Sub
And change your ParseEquation like so:
Public Function ParseEquation(ByVal Equation As String) As Double
Dim Numbers As New ArrayList
Dim Operators As New ArrayList
Dim answer As Double
Equation = EvaluateAndSimplyParentheticalExpressions(Equation)
TokenizeEquation(Numbers, Operators, Equation)
CalculateAnswer(Numbers, Operators, "^")
CalculateAnswer(Numbers, Operators, "*")
CalculateAnswer(Numbers, Operators, "/")
CalculateAnswer(Numbers, Operators, "+")
CalculateAnswer(Numbers, Operators, "-")
answer = Numbers(0)
Return answer
End Function
All I've done is moved some code from ParseEquation to TokenizeEquation, and called TokenizeEquation from the ParseEquation sub.
You'll understand why I'm doing so, in a minute.
Meanwhile, edit your EvaluateAndSimplifyParentheticalExpressions like so:
Private Function EvaluateAndSimplyParentheticalExpressions(ByVal Equation As String) As String
Dim PositionOfInnerMostOpen As Integer
Dim PositionOfInnerMostClosed As Integer
Dim x As Integer
Dim ParRemaining As Boolean = True
Dim Numbers As ArrayList
Dim Operators As ArrayList
'Expression in the parentheses
Dim ExpressionInPar As String
While ParRemaining
'Reset it, becuase this is a loop
'and we don't want any existing values from the previous iteration
x = 0
PositionOfInnerMostOpen = 0
PositionOfInnerMostClosed = 0
ExpressionInPar = ""
Numbers = New ArrayList
Operators = New ArrayList
'Find and retreive the innermost parenthesese
For x = 0 To Equation.Length - 1
If Equation.Chars(x) = "(" Then
PositionOfInnerMostOpen = x
End If
If Equation.Chars(x) = ")" Then
PositionOfInnerMostClosed = x
Exit For
End If
Next
'If there's no parentheses, Then exit.
If PositionOfInnerMostOpen = 0 And PositionOfInnerMostClosed = 0 Then
ParRemaining = False
Return Equation
End If
End While
End Function
I hope you understand what's going on. All I've really done is added a loop, since we'll be checking for parentheses until there are none left.
What we're going to do next is simply evaluate the expression in parentheses.
Right now we've got the position of the opening and the closing parentheses. How do we get the numbers in between?
Right after that last If statement, add this:
For x = PositionOfInnerMostOpen + 1 To PositionOfInnerMostClosed - 1
ExpressionInPar += Equation.Chars(x)
Next
MessageBox.Show("Expression in Parentheses: " & ExpressionInPar)
Run the program with the equation
(4 + (2 - 3 + 1)) + 1
It'll tell you that the Expression in Parentheses is "2 - 3 + 1" (Except, it'll be in a loop. Just exit out of the program using the Stop button in VS.NET to quit).
How do we evaluate that? Heh. We already have the code to evaluate this!
TokenizeEquation(Numbers, Operators, Equation)
CalculateAnswer(Numbers, Operators, "^")
CalculateAnswer(Numbers, Operators, "*")
CalculateAnswer(Numbers, Operators, "/")
CalculateAnswer(Numbers, Operators, "+")
CalculateAnswer(Numbers, Operators, "-")
There you go! Now it's time to:
1) Delete the expression that was in parentheses
2) Put the answer back into the equation
Pretty simple:
Private Function EvaluateAndSimplyParentheticalExpressions(ByVal Equation As String) As String
Dim PositionOfInnerMostOpen As Integer
Dim PositionOfInnerMostClosed As Integer
Dim x As Integer
Dim ParRemaining As Boolean = True
Dim Numbers As ArrayList
Dim Operators As ArrayList
'Expression in the parentheses
Dim ExpressionInPar As String
While ParRemaining
'Reset it, becuase this is a loop
'and we don't want any existing values.
x = 0
PositionOfInnerMostOpen = 0
PositionOfInnerMostClosed = 0
ExpressionInPar = ""
Numbers = New ArrayList
Operators = New ArrayList
'Find and retreive the innermost parenthesese
For x = 0 To Equation.Length - 1
If Equation.Chars(x) = "(" Then
PositionOfInnerMostOpen = x
End If
If Equation.Chars(x) = ")" Then
PositionOfInnerMostClosed = x
Exit For
End If
Next
If PositionOfInnerMostOpen = 0 And PositionOfInnerMostClosed = 0 Then
ParRemaining = False
Return Equation
End If
For x = PositionOfInnerMostOpen + 1 To PositionOfInnerMostClosed - 1
ExpressionInPar += Equation.Chars(x)
Next
MessageBox.Show(ExpressionInPar)
TokenizeEquation(Numbers, Operators, ExpressionInPar)
CalculateAnswer(Numbers, Operators, "^")
CalculateAnswer(Numbers, Operators, "*")
CalculateAnswer(Numbers, Operators, "/")
CalculateAnswer(Numbers, Operators, "+")
CalculateAnswer(Numbers, Operators, "-")
Dim Answer As Double = Numbers(0)
Dim AlreadyAddedAnswer As Boolean = False
Dim TemporaryEquation As String = ""
For x = 0 To Equation.Length - 1
If x >= PositionOfInnerMostOpen And x <= PositionOfInnerMostClosed Then
If Not AlreadyAddedAnswer Then
TemporaryEquation += Answer.ToString
AlreadyAddedAnswer = True
End If
Else
TemporaryEquation += Equation.Chars(x)
End If
Next
MessageBox.Show("New Equation: " & TemporaryEquation)
Equation = TemporaryEquation
End While
End Function
Everything in blue represents the changes made.
Explanation of the For Loop:
2 - (3 + 4) + 1
The parentheses, in this case, are in positions 5 and 10.
What the loop will do is write "2 - ".... and skip the part with the parentheses and only write the answer, and then continue on to do " + 1"
So it'll turn into 2 - 7 + 1
Run the program, type in:
(4 + ((8 - 3) + 2)) / 11
It'll do this:
(4 + ((8 - 3) + 2)) / 11 ----> (4 + (5 + 2)) / 11 ----> (4 + 7) / 11 ---> 11 / 11 ---> 1
Be sure you enter the parentheses correctly, or it'll come up with some weird answer.
The ExpressionParser Class: v5.0 (Bug Fixes and Quirks)
Every now and then you have to start stress-testing. There's a few bugs in our program.
Take the equation (4 + (2 - 3 + 1)) + 1
It'll do the 2 - 3 + 1 first. But since we told it to do Addition before Subtraction.... it'll do this
2 - 3 + 1 -----> 2 - 4 ------> -2
Whereas Addition and Subtraction should be performed at the same time so the answer should be 0
2 - 3 + 1 ---> -1 + 1 ----> 0
We'll fix that, same with Multiplication and Division (although I've never seen a case where it actually matters).
It's quite a simple fix:
Private Function CalculateAnswer(ByVal Numbers As ArrayList, ByVal Operators As ArrayList, ByVal OperationToPerform As String) As Double
Dim x As Integer
Dim answer As Double
For x = 0 To Operators.Count - 1
Dim FoundOperationToDo As Boolean = False
'If there are no more operators...
If Operators.Count = 0 Then Return answer 'Then exit! We've got the answer!
'For some odd reason, EVEN THOUGH the loop goes
'from 0 to operators.count - 1... it ends up BEING operators.count
If x = Operators.Count Then Exit Function
If OperationToPerform = "Exponent" Then
If Operators(x) = "^" Then
answer = Convert.ToDouble(Numbers(x)) ^ Convert.ToDouble(Numbers(x + 1))
FoundOperationToDo = True
End If
End If
If OperationToPerform = "MultiplicationAndDivision" Then
If Operators(x) = "*" Then
answer = Convert.ToDouble(Numbers(x)) * Convert.ToDouble(Numbers(x + 1))
FoundOperationToDo = True
End If
If Operators(x) = "/" Then
answer = Convert.ToDouble(Numbers(x)) / Convert.ToDouble(Numbers(x + 1))
FoundOperationToDo = True
End If
End If
If OperationToPerform = "AdditionAndSubtraction" Then
If Operators(x) = "+" Then
answer = Convert.ToDouble(Numbers(x)) + Convert.ToDouble(Numbers(x + 1))
FoundOperationToDo = True
End If
If Operators(x) = "-" Then
answer = Convert.ToDouble(Numbers(x)) - Convert.ToDouble(Numbers(x + 1))
FoundOperationToDo = True
End If
End If
If FoundOperationToDo Then
MessageBox.Show("Performing Operation: " & Numbers(x) & Operators(x) & Numbers(x + 1) & " = " & answer)
'Remove 2 numbers and an operator...
Numbers.RemoveAt(x)
Operators.RemoveAt(x)
Numbers.RemoveAt(x)
'... and replace them with an answer
Numbers.Insert(x, answer)
'Back up a bit. We just added a number, we need to backtrack
x -= 1
End If
Next
End Function
Instead of giving it individual operators, we'll just say "MultiplicationAndDivision" or "AdditionAndSubtraction" or "Exponent". Pretty simple.
Go back to your code.
In both ParseEquation and EvaluateAndSimplyParentheticalExpressions, change the lines to:
CalculateAnswer(Numbers, Operators, "Exponent")
CalculateAnswer(Numbers, Operators, "MultiplicationAndDivision")
CalculateAnswer(Numbers, Operators, "AdditionAndSubtraction")
Now run your code.
(4 + (2 - 3 + 1)) + 1 ----> (4 + 0) + 1 ----> 4 + 1 -----> 5
One more problem. Take the
delete key for example. It deletes 1 character.
1 + 2
If you push delete, the 2 will be deleted. If you push it again, the
space before the two will be deleted. This is kind of a
quirky thing, the end user won't really be able to comprehend that,
So, we'll just make it so that if you hit delete, and there's a
space.... delete that space as well :P.
In form1, the Click event of btnDelete:
lblDisplay.Text =
lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1) 'Remove 1
character
'while there are still spaces left...
While lblDisplay.Text.Chars(lblDisplay.Text.Length - 1) = " "
'... keep deleting them
lblDisplay.Text =
lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1)
End While
Great, now type in "1 + 2" and hit delete. It'll delete the
two and the space before the two.
One more quirk:
Type in "1 + ". If you hit delete, it'll only delete the space.
Obviously we have to fix that.
I'm sure there's a better way of doing this. I guess a bit of
laziness kicked in:
'while there are still spaces left...
While lblDisplay.Text.Chars(lblDisplay.Text.Length - 1) = " "
'... keep deleting them
lblDisplay.Text = lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1)
End While
lblDisplay.Text = lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1) 'Remove 1 character
'while there are still spaces left...
While lblDisplay.Text.Chars(lblDisplay.Text.Length - 1) = " "
'... keep deleting them
lblDisplay.Text = lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1)
End While
Heh. Whatever. So long as it works.
The ExpressionParser Class: v6.0 (Functions - Trig Functions and Sqrt)
Now we have to work on the trig functions and the Sqrt function.
If you run your program and push the keys <1> <+> <Tan>, the Display will look like this: "1 + Tan("
What the user of this program will have to do is simply use it like an argument:
1 + Tan (30 / 2)
Since everything in Parentheses is automatically simplified, the code ends up looking like so:
1 + Tan 15 - 1
Here we go (again)
With our current code:
1st item = Number
2nd item = Operator
3rd item = Number
4th item = Operator
5th item = Number
6th item = Operator
That would mean that Tan is a number and 15 is an operator, and - is a number and 1 is an operator. No way.
Simply evaluate the Tangent of 15, and convert it to a number (.2679 or so), delete "Tangent" and "15" and replace it with .2679.
A bit harder than you think. It took me a few tries to write this function (I had this crazy idea of doing it all at once), but if you break down the code, it's easy to understand:
Private Function TokenizeEquation(ByVal Numbers As ArrayList, ByVal Operators As ArrayList, ByVal Equation As String)
Dim SplitEquation() As String
Dim x As Integer
SplitEquation = Equation.Split(" ")
Dim NumbersAndOperators As New ArrayList
'This will store all the numbers and operators in an arraylist
For x = 0 To SplitEquation.Length - 1
Dim CurrentItem As String = SplitEquation(x)
NumbersAndOperators.Add(CurrentItem)
If CurrentItem = "Sin" Or CurrentItem = "Cos" Or CurrentItem = "Tan" _
Or CurrentItem = "ArcSin" Or CurrentItem = "ArcCos" Or CurrentItem = "ArcTan" Or _
CurrentItem = "Sqrt" Then
'This will calculate the Sin of whatever number....etc
'The reason for SplitEquation(x + 1) is becuase the
'number is always follwed AFTER the function
'Ex: Sin 30
CurrentItem = PerformOperation(CurrentItem, SplitEquation(x + 1))
'Remove the original thing ("Sin" or "Cos"...etc)
NumbersAndOperators.RemoveAt(x)
'And add the actual number
NumbersAndOperators.Add(CurrentItem)
'We're goin ahead, because we already evaluated
'splitEquation(x) and splitequation(x + 1)
x += 1
End If
Next
For x = 0 To NumbersAndOperators.Count - 1
If x Mod 2 = 0 Then
Numbers.Add(NumbersAndOperators(x))
End If
If x Mod 2 = 1 Then
Operators.Add(NumbersAndOperators(x))
End If
Next
End Function
Private Function PerformOperation(ByVal theFunction As String, ByVal Number As Double) As Double
Dim answer As Double
Select Case theFunction
Case "Sin"
answer = Math.Sin(Number)
Case "Cos"
answer = Math.Cos(Number)
Case "Tan"
answer = Math.Tan(Number)
Case "ArcSin"
answer = Math.Asin(Number)
Case "ArcCos"
answer = Math.Acos(Number)
Case "ArcTan"
answer = Math.Atan(Number)
Case "Sqrt"
answer = Math.Sqrt(Number)
End Select
Return answer
End Function
The PerformOperation function is self explanatory. However the changes to TokenizeEquation should be stressed. Basically I added all the numbers and operators
to 1 big arraylist. I then looped through that arraylist and looked for any function (sin cos tan .... etc), and then evaluated (for example) Sin <next number>. The
rest is all the same.
Hey guess what? Run your code... guess how sweet this is:
Sin (Sin (Sin (Sin (Sin (30))))) = about -.6250
Nope. There's still 1 problem. Even though we cna do the above equation we can't do this:
Sin (3) + Cos (5)
It bugs out at this line NumbersAndOperators.RemoveAt(x) saying Index out of bounds.
Note: This bug took me about an hour to fix.... ugh! And the fix was so simple too!
Let's break down the reason for this:
Cos (3) + Sin (5) ---------> Cos 3 + Cos 5
This is what it does initially (after evaluating the parentheses)
We then loop through the equation. Normally it would be
Cos = NumbersAndOperators(0)
3 = NumbersAndOperators(1)
+ = NumbersAndOperators(2)
Sin = NumbersAndOperators(3)
5 = NumbersAndOperators(4)
But we're dealing with 'X' here. In either case, X goes from 0 to 4. In our case, though, Sin 3 gets evaluated, and X gets incremented.
Alright, if our program worked it would do this:
<X = 0>
Add "Cos" to NumbersAndOperators <-- NumbersAndOperators(0)
CurrentItem = PerformOperation (NumbersAndOperators(X), and NumberOfOperators(X + 1)) <-- this is the same as PerformOpreation("Cos", 3)
Remove NumbersAndOperators(X) <-- NumbersAndOperators(0)
Add CurrentItem to position X (Position 0)
Increment X by 1 because we want to skip the "3"
Because it's a loop, X gets incremented by 1 again
<X = 2>
Add "+" to NumbersAndOperators <-- NumbersAndOperators(1)
Do nothing else
<X = 3>
Add "Sin" to NumbersAndOperators <-- NumbersAndOperators(2)
CurrentItem = PerformOperation (NumbersAndOperators(X), and NumberOfOperators(X + 1)) <-- this is the same as PerformOpreation("Sin", 5)
Remove NumbersAndOperators(X) <-- Remove NumbersAndOperators 3 *ERROR: There IS no NumbersAndOperators 3
Add CurrentItem to position X (Position 3)
Increment X by 1
See the error? This can be fixed:
In the TokenizeEquation sub, Dim Offset as Integer. Here's your new code. Blue represents the changes I made
'This will store all the numbers and operators in an arraylist
For x = 0 To SplitEquation.Length - 1
Dim CurrentItem As String = SplitEquation(x)
NumbersAndOperators.Add(CurrentItem)
If CurrentItem = "Sin" Or CurrentItem = "Cos" Or CurrentItem = "Tan" _
Or CurrentItem = "ArcSin" Or CurrentItem = "ArcCos" Or CurrentItem = "ArcTan" Or _
CurrentItem = "Sqrt" Then
'This will calculate the Sin of whatever number....etc
'The reason for SplitEquation(x + 1) is becuase the
'number is always follwed AFTER the function
'Ex: Sin 30
CurrentItem = PerformOperation(CurrentItem, SplitEquation(x + 1))
'Remove the original thing ("Sin" or "Cos"...etc)
NumbersAndOperators.RemoveAt(x - offset)
'And add the actual number
NumbersAndOperators.Add(CurrentItem)
'We're goin ahead, because we already evaluated
'splitEquation(x) and splitquation(x + 1)
x += 1
offset += 1
End If
Next
There's the new code. This fixes the error with the removal thing. If you go through the third iteration again:
<X = 3>
Add "Sin" to NumbersAndOperators <-- NumbersAndOperators(2)
CurrentItem = PerformOperation (NumbersAndOperators(X), and NumberOfOperators(X + 1)) <-- this is the same as PerformOpreation("Sin", 5)
Remove NumbersAndOperators(X - offset) <-- Remove NumbersAndOperators 2
Add CurrentItem to position X (Position 3)
Increment X by 1
Increment offset by 1
These small error are really annoying, but there you go, all patched up. Nice and straightforward.
That's it! We're DONE with the Equation Parser class (except for a few tweaks that we'll add)
More Features to your Program
The only 3 buttons in our program that don't work:
-Store
-Recall
-ToggleMode
Store and recall are fairly straightforward.
Here's the thing. We don't want the user storing something like 1 + 2. We want him storing something such as ... 1 or.... 10.5 or something. So, what I think the best
plan is to do is to allow the user to store a number only AFTER they press the equal sign. Upon pushing any other button, the store button should be disabled.
This will require about 35 more lines of code.
At the beginning of EVERY button click event, EXCEPT for btnEquals, type in the following line:
btnStore.Enabled = False
Do this also in form1_Load.
At the END of the btnEquals click event, type in the following line
btnStore.Enabled = True
Now to actually make the thing store a number:
-Declare a Global variable called StoredNumber
-In the btnStore click event:
StoredNumber = lblDisplay.Text
lblStore.Text = StoredNumber
To recall a number, in btnRecall:
lblDisplay.Text &= Convert.ToDouble(lblDisplay.Text)
Not too shabby.
ToggleMode is pretty simple as well. In btnToggleMode's click event:
If Parser.Mode = "Radians" Then
btnToggleMode.Text = "Rad."
Parser.Mode = "Degrees"
ElseIf Parser.Mode = "Degrees" Then
btnToggleMode.Text = "Deg."
Parser.Mode = "Radians"
End If
In ExpressionParser's global variables:
Public Mode As String = "Radians"
You should change the PerformOperation function like so:
Private Function PerformOperation(ByVal theFunction As String, ByVal Number As Double) As Double
Dim answer As Double
'No such thing as Degrees/Radians in Sqrt... so get that out of the way first
If theFunction = "Sqrt" Then answer = Math.Sqrt(Number)
'If it's degrees, then convert to radians
If Mode = "Degrees" Then Number *= Math.PI / 180
Select Case theFunction
Case "Sin"
answer = Math.Sin(Number)
Case "Cos"
answer = Math.Cos(Number)
Case "Tan"
answer = Math.Tan(Number)
Case "ArcSin"
answer = Math.Asin(Number)
Case "ArcCos"
answer = Math.Acos(Number)
Case "ArcTan"
answer = Math.Atan(Number)
End Select
Return answer
End Function
Basic.
Now run your program, change to Degrees mode and do Sin (90) and you'll get 1 as expected.
Fine tuning and bug fixes
Type in the number 12. Now hit delete twice. Error!
Start over. This time make sure your lblDisplay shows nothing. Now hit delete. Error!
Let's fix that. Change btnDelete's click event like so:
btnStore.Enabled = False
If lblDisplay.Text.Length = 0 Then Exit Sub
'while there are still spaces left...
While lblDisplay.Text.Chars(lblDisplay.Text.Length - 1) = " "
'... keep deleting them
lblDisplay.Text = lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1)
End While
lblDisplay.Text = lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1) 'Remove 1 character
If lblDisplay.Text.Length = 0 Then Exit Sub
'while there are still spaces left...
While lblDisplay.Text.Chars(lblDisplay.Text.Length - 1) = " "
'... keep deleting them
lblDisplay.Text = lblDisplay.Text.Remove(lblDisplay.Text.Length - 1, 1)
End While
Blue represents the changes I made.
Now. There's one last non-functional feature in our program! The lblHelp. It's just sitting there uselessly. Let's make it do something!
Add the following code at the end of the respective button's Click event
btnOpenParentheses:
lblHelp.Text = "Be sure you close the parentheses! Implied multiplication is not valid: Use 3 * Cos (30) rather than 3 Cos (30)"
btnCloseParentheses
lblHelp.Text = "lblHelp.Text = "Be sure you've closed all of them!"
btnSin, btnCos, btnTan, btnArcSin, btnArcCos, btnArcTan, btnSqrt:
lblHelp.Text = "Be sure you close the parentheses! Implied multiplication is not valid: 3 * Sin (30) rather than 3 Sin (30)
btnXRt:
lblHelp.Text = "If you wanted to do the Cube root of 4, it would be: 4 ^ (1 / 3)."
btnClear:
lblHelp.Text = "The display is now clear!"
btnDelete:
lblHelp.Text = "You just deleted 1 character"
btnEquals:
lblHelp.Text = "Voila! The answer!"
btnStore:
lblHelp.Text = "You just stored a number"
btnRecall:
lblHelp.Text = "You just recalled a number"
btnToggleMode should look like this:
If Parser.Mode = "Radians" Then
btnToggleMode.Text = "Rad."
Parser.Mode = "Degrees"
lblHelp.Text = "The mode is now set to Degrees"
ElseIf Parser.Mode = "Degrees" Then
btnToggleMode.Text = "Deg."
Parser.Mode = "Radians"
lblHelp.Text = "The mode is now set to Radians"
End If
Now go back and delete any MessageBoxes lines you have (the ones that say like "Peforming Operation: "....etc) and replace them with Console.Writelines if
you wish.
That's it for this tutorial. Enjoy your calculator that can handle insanely complex equations such as:
1 / Sin (90 * Cos (30 + Tan (3) / 2 ^ 2)) = 1.022 (In Degrees mode)
I hope you enjoyed this long tutorial. Tune in next time for more tutorials!
The Source
Code for this tutorial is located here:
You can also
locate this by logging in to vbProgramming Forums and going
to: Tutorials > Tutorial Source Code >
Source Code
|