Solution for
Programming Exercise 12.2


THIS PAGE DISCUSSES ONE POSSIBLE SOLUTION to the following exercise from this on-line Java textbook.

Exercise 12.2: In mathematics, several operations are defined on sets. The union of two sets A and B is a set that contains all the elements that are in A together with all the elements that are in B. The intersection of A and B is the set that contains elements that are in both A and B. The difference of A and B is the set that contains all the elements of A except for those elements that are also in B.

Suppose that A and B are variables of type set in Java. The mathematical operations on A and B can be computed using methods from the Set interface. In particular: The set A.addAll(B) is the union of A and B; A.retainAll(B) is the intersection of A and B; and A.removeAll(B) is the difference of A and B. (These operations change the contents of the set A, while the mathematical operations create a new set without changing A, but that difference is not relevant to this exercise.)

For this exercise, you should write a program that can be used as a "set calculator" for simple operations on sets of non-negative integers. (Negative integers are not allowed.) A set of such integers will be represented as a list of integers, separated by commas and, optionally, spaces and enclosed in square brackets. For example: [1,2,3] or [17, 42, 9, 53,108]. The characters +, *, and - will be used for the union, intersection, and difference operations. The user of the program will type in lines of input containing two sets, separated by an operator. The program should perform the operation and print the resulting set. Here are some examples:

          Input                                 Output
         -------------------------           -------------------
          [1, 2, 3] + [3,  5,  7]             [1, 2, 3, 5, 7]
          [10,9,8,7] * [2,4,6,8]              [8]
          [ 5, 10, 15, 20 ] - [ 0, 10, 20 ]   [5, 15]

To represent sets of non-negative integers, use TreeSets containing objects belonging to the wrapper class Integer. Read the user's input, create two TreeSets, and use the appropriate TreeSet method to perform the requested operation on the two sets. Your program should be able to read and process any number of lines of input. If a line contains a syntax error, your program should not crash. It should report the error and move on to the next line of input. (Note: To print out a Set, A, of Integers, you can just say System.out.println(A). I've chosen the syntax for sets to be the same as that used by the system for outputting a set.)


Discussion

This program would be much easier to write if we could assume that the user's input was in the correct format. Unfortunately, the exercise says that we have to detect errors in the user's input and handle them by printing error messages. To do this we have to constantly look ahead while reading the user's input, to see whether the input agrees with what we are expecting. The techniques for doing this were covered for the example LengthConverter2.java from Section 9.2. We can use the function TextIO.peek() to look ahead at the next character in the user's input. The user can put any number of blanks between two items in the input, so before looking ahead at the next item, we should always skip over any blanks in the input. Since this has to be done many times, it's useful to define a subroutine to do it. The subroutine skips over tab characters, represented by '\t', as well as blanks:

         static void skipBlanks() {
            while (TextIO.peek() == ' ' || TextIO.peek() == '\t')
               TextIO.getAnyChar();
         }

My program uses another technique from Chapter 9 -- exceptions -- to handle the errors. Exceptions make it possible to organize the error-handling code in a straightforward way. When an error is discovered, an exception is thrown. The main program uses a try...catch statement to try to process one line of input. If an error occurs, the program does not crash. The error is caught, an error message is printed, and the program continues. I throw an error of type IllegalArgumentException when an error is found. This exception is a standard part of Java, but it might be better style to define a new error class to represent the error.

My program uses a method named calc() to read and process one line of input. This method in turn uses another method, readSet() to read each of the two sets of integers from the input line. Without error handling, an algorithm for readSet() would be:

         Start with an empty set.
         Read the '[' that begins the set.
         Repeat:
            Read the next number and add it to the set.
            If the next character is ']':
               break.
            Read the comma that separates one number from the next.
         Read the ']'.
         Return the set.

To turn this into a robust routine, we just have to check, before reading anything, that the next character is legal. If not, throw an exception. This adds a lot of code, but the nice thing about throwing exceptions is that it doesn't disturb the logical flow of the routine.

There is one possible bug in the algorithm. It assumes that there is at least one number in the set. But in mathematics, a set can be empty. I decided to allow for the possibility of empty sets in my program. See the solution, below.

An algorithm for the calc() method is even more straightforward. Again, the algorithm ignores the possibility of errors:

         Read the first set, A.
         Read the operator.
         Read the second set, B.
         if the operator is '+'
            A.addAll(B)  // Sets A equal to the union of A and B.
         else if the operator is '*'
            A.retainAll(B)  // Sets A to the intersection.
         else
            A.removeAll(B)  // Sets A to the difference.
         Print A.

An error check has to be added to make sure that there is a legal operator in the correct position. I also add an error check to make sure that there is no extra data on the line.


The Solution

      /*
          This program is a very simple "set calculator" that can compute
          the intersection, union, and set difference of two sets of 
          non-negative integers.  Each line of the user's input contains two 
          such sets, separated by one of the operators +, *, or -, standing
          for union, intersection, and set difference respectively.  A set
          must be given in the form of a list of non-negative integers, separated
          by commas, and enclosed in square brackets.  For example:
          [1, 2, 3] + [4, 3, 10, 0].  Spaces can occur anywhere in the input.
          If an error is found in the input, the program will report it.
          The program ends when the user inputs an empty line.
          
          This program is mainly a demonstration of Sets.
      */


      import java.util.TreeSet;
      
      public class SetCalc {
      
         public static void main(String[] args) {
         
            System.out.println("This program will compute union, intersection,");
            System.out.println("and set difference of sets of integers.");
            System.out.println("");
            System.out.println("");
            System.out.println("Enter set computations (press return to end):");
            
            while (true) {
               char ch;
               System.out.print("\n? ");
               skipBlanks();
               if (TextIO.peek() == '\n') {
                     // The input line is empty.  
                     // Exit the loop and end the program.
                  break;
               }
               try {
                  calc(); // Reads and processes one line of input.
               }
               catch (IllegalArgumentException e) {
                     // An error was found in the input line.
                  System.out.println("Error in input: " + e.getMessage());
                  System.out.print("Discarding input: ");
               }
               do { // Copy extra characters on line to output.  If there 
                    // was no error, then the only character that is copied
                    // is the end-of-line character.
                  ch = TextIO.getAnyChar();
                  System.out.print(ch);
               } while (ch != '\n');
            }
         
         } // end main()
         

         static void calc() {
               // Read a line of input, consisting of two sets separated by
               // an operator.  Perform the operation and output the value.
               // If any syntax error is found in the input, an
               // IllegalArgumentException is thrown
               
            TreeSet A, B;  // The two sets of integers.
            char op;       // The operator, +, *, or -.
            
            A = readSet(); // Read the first set.

            skipBlanks();
            if (TextIO.peek() != '*' && TextIO.peek() != '+' 
                                                  && TextIO.peek() != '-')
               throw new IllegalArgumentException(
                                  "Expected *, +, or  - after first set.");
            op = TextIO.getAnyChar(); // Read the operator.

            B = readSet(); // Read the second set.

            skipBlanks();
            if (TextIO.peek() != '\n')
               throw new IllegalArgumentException("Extra unexpected input.");

            // Perform the operation.  This modifies set A to represent
            // the answer.  Display the answer by printing out A. The
            // output format for a set of integers is the same as the
            // input format used by this program.
            
            if (op == '+')
               A.addAll(B);     // Union.
            else if (op == '*')
               A.retainAll(B);  // Intersection.
            else
               A.removeAll(B);  // Set difference.
            System.out.print("Value:  " + A);

         } // end calc()
         
         
         static TreeSet readSet() {
              // Reads a set of non-negative integers from standard input,
              // and stores them in a TreeSet that contains objects belonging
              // to the wrapper class Integer.  The set must be enclosed
              // between square brackets and must contain a list of zero or
              // more non-negative integers, separated by commas.  Spaces 
              // are allowed anywhere.  If the input is not of the correct
              // form, an IllegalArgumentException is thrown.

            TreeSet set = new TreeSet();  // The set that will be read.

            skipBlanks();
            if (TextIO.peek() != '[')
               throw new IllegalArgumentException("Expected '[' at start of set.");
            TextIO.getAnyChar(); // Read the '['.

            skipBlanks();
            if (TextIO.peek() == ']') {
                  // The set has no integers.  This is the empty set, which
                  // is legal.  Return the value.
               TextIO.getAnyChar(); // Read the ']'.
               return set;
            }

            while (true) {
                  // Read the next integer and add it to the set.
               skipBlanks();
               if (! Character.isDigit(TextIO.peek()))
                  throw new IllegalArgumentException("Expected an integer.");
               int n = TextIO.getInt(); // Read the integer.
               set.add( new Integer(n) );
               skipBlanks();
               if (TextIO.peek() == ']')
                  break;  // ']' marks the end of the set.
               else if (TextIO.peek() == ',')
                  TextIO.getAnyChar(); // Read a comma and continue.
               else
                  throw new IllegalArgumentException("Expected ',' or ']'.");
            }

            TextIO.getAnyChar(); // Read the ']' that ended the set.

            return set;

         } // end readSet()
         
         
         static void skipBlanks() {
            while (TextIO.peek() == ' ' || TextIO.peek() == '\t')
               TextIO.getAnyChar();
         } 
      

      } // end class SetCalc

[ Exercises | Chapter Index | Main Index ]