![]() | Monkeys at Keyboards: The Javanomicon © Michael James Heron | ||||
| Topic: Java Programming Level: 2 Version: delta | |||||
30 - Case Study 9 - Maths Tester | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to:
|
In this chapter we're going to make use of our Mad Recursion Skillz to write an application suitable for testing basic mathematics. The application will generate a series of random sums of the form:
And then prompt the user for an answer. The application will then compare the user's answer against the correct answer and tally up the user's score. Although this seems like a fairly simple application, internally it is somewhat more complex than it first appears since we need to implement a recursive algorithm for computing the answers to the randomly generated questions - after all, we can hardly mark someone as correct if we don't know what the correct answer is ourselves! We also need to ensure that questions are properly formed according to the rules of mathematics and that we don't end up with questions that contain unmatched brackets. So, on with the show!
We're going to make use of a simple interface that consists of a display for the user's score, a label that displays the current question, and a JTextField where they can enter the answer. We also need a button for the user to submit their answer to the application. As usual, our first step is to develop the interface. This gives us a clear idea of the inputs and outputs of the application, which can help develop the underlying logic:
As with most of the applications we are developing, the interface is fairly simple. We're concerned mainly with what is happening underneath - the actual code that underpins what is going on. So the code for our interface is as follows:
As usual, the actionPerformed method is going to drive most of our application's functionality, but first we need a couple of other classes. One will generate questions, the other will parse questions to return the answer and compare that value against what the user entered. Before we can implement the answer part, we need the actual questions, so we'll start with that.
Before we begin generating questions, we need to put some limits on what we're going to generate, and what symbols we are going to use to indicate operator is to be applied. We'll use the standard set of programming operators, plus the symbol ^ to represent a power. We don't want to generate questions such as 16543 ^ 12211 - that involves working with ludicrously large numbers that most calculators won't even be able to deal with. Instead, we're going to set up some variables that indicate the range of the numbers we generate.
We're going to return the question as a String, so we need to create an empty string as our first task. We'll call it question. We will worry about implementing brackets when we've got the bulk of the method fleshed out. We start off by creating a random number within the specified constraints, and then add that to our question string:
Then we need to pick a random operator to apply:
And the add that to our question:
This gives us our starting base in the form of a question that looks like this:
Now, we need to keep adding numbers and operators onto the end with a for loop. We will need to add numbers and operators alternately, so will use the % operator on the counter to work out which we should be adding. In fact, since we'll be generating numbers and operators quite a lot, let's move the code into two separate methods rather than duplicate the functionality:
Taking into account these changes, our generateQuestion method now looks like this:
This serves as the basic framework for what we're trying to accomplish with this class. We also need to add another method that gives us a random power:
And then we need to keep track of what the last operator we generated was so that we can ensure we don't force people to compute ridiculously large numbers:
Now our questions will be generated at the call of a method. However, we've missed out a fairly large chunk of functionality - generating brackets. This is somewhat more complicated. The first thing we need is a way of determining when to create an open bracket - we'll base this on a simple percentage, creating them 25% of the time. We'll write a method that returns a boolean value indicating when we should create a new bracket:
We also need to keep count of how many open brackets we have - obviously we'll never create a close bracket if there are no open brackets - that would just be silly. We're only ever going to put open brackets around numbers, never around operators... effectively a formula surrounded by brackets is standing in place of any given discreet number. We put the code for generating an open bracket into the part of the for loop responsible for generating numbers:
Then we need to create a corresponding closing bracket - we'll do this only if there is an open bracket and if the addedBracket variable is set to false - in this way, we ensure that we won't put brackets around single terms only:
We also need to put a check at the end of the loop, making sure that any remaining open brackets are closed off (this can occur with terms that occur at the end of a question):
Putting this all together, we get the following method:
And there we have it, a powerhouse of a method that generates random questions for our math tester. Awesome.
Now comes the difficult bit - writing some code that can be used to actually compute the answers to our questions. This involves some recursion because of the way our questions involve brackets. In this example we are only looking at pairs of brackets rather than brackets within brackets - the framework we set up for this will be good for either, but actually generating questions of that form is left as an exercise for the reader. We're going to use another class to perform this task - we'll call it RecursiveParser since that sounds nice and impressive. We should think a bit about how we're going to calculate these answers - we'll be getting a question as a string and we need to ensure operator precedence is enforced, including the parsing of brackets. The first question we must address when developing any program is how we are going to represent the data - we'll use an ArrayList of terms, so that the formula "2 * 3 * 4" becomes represented by an ArrayList of the form:
Or the formula 2 + (3 - 5 ) * 2:
We're also going to hold an array of operators, stored in the order of their precedence:
Then we step over each element of our operators array within a for loop. Within that loop, we will be stepping over every element of our ArrayList. We check each element in turn, looking for one that matches the current operator we're looking for - when we find a match, we call it a pivot point since it will be surrounded by two terms. So, for the multiply operator on the following ArrayList:
The pivot point is element 1:
We then perform the appropriate calculation and collapse these three terms down into a single value which occupies the place the calculation previously did in the ArrayList:
We continue doing this until we end up collapsing everything into a single value in the ArrayList:
Becomes:
So when we are left with a single term, we have the answer to the whole question. If we encounter an open bracket, we'll pass everything between the brackets to our calculation function (recursively) so that it collapses it all down into a single value and returns that as an answer. We do this before we ever start iterating over our operators:
We look through this ArrayList for an opening bracket, which we find at position 2. We then find the bracket that closes that opening bracket (which is found at position 6), and then pass everything between these indexes as a new ArrayList for calculation:
And then we replace that whole chunk of the ArrayList, brackets and all, with the answer to the bracketed question:
This would work equally for brackets which contained brackets, since the first thing the calculate method does is look for an open bracket and pass everything between the open bracket and its closing bracket as a parameter to a recursive call to itself:
The idea is very simple in theory, and recursion allows us to implement it equally simply in code. However, as with the example of permutations of words, it is a piece of code that benefits from multiple readings before it makes complete sense. First, we need a few utility methods. We need one that will take our String and turn it into an ArrayList suitable for parsing. We'll call this method makeList:
We need a method to get the elements of an ArrayList that lie between two brackets. We'll call this method split:
We need a method that finds the closing bracket for a corresponding open brace:
And finally, a method that takes two terms and performs the appropriate calculation on them:
The code for each of these methods is individually quite simple. That's because we have yet to look at the tricky bit that does the actual recursion. We'll call this method calculateAnswer. First of all, the bit that deals with the brackets:
And then the bit that deals with the operator precedence:
There's a lot of code there, so we'll go through it bit by bit:
This piece of code is responsible for dealing with brackets. It steps over every element of the ArrayList passed into the method, looking for open brackets:
It goes every element, setting the variable pivot to the current element. If the pivot character is an open bracket:
It then looks for the closing bracket for that open bracket, and then sets the variable end to the location of that bracket:
These two values are then used as the parameters for the split method, which takes the whole formula and returns only the bits between the start and end parameters:
This is then used as the basis of a new call to calculateAnswer. Whatever the method returns is then added to an ArrayList, and the for loop continues from the point at which we found the closing bracket:
If the pivot character isn't an open bracket and isn't a closing bracket, the code adds it to another ArrayList it maintains (this is called newList). So for the first two elements, we have a newList variable that contains their values:
Then, when it hits the open bracket, it passes the contents of the brackets to another invocation of the calculateAnswer method, which returns an answer that gets added to the newList variable:
And then the method continues stepping over our formula variable from the next index after the end bracket, which gives us the following contents for newList at the end of the loop:
The effect is that we end up computing out all the brackets, leaving us with a single level question to deal with. The next piece of code calculates the answer to this:
We step over every element of our operators array within a for loop:
And within this loop, we step over every element in our formula ArrayList looking for elements that match the current operator and are only one character long (so we don't match negative numbers). So for the first loop, we go over every element looking for one that matches ^:
If the element doesn't match, it is added to a newly cleared newList variable... so if nothing matches for a particular operator, newList contains the current state of the formula ArrayList. At the end of the loop, the formula ArrayList is set to whatever we ended up within newList... so for this particular operator in this particular question, it gets assigned to the contents of newList, which are exactly the same as the original formula. It is only when we get to iteration 3 of the outer loop (the - operator) that we start doing anything interesting. When an element matches the current operator, we set the tmp1 variable to the last element in newList, and tmp2 to the element that lies directly after the operator.
The operator itself is the pivot point. We pass both the terms and the operator into our calculate method, which returns the answer. We then remove the last element we added to newList and replace it with our answer. So for step one over the formula, newList contains one element:
Then at step two, it matches an operator. It removes the last element that was added, and replaces it with the answer to the calculation:
And we then continues the loop over formula at the point after the second term:
The point after the second term is also a matched operator, so it sets tmp1 to be the last element in newList (which is 5), and tmp2 becomes the element after the operator (which is also 5). We then perform the calculation and replace the contents of our newList element with the answer:
It continues this in a while loop that is based on the number of elements in the formula being greater than 1. As soon as there is only element left, the loop terminates and the method returns a double representing what the value of the calculation was. Tricky? Just a bit!
Now we need to put the code into our application. When we setup the interface, we're going to set the contents of our question label to be a question generated from our generator. When the user presses a button, it is going to pass the text from the label to our recursive parser and compare the answer from that to the answer the user typed:
There's a problem with our application in that the numbers it produces are completely random (within limits), and so the answers to the questions are often very precise. It would be much better for us if we could limit the answers to a certain number of significant decimal places. In the java.text package is a class called DecimalFormat that allows us to do just that. We create a DecimalFormat object, passing in a string that represents the format we wish to use:
And then pass in the value we wish to truncate as a parameter to the format method of this object:
And then finally:
Now the answers will only ever be to two significant decimal places. Neato!
Although from the introduction it seemed like a fairly simple application, the complexity of bracket and operator precedence means that it is surprisingly complex to develop this particular program. If we didn't have recursion as a tool available to us, it would be even more difficult - straightforward iteration doesn't lend us the same power when dealing with these kind of problems. At this point, we've mastered all of the programming tools we have available to us as intermediate Java programmers. There are obviously more, and of greater power and complexity than what we've covered - but as far as this book goes, there are no more coding tools to learn - our toolkit is full of all the goodness we can possibly squeeze into it. In the next few chapters, we'll be looking at some of the context of Java - why certain tools are better than others for some kinds of problems, and how we can bring together everything we've learned to create a 'finished' application. But you deserve a great big pat on the back for the hard work you've done up until now. Well done! Further ReadingThe following table details further reading on the topic in this chapter, and also any external resources that you may find useful.
|
| Previous | Table of Contents | Next |
© 2004-2006 Michael James Heron