User interfaces for object-oriented systems, Part 6: The RPN calculator

The RPN-calculator application demonstrates object-oriented UI principles

1 2 3 4 5 6 Page 3
Page 3 of 6
Listing 2: /src/rpn/Math_stack.java

1: package rpn; 2: 3: import java.lang.*; 4: import java.util.*; 5: import java.text.*; 6: import java.awt.*; 7: import java.awt.event.*; 8: import javax.swing.*; 9: 10: import com.holub.ui.User_interface; 11: import com.holub.ui.Menu_site; 12: import com.holub.ui.Scrollable_JTextArea; 13: import com.holub.string.Align; 14: 15: import com.holub.tools.debug.Assert; 16: 17: /** © 1998, Allen I. Holub. All rights reserved. 18: *

19: * The Math_stack implements a fixed-size stack of doubles. It supports 20: * all operations supported by java.util.Stack() (empty, peek, pop, 21: * push, and search), though the arguments and return values are all 22: * double, and DOUBLE.NaN is returned in places where the standard 23: * stack would return null. As with the standard class, 24: * EmptyStackException is thrown if you try to pop from an empty stack. 25: * Math_stack.FullStackException is thrown if you try to push on a full 26: * stack (not needed in the standard implementation, which is based on 27: * a vector). Like the standard EmptyStackException, you should not 28: * usually catch the exception or mention it in a throws statement. 29: * (It's a RuntimeException.) Various mathematics functions are 30: * supported as well (documented below). 31: * 32: *

Revisions: 33: * 1/7/99 34: * Added the notions of registers, each of which 35: * can provide a visual proxy. There are ten 36: * registers named 0, 1, 2, ... 9. Store the TOS 37: * item in a register by pushing the register 38: * number and issuing a store() request. Recall 39: * the previous value by pushing the register number 40: * and issuing a recall() request. 41: * 42: * 43: * @author Allen Holub 44: * @version 1.1 45: **/ 46: 47: 48: public final class Math_stack implements User_interface 49: { 50: private int max_size; 51: private double[] stack; 52: private double[] register = new double[10]; // 10 registers 53: private int sp; 54: 55: // These will be AWTEvent Multicasters that keep track of the 56: // various proxies. 57: 58: private ActionListener stack_proxies = null; 59: 60: //--------------------------------------------------------------- 61: public static class FullStackException extends RuntimeException 62: { 63: public 64: String toString(){return "Push onto full Math_stack";} 65: } 66: 67: /******************************************************************* 68: * The Stack_viewer class is a small frame that shows the top few stack 69: * items. (You can resize it or scroll it to see the entire stack 70: * if you wish.) Modifications to the associated Math_stack are 71: * automatically displayed in the viewer with no action required 72: * on your part. You cannot declare a viewer. (Its constructor is 73: * private.) Get one from Math_stack.visual_proxy(). Unfortunately, 74: * even though the JTextArea is not editable, it will still 75: * capture the cursor if a user clicks in it. This problem can 76: * be (and is) addressed in the object that gets the viewer. 77: * 78: * A Stack_proxy is always treated as simple Components (i.e., they 79: * are accessed through a public interface), so the 80: * class itself can be made private. 81: * 82: * @see User_interface 83: * @see Math_stack#user_interface 84: **/ 85: 86: private class Stack_viewer extends Scrollable_JTextArea implements ActionListener 87: { 88: public Stack_viewer() 89: { 90: super(true); 91: 92: setVerticalScrollBarPolicy ( JScrollPane.VERTICAL_SCROLLBAR_ALWAYS ); 93: setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); 94: 95: setBackground( Color.lightGray ); 96: setForeground( Color.black ); 97: setFont ( new Font("Monospaced",Font.BOLD,11) ); 98: setLineWrap ( false ); 99: 100: setMinimumSize( new Dimension(200,100) ); 101: } 102: 103: /*************************************************************** 104: * Called when the stack being viewed has been modified, clears 105: * the entire text box, then draws the stack (with the top of 106: * stack item at the top of the window. Finally, it 107: * set the cursor ("Caret Position") to the top line, 108: * effectively scrolling the top-of-stack item back into 109: * view if the stack turns out to be larger than the window. 110: **/ 111: 112: public void actionPerformed( ActionEvent ignored ) 113: { 114: DecimalFormat compositor = new DecimalFormat("#,##0.00##"); 115: 116: JTextArea text_area = getTextArea(); 117: text_area.setText(""); 118: 119: for( int i = sp ; i < max_size; ++i ) 120: { 121: append( Align.align( compositor.format(stack[i]), 122: 20, // column width 123: 15, // alignment column 124: '.', // align on this character 125: ' ' // pad with spaces 126: )); 127: append( "\n" ); 128: } 129: text_area.setCaretPosition(0); 130: } 131: } 132: 133: /******************************************************************* 134: * Get a viewer proxy for the current stack. Delete it when you're 135: * done with it (or let the user get rid of it by clicking on the 136: * "close" box). 137: * 138: *

BUG

: 139: * The returned proxy will not be garbage collected until the 140: * associated Math_stack is destroyed unless you call {@link release_proxies}. 141: * 142: * @param type ignored 143: * @param parent the containing window 144: * @returns The user interface. 145: * @see Math_stack.Stack_viewer 146: **/ 147: 148:

public JComponent visual_proxy(String type, boolean is_read_only )

149: { 150: Stack_viewer viewer = new Stack_viewer(); 151: stack_proxies = AWTEventMulticaster.add(stack_proxies, viewer); 152: return viewer; 153: } 154: /******************************************************************* 155: * Release all proxies back to the Math_stack. The proxy becomes 156: * unusable after being released, so it's reasonable to set 157: * all references to it to null. 158: * 159: * @param proxy The proxy to release. The referenced proxy must 160: * have been returned from a previous call to 161: *

proxy()

. 162: */ 163: 164:

public final void release_proxies( Component proxy )

165: { stack_proxies = null; 166: } 167: 168: //--------------------------------------------------------------- 169:

private void needs( int elements_needed )

170: { 171: // Throws an EmptyStackException of there aren't 172: // elements_needed slots available on the stack. 173: 174: if( (sp + elements_needed) > max_size ) 175: throw new EmptyStackException(); 176: } 177: 178:

private void update( )

179: { 180: // Called by all methods that modify the stack just before 181: // they return. Notifies all view proxies (the observers) 182: // that the stack has changed so that they can redraw 183: // themselves. 184: 185: stack_proxies.actionPerformed( null ); 186: } 187: /******************************************************************* 188: * Creates a math stack (of doubles) of the given size. 189: */ 190:

public Math_stack( int max_size )

191: { this.max_size = max_size; 192: stack = new double[max_size]; 193: sp = max_size; 194: } 195: 196: /******************************************************************* 197: * Push an item on the stack 198: * @throws FullStackException 199: **/ 200:

public void push( double d )

201: { if( sp <= 0 ) 202: throw new FullStackException(); 203: 204: stack[ --sp ] = d; 205: update(); 206: } 207: 208: /******************************************************************* 209: * Pop an item from the stack and return it. 210: * @throws Math_stack.EmptyStackException 211: * @returns the popped item. 212: **/ 213:

public double pop()

214: { needs( 1 ); 215: double return_value = stack[ sp++ ]; 216: update(); 217: return return_value; 218: } 219: 220: /******************************************************************* 221: * @returns true if the stack is empty 222: */ 223:

public boolean empty() { return sp >= max_size; }

224: 225: /******************************************************************* 226: * @returns true if the stack is full 227: */ 228:

public boolean full() { return sp <= 0; }

229: 230: 231: /******************************************************************* 232: * @returns number of elements on the stack 233: */ 234:

public int has(){ return max_size - sp; }

235: 236: /******************************************************************* 237: * @returns the item at top of stack 238: */ 239:

public double peek() // throws EmptyStackException

240: { needs(1); 241: return stack[sp]; 242: } 243: 244: /******************************************************************* 245: * Search the stack from the top down looking for a match of 246: * look_for. 247: * 248: * @returns the distance from top of stack to the item or 249: * -1 if the item is not found. 250: **/ 251:

public int search( double look_for )

252: { for( int i = max_size; --i >= sp ; ) 253: if( stack[i] == look_for ) 254: return sp - i; 255: return -1; 256: } 257: 258: /******************************************************************* 259: * Replace the two items at top of stack with their sum. 260: * 261: * @throws EmptyStackException if there is one or fewer items on 262: * the stack. 263: **/ 264: 265:

public void add() // throws EmptyStackException

266: { needs(2); 267: stack[sp+1] += stack[sp]; 268: ++sp; 269: update(); 270: } 271: 272: /******************************************************************* 273: * Replace the two items at top of stack with their difference. 274: * (The item at top of stack is subtracted from the item just 275: * under it). 276: * 277: * @throws EmptyStackException if there is one or fewer items on 278: * the stack. 279: **/ 280: 281:

public void subtract() // throws EmptyStackException

282: { needs(2); 283: stack[sp+1] -= stack[sp]; 284: ++sp; 285: update(); 286: } 287: 288: /******************************************************************* 289: * Replace the two items at top of stack with their product. 290: * 291: * @throws EmptyStackException if there is one or fewer items on 292: * the stack. 293: **/ 294: 295:

public void multiply() // throws EmptyStackException

296: { needs(2); 297: stack[sp+1] *= stack[sp]; 298: ++sp; 299: update(); 300: } 301: 302: /******************************************************************* 303: * Replace the two items at top of stack with their quotient. 304: * (The item one cell down from top of stack is divided by the 305: * the item at top of stack). 306: * 307: * @throws EmptyStackException if there is one or fewer items on 308: * the stack. 309: **/ 310: 311:

public void divide() // throws EmptyStackException

312: { needs(2); 313: stack[sp+1] /= stack[sp]; 314: ++sp; 315: update(); 316: } 317: 318: /******************************************************************* 319: * TOS = -TOS 320: * @throws EmptyStackException 321: **/ 322:

public void invert()

323: { needs(1); 324: stack[sp] = -stack[sp]; 325: update(); 326: } 327: 328: /******************************************************************* 329: * TOS starts out with a time represented as hh.mmss. The TOS 330: * item is replaced with the same time represented in decimal. 331: * For example, the time of 6:30 is input as 6.30, executing 332: * convert_hms_to_decimal results in a the 6.30 being replaced 333: * by 6.50 (6 and 1/2 hours). 334: */ 335:

public void covert_hms_to_decimal()

336: { needs( 1 ); 337: 338: long time = (long)( Math.floor(stack[sp] * 10000.0) ); 339: 340: double seconds = time % 100; time /= 100; 341: double minutes = time % 100; time /= 100; 342: double hours = time; 343: 344: stack[sp] = hours + (minutes/60.0) + (seconds/(60.0 * 60.0)); 345: update(); 346: } 347: 348: /******************************************************************* 349: * TOS = square root of item at top of stack 350: * @throws EmptyStackException 351: **/ 352:

public void sqrt()

353: { needs(1); 354: stack[sp] = Math.sqrt(stack[sp]); 355: update(); 356: } 357: 358: /******************************************************************* 359: * Replaces top two stack items with TOS-1, raised to the power 360: * of TOS. 361: * @throws EmptyStackException 362: **/ 363:

public void pow()

364: { needs(2); 365: stack[sp+1] = Math.pow( stack[sp+1], stack[sp] ); 366: ++sp; 367: update(); 368: } 369: 370: /******************************************************************* 371: * Swap the two items at top of stack. 372: * 373: * @throws EmptyStackException if there is one or fewer items on 374: * the stack. 375: **/ 376: 377:

public void swap() // throws EmptyStackException

378: { needs(2); 379: 380: double tmp = stack[ sp ]; 381: stack[sp ] = stack[ sp+1 ]; 382: stack[sp+1] = tmp; 383: update(); 384: } 385: 386: /******************************************************************* 387: * Push a duplicate of the current top-of-stack item. 388: * 389: * @throws EmptyStackException if there is one or fewer items on 390: * the stack. 391: */ 392: 393:

public void duplicate() //throws EmptyStackException, FullStackException

394: { 395: push( peek() ); // Observers are notified by push 396: } 397: 398: /******************************************************************* 399: * Deletes all stack items. 400: */ 401: 402:

public void clear()

403: { 404: sp = max_size; 405: update(); 406: } 407: 408: /******************************************************************* 409: * Set the named register to a given value 410: */ 411:

public void register_set( int name, double value )

412: { register[name]=value; 413: } 414: 415: /******************************************************************* 416: * Return the value of the named register. 417: */ 418:

public double register_get( int name )

419: { return register[name]; 420: } 421: 422: /******************************************************************* 423: * Push the value of the named register onto the operand stack. 424: */ 425:

public void register_push( int name )

426: { push( register[name] ); 427: } 428: 429: /******************************************************************* 430: * Pop the top of stack item into the named register 431: */ 432:

public void register_pop( int name )

433: { register[name] = pop(); 434: } 435: 436: /******************************************************************* 437: * Save the top of stack item into the named register without popping. 438: */ 439:

public void register_save( int name )

440: { register[name] = peek(); 441: } 442: 443: /******************************************************************* 444: * A unit-test class. Main prints nothing if the stack is okay. 445: */ 446:

public static class Test

447: { 448:

public static void main( String[] args )

449: { 450: final Math_stack s = new Math_stack( 4 ); 451: 452: s.push( 1.0 ); 453: s.push( 2.0 ); 454: s.add(); 455: if( s.peek() != 3.0 ) 456: System.out.println("Math_stack: add" ); 457: 458: s.push( 2.0 ); 459: s.subtract(); 460: if( s.peek() != 1.0 ) 461: System.out.println("Math_stack: subtract" ); 462: 463: s.pop(); 464: if( !s.empty() ) 465: System.out.println("Math_stack: empty" ); 466: 467: s.push( 5 ); 468: s.duplicate (); 469: s.multiply(); 470: if( s.peek() != 25.0 ) 471: System.out.println("Math_stack: dup or multiply" ); 472: 473: s.push( 100.0 ); 474: s.swap(); 475: s.divide(); 476: 477: if( s.peek() != 4.0 ) 478: System.out.println("Math_stack: swap or divide" ); 479: 480: if( s.search(7) != -1 ) 481: System.out.println("Math_stack: search found nonexistent"); 482: 483: if( s.search(4) != 0 ) 484: System.out.println("Math_stack: search missed existent"); 485: 486: try 487: { 488: s.push(3.0); // two things 489: s.push(2.0); // two things 490: s.push(1.0); // two things 491: } 492: catch ( Exception e ) 493: { 494: System.out.println("Math_stack: Unexpected exception 1:"+e); 495: } 496: 497: try 498: { 499: s.push(0.0); // should cause overflow 500: System.out.println( "Math_stack: Overflow not caught\n"); 501: } 502: catch ( Math_stack.FullStackException e ){} 503: catch ( Exception e ) 504: { 505: System.out.println("Math_stack: Unexpected exception 2:"+e); 506: } 507: 508: try 509: { 510: if(s.pop() != 1.0)System.out.println("Math_stack: pop 1"); 511: if(s.pop() != 2.0)System.out.println("Math_stack: pop 2"); 512: if(s.pop() != 3.0)System.out.println("Math_stack: pop 3"); 513: if(s.pop() != 4.0)System.out.println("Math_stack: pop 4"); 514: if(!s.empty() )System.out.println("Math_stack: pop 5"); 515: } 516: catch ( Exception e ) 517: { 518: System.out.println("Math_stack: Unexpected exception 3:"+e); 519: } 520: 521: try 522: { 523: s.pop(); // should cause underflow 524: System.out.println( "Math_stack: Underflow not caught\n" ); 525: } 526: catch ( EmptyStackException e ){} 527: catch ( Exception e ) 528: { 529: System.out.println("Math_stack: Unexpected exception 4:"+e); 530: } 531: 532: s.push(9.0); 533: s.push(2.0); 534: s.pow(); 535: if( s.peek() != 81.0 ) 536: System.out.println("Math_stack: pow()"); 537: s.sqrt(); 538: if( s.peek() != 9.0 ) 539: System.out.println("Math_stack: sqrt()"); 540: 541: 542: System.out.println( "Testing visual components. Type x<enter>" 543: +" eight times.\n"); 544: 545: Frame f = new Frame(); 546: 547: f.setBounds ( 0, 0, 300, 300 ); 548: f.add ( "Center", s.visual_proxy(null,false) ); 549: f.show ( ); 550: 551: s.push( 1.0 ); 552: try{while(System.in.read() != 'x');}catch(Exception e){} 553: s.push( 2.0 ); 554: try{while(System.in.read() != 'x');}catch(Exception e){} 555: s.push( 3.0 ); 556: try{while(System.in.read() != 'x');}catch(Exception e){} 557: s.push( 4.0 ); 558: try{while(System.in.read() != 'x');}catch(Exception e){} 559: s.add (); 560: try{while(System.in.read() != 'x');}catch(Exception e){} 561: s.swap (); 562: try{while(System.in.read() != 'x');}catch(Exception e){} 563: s.subtract (); 564: try{while(System.in.read() != 'x');}catch(Exception e){} 565: s.multiply (); 566: try{while(System.in.read() != 'x');}catch(Exception e){} 567: 568: f.dispose(); 569: System.exit(0); 570: } 571: } 572: }

The Parser

Unlike the Math_stack, the Parser is fairly complicated. Let's start by looking at the pieces; then we'll put the pieces together.

The Keypad view

The first piece is the Calculator_keypad class (the UML is in Figure 8).

Figure 8. The keypad-style calculator UI

To refresh your memory, the keypad is shown in Figure 9.

Figure 9. Keypad screenshot

The keypad -- a JPanel -- uses a GridBag to lay itself out into two columns and five rows as seen in Figure 10 below.

Figure 10. The outermost panel

In Figure 10, the top row holds a JLabel accumulator, and the right columns of the next three rows hold the add, subtract, and multiply keys. The bottom row holds the "Enter" and "divide" keys.

The left column of the second, third, and fourth rows hold a sub-JPanel, which uses a second GridBag to lay itself out as a numeric keypad, as seen in Figure 11 below.

Figure 11. The calculator-keypad view's nested subpanel (numeric keys)

This organization -- a class both derives from and contains instances of another class, and objects of that class are organized in a containment relationship -- is an example of the Gang of Four Composite design pattern (for more on the Gang of Four, see Resources).

Other design patterns are represented here as well. For example, the Calculator_keypad and Tape widgets use the Gang of Four Observer pattern to communicate with the outside world. When an object wants to find out when a user enters text into either widget, the object expresses its interest by telling the widget to send a notification to the object. (It does so by calling addActionListener(...). See (Listing 3, line 142.) The widget notifies the listeners when text is available by sending them actionPerformed(...) messages.

The JButton objects, when pressed, also notify their listeners (the Observer pattern again), but the current implementation uses a single Listener object to mediate between the whole set of buttons and the Calculator_keypad object itself (Mediator). That is, all the buttons talk to a single Mediator object, whose job is to update the keypad as necessary. The Controller object's actionPerformed(...) method (Listing 3, line 171) does the work, intercepting button-press notifications and figuring out what to do with them. Numbers accumulate (and are echoed to the accumulator label) until a non-number is encountered, in which case an actionPerformed() message is sent to the keypad's observers. Non-numeric key presses are dispatched immediately (and the accumulator window is cleared).

1 2 3 4 5 6 Page 3
Page 3 of 6