/*
 * @(#)JPTParser.java    2.2  26 September 2002
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.parser;

import edu.neu.ccs.*;
import java.text.*;
import java.util.*;
import java.math.*;

// debug
// import edu.neu.ccs.console.*;

/**
 * <P>Parses and evaulates data <CODE>String</CODE>s 
 * using simple expression evaluation.<P>
 *
 * <P>Grammar notation used in this specification 
 * is as defined within the Java Language Specification
 * by James Gosling, Bill Joy, and Guy Steele,
 * that is referenced below.
 *
 * This language specification is based largely 
 * on the Java Language Specification.</P>
 * 
 * <P>The characters of the parser input <CODE>String</CODE> are 
 * considered to be a sequence of input elements, 
 * which are white space and tokens. 
 *
 * The tokens are the identifiers, numbers, separators, and 
 * operations of the syntactic grammar specified below.</P>
 *
 * <P>The lexical grammar for the language 
 * defined by this parser is as follows:</P>
 *
 * <CODE>
 * <DT><I>Input</I>:
 *     <DD><I>InputElements</I>
 * <DT><I>InputElements</I>:
 *     <DD><I>InputElement InputElements<SUB>opt</SUB></I>
 * <DT><I>InputElement</I>:
 *     <DD><I>Whitespace</I>
 *     <DD><I>Token</I>
 * </DL></CODE>
 * <P><I>White space</I> is defined as the ASCII
 * space, horizontal tab, form feed, line feed, 
 * and return characters.
 *
 * White space is required to separate tokens
 * except for situations where a separator is not needed
 * to distinguish separate tokens.</P>
 * <CODE>
 * <DT><I>Whitespace</I>:
 *     <DD>The ASCII SP character, aka "space"
 *     <DD>The ASCII HT character, aka "horizontal tab"
 *     <DD>The ASCII FF character, aka "form feed"
 *     <DD>The ASCII LF character, aka "line feed"
 *     <DD>The ASCII CR character, aka "return"
 * <DT><I>Token</I>:
 *     <DD><I>Identifier</I>
 *     <DD><I>Number</I>
 *     <DD><I>Operation</I>
 *     <DD><I>Separator</I>
 * </DL></CODE>
 * <P>An <I>identifier</I> is an unlimited-length sequence
 * of <I>identifier characters</I> that must begin with
 * a letter.
 * 
 * Identifiers denote constants and procedures.</P>
 * <CODE>
 * <DT><I>Identifier</I>:
 *     <DD><I>Letter IdentifierChars<SUB>opt</SUB></I>
 * <DT><I>IdentifierChars</I>:
 *     <DD><I>IdentifierChar IdentifierChars<SUB>opt</SUB></I>
 * <DT><I>IdentifierChar</I>:
 *     <DD><I>Letter</I>
 *     <DD><I>Digit</I>
 *     <DD>The ASCII '_' character, aka "underscore"
 * <DT><I>Letter</I>:
 *     <DD>An ASCII character whose code is in the range 65-90
 *     <DD>An ASCII character whose code is in the range 97-122
 * </DL></CODE>
 * <P>A <I>number</I> is an integer or real number,
 * as defined below.</P>
 *
 * <P>All integers are represented internally as type 
 * <CODE>BigInteger</CODE> encapsulated in <CODE>XBigInteger</CODE>.</P>
 *
 * <P>All real numbers are represented internally as type
 * <CODE>double</CODE> encapsulated in <CODE>XDouble</CODE>.</P>
 *
 * <P>Computations will be maintained in <CODE>BigInteger</CODE>
 * format as long as possible.  If the arguments of an arithmetic
 * operation are both <CODE>BigInteger</CODE> then the result
 * will be computed as <CODE>BigInteger</CODE>.  If at least one
 * of the arguments is <CODE>double</CODE> then both arguments
 * will be forced to <CODE>double</CODE> and the computation will
 * be done in <CODE>double</CODE>.  Functions that work only with
 * <CODE>double</CODE> will force the computation to
 * <CODE>double</CODE>.</P>
 *
 * <P>It is possible in the course of computation that a number
 * classified as a real number below could be an integer value
 * but be represented by a <CODE>double</CODE>.  Note that this
 * conversion can entail a loss of digits.  Indeed, large values
 * of type <CODE>long</CODE> cannot in Java be represented with
 * full accurary in type <CODE>double</CODE>.</P>
 *
 * <P>Note that this parser does not handle <CODE>BigDecimal</CODE>
 * as a type. Any data entered as a <CODE>BigDecimal</CODE> will be
 * interpreted as <CODE>double</CODE>.  The reason for this design
 * is that the mathematical functions supported in the parser are
 * not available for <CODE>BigDecimal</CODE> in the Java libraries.
 * As a consequence, the <CODE>fromStringData</CODE> method of the
 * <CODE>XBigDecimal</CODE> does not use this parser and does not
 * support any form of arithmetic expression evaluation.</P>
 *
 * <CODE>
 * <DT><I>Number</I>:
 *     <DD><I>Digits</I>
 *     <DD><I>RealNumber</I>
 * <DT><I>Digits</I>:
 *     <DD><I>Digit Digits<SUB>opt</SUB></I>
 * <DT><I>Digit</I> one of:
 *     <DD>0 1 2 3 4 5 6 7 8 9
 * <DT><I>RealNumber</I>:
 *     <DD><I>Digits RadixPoint Digits<SUB>opt</SUB> 
 *            Exponent<SUB>opt</SUB></I>
 *     <DD><I>RadixPoint Digits<SUB>opt</SUB> 
 *            Exponent<SUB>opt</SUB></I>
 *     <DD><I>Digits Exponent</I>
 * <DT><I>RadixPoint</I>:
 *     <DD>The stored "radix point" token
 * <DT><I>Exponent</I>:
 *     <DD><I>ExponentIndicator Sign<SUB>opt</SUB> Digits</I>
 * <DT><I>Sign</I> one of:
 *     <DD>+ -
 * </DL></CODE>
 *
 * <P>The following tokens are operations
 * in this language.</P>
 * <CODE>
 * <DT><I>Operation</I> one of:
 *     <DD>+ - * / %
 *     <DD>== != < > <= >= && || !
 * </DL></CODE>
 *
 * <P>The following tokens are separators
 * in this language.</P>
 * <CODE>
 * <DT><I>Separator</I> one of:
 *     <DD>The ASCII '(' character, aka "left parenthesis"
 *     <DD>The ASCII ')' character, aka "right parenthesis"
 *     <DD>The stored "argument list start" token
 *     <DD>The stored "argument separator" token
 *     <DD>The stored "argument list end" token
 * </DL></CODE>
 *
 * <P>The syntactical grammar for the language 
 * defined by this parser is as follows:</P>
 * <CODE>
 * <DL>
 * <DT><I>Expression</I>:
 *     <DD><I>NumericExpression</I>
 * </DL></CODE>
 *
 * <P>Since this is largely a numeric evaluation language,
 * all values are considered to be numeric values
 * in the specification below.  This parser does support
 * boolean values and boolean expressions which are
 * handled in the standard fashion.</P>
 *
 * <CODE>
 * <DT><I>NumericExpression</I>:
 *     <DD>( <I>NumericExpression</I> )</DD>
 *     <DD><I>UnaryOperation NumericExpression</I>
 *     <DD><I>NumericExpression BinaryOperation
 *            NumericExpression</I>
 *     <DD><I>ProceduralExpression</I>
 *     <DD><I>Number</I>
 *     <DD><I>Constant</I>
 * </DL></CODE>
 *
 * <P>As stated above, all arithmetic operations are carried
 * out either as <CODE>BigInteger</CODE> or <CODE>double</CODE>.</P>
 *
 * <P>Overflow is extremely unlikely in <CODE>BigInteger</CODE>.
 * Overflow when using <CODE>double</CODE> is handled by the
 * use of the IEEE values +Infinity, -Infinity, and Nan.</P>
 *
 * <CODE>
 * <DT><I>UnaryOperation</I> one of:
 *     <DD>! + -
 * <DT><I>BinaryOperation</I>:
 *     <DD><I>Operation</I> but not !
 * </DL></CODE>
 *
 * <P>The following six procedures will maintain the
 * internal data type as <CODE>BigInteger</CODE> or
 * <CODE>double</CODE>:
 * <CODE>abs</CODE>,
 * <CODE>cieling</CODE>,
 * <CODE>floor</CODE>,
 * <CODE>round</CODE>,
 * <CODE>max</CODE>,
 * <CODE>min</CODE>.
 *
 * <P>All other procedures are calculated using
 * <CODE>double</CODE> values.</P>
 *
 * <CODE>
 * <DT><I>ProceduralExpression</I>:
 *     <DD><I>ProcedureIdentifier ArgumentList</I>
 * <DT><I>ProcedureIdentifier</I> one of:
 *     <DD>
 *      <TABLE><CODE>
 *          <TR>
 *              <TD><B><I>name</I></B>
 *              <TD><B><I>usage</I></B>
 *              <TD><B><I>explanation</I></B>
 *          <TR>
 *              <TD>abs
 *              <TD>abs(x)
 *              <TD>absolute value of x
 *          <TR>
 *              <TD>ceiling
 *              <TD>ceiling(x)
 *              <TD>smallest integral i >= x
 *          <TR>
 *              <TD>floor
 *              <TD>floor(x)
 *              <TD>largest integral i <= x
 *          <TR>
 *              <TD>round
 *              <TD>round(x)
 *              <TD>nearest integral i to x
 *          <TR>
 *              <TD>max
 *              <TD>max(x, y)
 *              <TD>maximum of x and y
 *          <TR>
 *              <TD>min
 *              <TD>min(x, y)
 *              <TD>minimum of x and y
 *          <TR>
 *              <TD>sqrt
 *              <TD>sqrt(x)
 *              <TD>square root of x for x >= 0
 *          <TR>
 *              <TD>power
 *              <TD>power(x, y)
 *              <TD>x to the power y for x > 0 or x == 0 with y > 0
 *          <TR>
 *              <TD>todegrees
 *              <TD>todegrees(x)
 *              <TD>convert radians to degrees
 *          <TR>
 *              <TD>toradians
 *              <TD>toradians(x)
 *              <TD>convert degrees to radians 
 *          <TR>
 *              <TD>sin
 *              <TD>sin(x)
 *              <TD>sine of x for x in radians
 *          <TR>
 *              <TD>sindeg
 *              <TD>sindeg(x)
 *              <TD>sine of x for x in degrees
 *          <TR>
 *              <TD>cos
 *              <TD>cos(x)
 *              <TD>cosine of x for x in radians
 *          <TR>
 *              <TD>cosdeg
 *              <TD>cosdeg(x)
 *              <TD>cosine of x for x in degrees
 *          <TR>
 *              <TD>tan
 *              <TD>tan(x)
 *              <TD>tangent of x for x in radians
 *          <TR>
 *              <TD>tandeg
 *              <TD>tandeg(x)
 *              <TD>tangent of x for x in degrees
 *          <TR>
 *              <TD>asin
 *              <TD>asin(x)
 *              <TD>arcsine in radians of x for -1 <= x <= 1
 *          <TR>
 *              <TD>asindeg
 *              <TD>asindeg(x)
 *              <TD>arcsine in degrees of x for -1 <= x <= 1
 *          <TR>
 *              <TD>acos
 *              <TD>acos(x)
 *              <TD>arccosine in radians of x for -1 <= x <= 1
 *          <TR>
 *              <TD>acosdeg
 *              <TD>acosdeg(x)
 *              <TD>arccosine in degrees of x for -1 <= x <= 1
 *          <TR>
 *              <TD>atan
 *              <TD>atan(x)
 *              <TD>arctangent in radians of x
 *          <TR>
 *              <TD>atandeg
 *              <TD>atandeg(x)
 *              <TD>arctangent in degrees of x
 *          <TR>
 *              <TD>atan2
 *              <TD>atan2(y, x)
 *              <TD>arctangent in radians of point (x, y)
 *          <TR>
 *              <TD>atan2deg
 *              <TD>atan2deg(y, x)
 *              <TD>arctangent in degrees of point (x, y)
 *          <TR>
 *              <TD>exp
 *              <TD>exp(x)
 *              <TD>e to the power x for e = 2.718...
 *          <TR>
 *              <TD>log
 *              <TD>log(x)
 *              <TD>natural log of x for x > 0
 *          <TR>
 *              <TD>ln
 *              <TD>ln(x)
 *              <TD>synomym for natural log of x for x > 0
 *          <TR>
 *              <TD>logtobase
 *              <TD>logtobase(x, b)
 *              <TD>log of x to base b for x > 0 and b > 0
 *          <TR>
 *              <TD>random
 *              <TD>random(x, y)
 *              <TD>random number between x and y
 *      </CODE></TABLE>
 * <DT><I>ArgumentList</I>:
 *     <DD><I>Start End</I>
 *     <DD><I>Start NumericExpression 
 *            Arguments<SUB>opt</SUB> End</I>
 * <DT><I>Start</I>:
 *     <DD>The stored "argument list start" token
 * <DT><I>End</I>:
 *     <DD>The stored "argument list end" token
 * <DT><I>Arguments</I>:
 *     <DD><I>ArgumentSeparator NumericExpression 
 *            Arguments<SUB>opt</SUB></I>
 * <DT><I>ArgumentSeparator</I>
 *     <DD>The stored "argument separator" token
 * </DL></CODE>
 *
 * <P>The constants <CODE>e</CODE> and <CODE>pi</CODE>
 * are real-numbers with their value taken from
 * the constants provided in the 
 * Java <CODE>Math</CODE> class.
 *
 * The constants <CODE>true</CODE> and <CODE>false</CODE>
 * are boolean values and are the only way to represent
 * a primitive boolean value 
 * without operation on numeric values.</P>
 * <CODE>
 * <DT><I>Constant</I> one of:
 *     <DD>e pi true false Infinity NaN
 * </DL></CODE>
 *
 * @author  Jeff Raab
 * @author  Richard Rasala
 * @version 2.2
 * @since   1.0
 * @see <A HREF="http://java.sun.com/docs/books/jls/html/index.html">
 *      Java Language Specification</A> 
 */
public class JPTParser 
    extends AbstractParser
// debug
//    implements ConsoleAware
{
    ////////////
    // Parser //
    ////////////

    public Object parse(String d) 
        throws ParseException 
    {
        // initialize variables
        data = d;
        next = 0;

        // sanity check for null data
        if (data == null)
            throw new ParseException("Input was null.", -1);
            
        // evaluate the expression
        ObjectOperationPair ooPair
            = parseExpression(new ObjectOperationPair(null, identity));
        
        // debug
        // showState(ooPair);
        
        Object value = ooPair.value;
        
        // if value is null report error
        if (value == null)
            throw new ParseException("Expected expression.", next);
        
        // if there is more data to parse report error
        if (next < data.length())
            throw new ParseException("Expected end of expression.", next);
        
        return value;
    }

    ///////////////
    // Operations //
    ///////////////
    
    /** NumericOperation to implement plus. */
    protected NumericOperation opPlus;
    
    /** NumericOperation to implement minus. */
    protected NumericOperation opMinus;
    
    /** NumericOperation to implement times. */
    protected NumericOperation opTimes;
    
    /** NumericOperation to implement divide. */
    protected NumericOperation opSlash;
    
    /** NumericOperation to implement remainder. */
    protected NumericOperation opPercent;
    
    /**
     * Define the numeric operations.
     *
     * Replaces the method named defineNumericOperators.
     *
     * @since 2.2
     */
    protected void defineNumericOperations() {
        opPlus = new NumericOperation("+", true, true) {
            public Object unaryForXIntegral(XBigInteger a) {
                return a;
            }
            
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                return new XBigInteger(va.add(vb));
            }
            
            public Object unaryForXFloating(XDouble x) {
                return x;
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XDouble(vx + vy);
            }
        };
            
        opMinus = new NumericOperation("-", true, true) {
            public Object unaryForXIntegral(XBigInteger a) {
                BigInteger va = a.getValue();
                
                return new XBigInteger(va.negate());
            }
            
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                return new XBigInteger(va.subtract(vb));
            }
            
            public Object unaryForXFloating(XDouble x) {
                double vx = x.getValue();
                
                return new XDouble(-vx);
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XDouble(vx - vy);
            }
        };
            
        opTimes = new NumericOperation("*", false, true) {
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                return new XBigInteger(va.multiply(vb));
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XDouble(vx * vy);
            }
        };
        
        opSlash = new NumericOperation("/", false, true) {
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                if (vb.equals(BigInteger.ZERO)) {
                    int test = va.compareTo(BigInteger.ZERO);
                    
                    if (test > 0)
                        return new XDouble(Double.POSITIVE_INFINITY);
                    
                    if (test < 0)
                        return new XDouble(Double.NEGATIVE_INFINITY);
                    
                    return new XDouble(Double.NaN);
                }
                
                return new XBigInteger(va.divide(vb));
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XDouble(vx / vy);
            }
        };
        
        opPercent = new NumericOperation("%", false, true) {
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                if (vb.equals(BigInteger.ZERO))
                    return new XDouble(Double.NaN);
                
                return new XBigInteger(va.remainder(vb));
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XDouble(vx % vy);
            }
        };
    }
    
    /** BooleanOperation to implement equals. */
    protected BooleanOperation opEQ;
    
    /** BooleanOperation to implement not equals. */
    protected BooleanOperation opNE;
    
    /** BooleanOperation to implement less than. */
    protected BooleanOperation opLT;
    
    /** BooleanOperation to implement greater than. */
    protected BooleanOperation opGT;
    
    /** BooleanOperation to implement less than or equals. */
    protected BooleanOperation opLE;
    
    /** BooleanOperation to implement greater than or equals. */
    protected BooleanOperation opGE;
    
    /** BooleanOperation to implement and (as double ampersand). */
    protected BooleanOperation opAND;
    
    /** BooleanOperation to implement or (as double bar). */
    protected BooleanOperation opOR;
    
    /** BooleanOperation to implement not (as exclamation). */
    protected BooleanOperation opNOT;
    
    /**
     * Define the boolean operations.
     *
     * Replaces the method named defineBooleanOperators
     *
     * @since 2.2
     */
    protected void defineBooleanOperations() {
        opEQ = new BooleanOperation("==", false, true, true) {
            public Object binaryForXBoolean(XBoolean p, XBoolean q) {
                boolean vp = p.getValue();
                boolean vq = q.getValue();
                
                return new XBoolean(vp == vq);
            }
        
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                int test = va.compareTo(vb);
                    
                return new XBoolean(test == 0);
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XBoolean(vx == vy);
            }
        };
        
        opNE = new BooleanOperation("!=", false, true, true) {
            public Object binaryForXBoolean(XBoolean p, XBoolean q) {
                boolean vp = p.getValue();
                boolean vq = q.getValue();
                
                return new XBoolean(vp != vq);
            }
        
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                int test = va.compareTo(vb);
                    
                return new XBoolean(test != 0);
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XBoolean(vx != vy);
            }
        };
        
        opLT = new BooleanOperation("<", false, true, true) {
            public Object binaryForXBoolean(XBoolean p, XBoolean q) {
                boolean vp = p.getValue();
                boolean vq = q.getValue();
                
                return new XBoolean(!vp && vq);
            }
        
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                int test = va.compareTo(vb);
                    
                return new XBoolean(test < 0);
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XBoolean(vx < vy);
            }
        };
        
        opGT = new BooleanOperation(">", false, true, true) {
            public Object binaryForXBoolean(XBoolean p, XBoolean q) {
                boolean vp = p.getValue();
                boolean vq = q.getValue();
                
                return new XBoolean(vp && !vq);
            }
        
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                int test = va.compareTo(vb);
                    
                return new XBoolean(test > 0);
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XBoolean(vx > vy);
            }
        };
        
        opLE = new BooleanOperation("<=", false, true, true) {
            public Object binaryForXBoolean(XBoolean p, XBoolean q) {
                boolean vp = p.getValue();
                boolean vq = q.getValue();
                
                return new XBoolean(!vp || vq);
            }
        
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                int test = va.compareTo(vb);
                    
                return new XBoolean(test <= 0);
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XBoolean(vx <= vy);
            }
        };
        
        opGE = new BooleanOperation(">=", false, true, true) {
            public Object binaryForXBoolean(XBoolean p, XBoolean q) {
                boolean vp = p.getValue();
                boolean vq = q.getValue();
                
                return new XBoolean(vp || !vq);
            }
        
            public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
                BigInteger va = a.getValue();
                BigInteger vb = b.getValue();
                
                int test = va.compareTo(vb);
                    
                return new XBoolean(test >= 0);
            }
            
            public Object binaryForXFloating(XDouble x, XDouble y) {
                double vx = x.getValue();
                double vy = y.getValue();
                
                return new XBoolean(vx >= vy);
            }
        };
        
        opAND = new BooleanOperation("&&", false, true, false) {
            public Object binaryForXBoolean(XBoolean p, XBoolean q) {
                boolean vp = p.getValue();
                boolean vq = q.getValue();
                
                return new XBoolean(vp && vq);
            }
        };
        
        opOR = new BooleanOperation("||", false, true, false) {
            public Object binaryForXBoolean(XBoolean p, XBoolean q) {
                boolean vp = p.getValue();
                boolean vq = q.getValue();
                
                return new XBoolean(vp || vq);
            }
        };
        
        opNOT = new BooleanOperation("!", true, false, false) {
            public Object unaryForXBoolean(XBoolean p) {
                boolean vp = p.getValue();
                
                return new XBoolean(! vp);
            }
        };
        
    }
    
    /**
     * Adds the standard operations for this parser 
     * to the operation table.
     *
     * Replaces the method named addOperators.
     *
     * @since 2.2
     */
    protected void addOperations() {
        
        defineNumericOperations();
        defineBooleanOperations();
        
        /**
         * Follows Arnold, Gosling, Holmes
         * The Java Programming Language Third Edition, 6.10
         */
        
        addOperationAfterPrecedenceOf(identity, opOR      );
        
        addOperationAfterPrecedenceOf(opOR    , opAND     );
        
        addOperationAfterPrecedenceOf(opAND   , opEQ      );
        addOperationAtPrecedenceOf   (opEQ    , opNE      );
        
        addOperationAfterPrecedenceOf(opEQ    , opLT      );
        addOperationAtPrecedenceOf   (opLT    , opGT      );
        addOperationAtPrecedenceOf   (opLT    , opLE      );
        addOperationAtPrecedenceOf   (opLT    , opGE      );
        
        addOperationAfterPrecedenceOf(opLT    , opPlus    );
        addOperationAtPrecedenceOf   (opPlus  , opMinus   );
        
        addOperationAfterPrecedenceOf(opPlus  , opTimes   );
        addOperationAtPrecedenceOf   (opTimes , opSlash   );
        addOperationAtPrecedenceOf   (opTimes , opPercent );
        
        addOperationAfterPrecedenceOf(opTimes , opNOT     );
    }
    
    ////////////////
    // Procedures //
    ////////////////
    
    /** Procedure to implement <CODE>abs</CODE>. */
    protected Procedure procAbs;
    
    /** Procedure to implement <CODE>ceiling</CODE>. */
    protected Procedure procCeiling;
    
    /** Procedure to implement <CODE>floor</CODE>. */
    protected Procedure procFloor;
    
    /** Procedure to implement <CODE>round</CODE>. */
    protected Procedure procRound;
    
    /** Procedure to implement <CODE>max</CODE>. */
    protected Procedure procMax;
    
    /** Procedure to implement <CODE>min</CODE>. */
    protected Procedure procMin;
    
    /** Procedure to implement <CODE>sqrt</CODE>. */
    protected Procedure procSqrt;
    
    /** Procedure to implement <CODE>power</CODE>. */
    protected Procedure procPower;
    
    /** Procedure to implement <CODE>todegrees</CODE>. */
    protected Procedure procToDegrees;
    
    /** Procedure to implement <CODE>toradians</CODE>. */
    protected Procedure procToRadians;
    
    /** Procedure to implement <CODE>sin</CODE>. */
    protected Procedure procSin;
    
    /** Procedure to implement <CODE>sindeg</CODE>. */
    protected Procedure procSinDeg;
    
    /** Procedure to implement <CODE>cos</CODE>. */
    protected Procedure procCos;
    
    /** Procedure to implement <CODE>cosdeg</CODE>. */
    protected Procedure procCosDeg;
    
    /** Procedure to implement <CODE>tan</CODE>. */
    protected Procedure procTan;
    
    /** Procedure to implement <CODE>tandeg</CODE>. */
    protected Procedure procTanDeg;
    
    /** Procedure to implement <CODE>asin</CODE>. */
    protected Procedure procASin;
    
    /** Procedure to implement <CODE>asindeg</CODE>. */
    protected Procedure procASinDeg;
    
    /** Procedure to implement <CODE>acos</CODE>. */
    protected Procedure procACos;
    
    /** Procedure to implement <CODE>acosdeg</CODE>. */
    protected Procedure procACosDeg;
    
    /** Procedure to implement <CODE>atan</CODE>. */
    protected Procedure procATan;
    
    /** Procedure to implement <CODE>atandeg</CODE>. */
    protected Procedure procATanDeg;
    
    /** Procedure to implement <CODE>atan2</CODE>. */
    protected Procedure procATan2;
    
    /** Procedure to implement <CODE>atan2deg</CODE>. */
    protected Procedure procATan2Deg;
    
    /** Procedure to implement <CODE>exp</CODE>. */
    protected Procedure procExp;
    
    /** Procedure to implement <CODE>ln</CODE>. This is identical to log. */
    protected Procedure procLn;
    
    /** Procedure to implement <CODE>log</CODE>. This is identical to ln. */
    protected Procedure procLog;
    
    /** Procedure to implement <CODE>logtobase</CODE>. */
    protected Procedure procLogToBase;
    
    /** Procedure to implement <CODE>random</CODE>. */
    protected Procedure procRandom;
    
    /**
     * Define the procedures.
     *
     * @since 2.1
     */
    protected void defineProcedures() {
        procAbs = new Procedure("abs", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                if (ParserUtilities.isXIntegral(args[0])) {
                    XBigInteger a = ParserUtilities.toXBigInteger((XNumber) args[0]);
                    BigInteger va = a.getValue();
                    
                    int test = va.compareTo(BigInteger.ZERO);
                    
                    if (test >= 0)
                        return a;
                    
                    va = va.negate();
                    
                    return new XBigInteger(va);
                }
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                if (vx >= 0)
                    return x;
                
                return new XDouble(-vx);
            }
        };
        
        procCeiling = new Procedure("ceiling", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                if (ParserUtilities.isXIntegral(args[0])) {
                    XBigInteger a = ParserUtilities.toXBigInteger((XNumber) args[0]);
                    return a;
                }
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                vx = Math.ceil(vx);
                return new XDouble(vx);
            }
        };
        
        procFloor = new Procedure("floor", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                if (ParserUtilities.isXIntegral(args[0])) {
                    XBigInteger a = ParserUtilities.toXBigInteger((XNumber) args[0]);
                    return a;
                }
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                vx = Math.floor(vx);
                return new XDouble(vx);
            }
        };
        
        procRound = new Procedure("round", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                if (ParserUtilities.isXIntegral(args[0])) {
                    XBigInteger a = ParserUtilities.toXBigInteger((XNumber) args[0]);
                    return a;
                }
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                vx = Math.rint(vx);
                return new XDouble(vx);
            }
        };
        
        procMax = new Procedure("max", 2) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                if (ParserUtilities.isXIntegral(args[0]) &&
                    ParserUtilities.isXIntegral(args[1]))
                {
                    XBigInteger a = ParserUtilities.toXBigInteger((XNumber) args[0]);
                    XBigInteger b = ParserUtilities.toXBigInteger((XNumber) args[1]);
                    BigInteger va = a.getValue();
                    BigInteger vb = b.getValue();
                    
                    int test = va.compareTo(vb);
                    
                    if (test >= 0)
                        return a;
                    
                    return b;
                }
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                XDouble y = ParserUtilities.toXDouble((XNumber) args[1]);
                double vx = x.getValue();
                double vy = y.getValue();
                
                if (vx >= vy)
                    return x;
                
                return y;
            }
        };
        
        procMin = new Procedure("min", 2) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                if (ParserUtilities.isXIntegral(args[0]) &&
                    ParserUtilities.isXIntegral(args[1]))
                {
                    XBigInteger a = ParserUtilities.toXBigInteger((XNumber) args[0]);
                    XBigInteger b = ParserUtilities.toXBigInteger((XNumber) args[1]);
                    BigInteger va = a.getValue();
                    BigInteger vb = b.getValue();
                    
                    int test = va.compareTo(vb);
                    
                    if (test >= 0)
                        return b;
                    
                    return a;
                }
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                XDouble y = ParserUtilities.toXDouble((XNumber) args[1]);
                double vx = x.getValue();
                double vy = y.getValue();
                
                if (vx >= vy)
                    return y;
                
                return x;
            }
        };
        
        procSqrt = new Procedure("sqrt", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                if (vx == 0)
                    return x;
                
                if (vx > 0)
                    return new XDouble(Math.sqrt(vx));
                
                throw new ParseException(
                    "Negative argument " + vx + " to sqrt procedure"
                    , 0);
            }
        };
        
        procPower = new Procedure("power", 2) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                XDouble y = ParserUtilities.toXDouble((XNumber) args[1]);
                double vx = x.getValue();
                double vy = y.getValue();
                
                // when the base is zero
                if ((vx == 0) && (vy > 0))
                    return x;
                
                // when the base is positive
                if (vx > 0)
                    return new XDouble(Math.pow(vx, vy));
                
                throw new ParseException(
                    "Invalid arguments " + vx + "," + vy + " to power procedure"
                    , 0);
            }
        };
        
        procToDegrees = new Procedure("todegrees", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.toDegrees(vx));
            }
        };
        
        procToRadians = new Procedure("toradians", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.toRadians(vx));
            }
        };
        
        procSin = new Procedure("sin", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.sin(vx));
            }
        };
        
        procSinDeg = new Procedure("sindeg", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.sin(Math.toRadians(vx)));
            }
        };
        
        procCos = new Procedure("cos", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.cos(vx));
            }
        };
        
        procCosDeg = new Procedure("cosdeg", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.cos(Math.toRadians(vx)));
            }
        };
        
        procTan = new Procedure("tan", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.tan(vx));
            }
        };
        
        procTanDeg = new Procedure("tandeg", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.tan(Math.toRadians(vx)));
            }
        };
        
        procASin = new Procedure("asin", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                if ((vx < -1) || (vx > 1))
                    throw new ParseException(
                        "Invalid argument " + vx + " to asin procedure"
                        , 0);

                return new XDouble(Math.asin(vx));
            }
        };
        
        procASinDeg = new Procedure("asindeg", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                if ((vx < -1) || (vx > 1))
                    throw new ParseException(
                        "Invalid argument " + vx + " to asindeg procedure"
                        , 0);

                return new XDouble(Math.toDegrees(Math.asin(vx)));
            }
        };
        
        procACos = new Procedure("acos", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                if ((vx < -1) || (vx > 1))
                    throw new ParseException(
                        "Invalid argument " + vx + " to acos procedure"
                        , 0);

                return new XDouble(Math.acos(vx));
            }
        };
        
        procACosDeg = new Procedure("acosdeg", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                if ((vx < -1) || (vx > 1))
                    throw new ParseException(
                        "Invalid argument " + vx + " to acosdeg procedure"
                        , 0);

                return new XDouble(Math.toDegrees(Math.acos(vx)));
            }
        };
        
        procATan = new Procedure("atan", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.atan(vx));
            }
        };
        
        procATanDeg = new Procedure("atandeg", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.toDegrees(Math.atan(vx)));
            }
        };
        
        procATan2 = new Procedure("atan2", 2) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                // note that for atan2, y precedes x
                XDouble y = ParserUtilities.toXDouble((XNumber) args[0]);
                XDouble x = ParserUtilities.toXDouble((XNumber) args[1]);
                double vy = y.getValue();
                double vx = x.getValue();
                
                if ((vx == 0) && (vy == 0))
                    throw new ParseException(
                        "Invalid arguments " + vx + "," + vy + " to atan2 procedure"
                        , 0);
                
                return new XDouble(Math.atan2(vy, vx));
            }
        };
        
        procATan2Deg = new Procedure("atan2deg", 2) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                // note that for atan2deg, y precedes x
                XDouble y = ParserUtilities.toXDouble((XNumber) args[0]);
                XDouble x = ParserUtilities.toXDouble((XNumber) args[1]);
                double vy = y.getValue();
                double vx = x.getValue();
                
                if ((vx == 0) && (vy == 0))
                    throw new ParseException(
                        "Invalid arguments " + vx + "," + vy + " to atan2deg procedure"
                        , 0);
                
                return new XDouble(Math.toDegrees(Math.atan2(vy, vx)));
            }
        };
        
        procExp = new Procedure("exp", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                return new XDouble(Math.exp(vx));
            }
        };
        
        procLn = new Procedure("ln", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                if (vx > 0)
                    return new XDouble(Math.log(vx));
                
                throw new ParseException(
                    "Invalid argument " + vx + " to ln procedure"
                    , 0);
            }
        };
        
        procLog = new Procedure("log", 1) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                double vx = x.getValue();
                
                if (vx > 0)
                    return new XDouble(Math.log(vx));
                
                throw new ParseException(
                    "Invalid argument " + vx + " to log procedure"
                    , 0);
            }
        };
        
        procLogToBase = new Procedure("logtobase", 2) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                XDouble b = ParserUtilities.toXDouble((XNumber) args[1]);
                double vx = x.getValue();
                double vb = b.getValue();
                
                if ((vx > 0) && (vb > 0))
                    return new XDouble(Math.log(vx) / Math.log(vb));
                
                throw new ParseException(
                    "Invalid arguments " + vx + "," + vb + " to logtobase procedure"
                    , 0);
            }
        };
        
        procRandom = new Procedure("random", 2) {
            public Object procedureCall(Object[] args)
                throws ParseException
            {
                checkArgs(args);
                
                XDouble x = ParserUtilities.toXDouble((XNumber) args[0]);
                XDouble y = ParserUtilities.toXDouble((XNumber) args[1]);
                double vx = x.getValue();
                double vy = y.getValue();
                double r  = Math.random();
                
                return new XDouble((1 - r) * vx + r * vy);
            }
        };
        
    }
    
    /**
     * Adds the standard procedures for this parser
     * to the procedure table.
     */
    protected void addProcedures() {
        defineProcedures();
    
        addProcedure(procAbs);
        addProcedure(procCeiling);
        addProcedure(procFloor);
        addProcedure(procRound);
        addProcedure(procMax);
        addProcedure(procMin);
    
        addProcedure(procSqrt);
        addProcedure(procPower);
    
        addProcedure(procToDegrees);
        addProcedure(procToRadians);
    
        addProcedure(procSin);
        addProcedure(procSinDeg);
        addProcedure(procCos);
        addProcedure(procCosDeg);
        addProcedure(procTan);
        addProcedure(procTanDeg);
    
        addProcedure(procASin);
        addProcedure(procASinDeg);
        addProcedure(procACos);
        addProcedure(procACosDeg);
        addProcedure(procATan);
        addProcedure(procATanDeg);
        addProcedure(procATan2);
        addProcedure(procATan2Deg);
    
        addProcedure(procExp);
        addProcedure(procLn);
        addProcedure(procLog);
        addProcedure(procLogToBase);
    
        addProcedure(procRandom);
    }
    
    ///////////////
    // Constants //
    ///////////////
    
    /**
     * Adds the standard constants for this parser
     * to the environment.
     */
    protected void addConstants() {
        addConstant("pi", new XDouble(Math.PI));
        addConstant("e", new XDouble(Math.E));
        addConstant("true", new XBoolean(true));
        addConstant("false", new XBoolean(false));
        addConstant("Infinity", new XDouble(Double.POSITIVE_INFINITY));
        addConstant("NaN", new XDouble(Double.NaN));
    }
    
    ////////////////////
    // Parser Details //
    ////////////////////
    
    /**
     * Parses the next expression 
     * in the data <CODE>String</CODE>.
     *
     * Replaces method with the same name due to changes in
     * the parameter and return type name.
     *
     * @param  standing the value of the left operand
     *         and the operation immediately preceding
     *         the expression to be parsed
     * @return the value of the parsed expression
     *         and the operation immediately following it
     * @throws ParseException if the expression is malformed
     *         or if the incoming operation is <CODE>null</CODE>
     * @since 2.2
     */
    protected ObjectOperationPair parseExpression
        (ObjectOperationPair standing) throws ParseException 
    {
        // debug
        // showState(standing);
        
        Object left = standing.value;
        Object term = null;
        
        Operation oldOp = standing.operation;
        Operation newOp = null;
        
        int oldPr = precedenceOf(oldOp);
        int newPr = -1;
        
        if (oldOp == null)
            throw new ParseException
                ("Expected binary operation.", next);
        
        // repeat until all consecutive binary operations at the current level
        // of precedence or higher have been carried out
        while (true) {
            term  = nextTerm();
            newOp = nextOperation();
            newPr = precedenceOf(newOp);
            
            if ((newOp != null) && (! newOp.isBinary()))
                throw new ParseException
                    ("Expected binary operation.", next);
            
            // loop and use recursion to handle higher levels of precedence
            while (newPr > oldPr) {
                ObjectOperationPair ooPair
                    = parseExpression(new ObjectOperationPair(term, newOp));
                
                term  = ooPair.value;
                newOp = ooPair.operation;
                newPr = precedenceOf(newOp);
            }
            
            // now handle this level of precedence
            term = oldOp.performOperation(left, term);
            
            if (newPr == oldPr) {
                left  = term;
                oldOp = newOp;
            }
            else {
                // exit if next operation has a lower level of precedence
                return new ObjectOperationPair(term, newOp);
            }
        }
    }
    
    /**
     * Returns the next term in the data <CODE>String</CODE> with all
     * leading unary operations already applied
     * or throws a <CODE>ParseException</CODE> if no term is present.
     *
     * @since 2.2
     */
    protected Object nextTerm() throws ParseException {
        Operation[] operations = nextUnaryOperations();
        int length = operations.length;
        
        Object term = nextSimpleTerm();
        
        // apply unary operations from right to left
        for (int i = (length - 1); i >= 0; i--)
            term = operations[i].performOperation(null, term);
        
        return term;
    }
    
    /**
     * Returns the next simple term in the data <CODE>String</CODE>
     * or throws a <CODE>ParseException</CODE> if no term is present.
     *
     * @since 2.2
     */
    protected Object nextSimpleTerm() throws ParseException {
        Object term = null;

        if (nextTokenIs(NESTED_EXPRESSION_START))
            term = parseNestedExpression();
            
        else if (startsNumber())
            term = parseNumber();

        else if (startsIdentifier()) {
        
            String identifier = parseIdentifier();
            
            if (nextTokenIs(ARGUMENT_LIST_START))
                term = parseProcedureCall(identifier);
            
            else
                term = parseVariable(identifier);
        }
        else {
            throw new ParseException
                ("Expected term.", next);
        }
        
        return term;
    }
    
    /**
     * Returns the array of unary operations that occur after the current
     * position in the data <CODE>String</CODE>
     * and throws a ParseException if an operation is encountered that is
     * binary but not unary.
     *
     * @since 2.2
     */
    protected Operation[] nextUnaryOperations() throws ParseException {
        Vector operations = new Vector();
        
        while (true) {
            Operation operation = nextOperation();
            
            if (operation == null)
                break;
            
            if (operation.isUnary())
                operations.add(operation);
            else
                throw new ParseException
                    ("Expected unary operation.", next);
        }
        
        return (Operation[]) operations.toArray(new Operation[0]);
    }
    
    /**
     * Parse an expression in parentheses and return its value.
     *
     * @since 2.2
     */
    protected Object parseNestedExpression()
        throws ParseException
    {
        Object term = null;
        
        if (nextTokenIs(NESTED_EXPRESSION_START)) {
            // eat left parenthesis
            next++;
            
            // parse contained expression
            ObjectOperationPair ooPair
                = parseExpression(new ObjectOperationPair(null, identity));
            
            // debug
            // showState(ooPair);
            
            term = ooPair.value;
            
            // ignore whitespace and report error if necessary
            skipWhitespace();
            
            if ((next == data.length()) || (! nextTokenIs(NESTED_EXPRESSION_END))) 
                throw new ParseException
                    ("Expected end of nested expression.", next);

            // eat right parenthesis
            next++;
        }
        
        return term;
    }
    
    /**
     * Parse a procedure with arguments and return its value.
     *
     * @since 2.2
     */
    protected Object parseProcedureCall(String identifier)
        throws ParseException
    {
        // test if the identifier represents a procedure
        if (! procedures.containsKey(identifier))
            throw new ParseException
                ("Unrecognized procedure " + identifier + ".", next);
        
        // get the procedure
        Procedure procedure = (Procedure)procedures.get(identifier);
        
        Object term = null;
        
        // get the arguments and evaluate the procedure
        try {
            term = procedure.procedureCall(parseArgumentList());
        }
        catch (ParseException exception) {
            throw new ParseException(exception.getMessage(), next);
        }
        
        return term;
    }
    
    /**
     * Parse a variable expression and return its value.
     *
     * @since 2.2
     */
    protected Object parseVariable(String identifier)
        throws ParseException
    {

        // test that this variable is in the environment
        if (!environment.containsKey(identifier))
            throw new ParseException("Unrecognized identifier.", next);
        
        // obtain variable from environment
        return environment.get(identifier);
    }
    
    ///////////////////
    // Inner Classes //
    ///////////////////
    
    /**
     * <P>Class used to store a numeric operation.</P>
     *
     * Replaces the class named NumericOperator.
     *
     * @author  Jeff Raab
     * @author  Richard Rasala
     * @version 2.2
     * @since 2.2
     */
    public static class NumericOperation extends Operation {
        
        /** Constructs an operation with the default symbol. */
        public NumericOperation() {}
        
        /**
         * Constructs an operation with the given symbol.
         *
         * @param s the symbol for the operation
         */
        public NumericOperation(String s) { super(s); }
        
        /**
         * Constructs an operation with the given symbol and settings
         * for unary and binary usage.
         *
         * @param s      the symbol for the operation
         * @param unary  whether the operation may be unary
         * @param binary whether the operation may be binary
         */
        public NumericOperation(String s, boolean unary, boolean binary) {
            super(s, unary, binary);
        }
        
        /**
         * Performs the operation on the given values and returns
         * the result.
         *
         * Replaces the method named operationPerformed.
         *
         * @param left  the left side operand for a binary operation
         *              or <CODE>null</CODE> for a unary operation
         * @param right the right side operand for a unary or binary
         *              operation
         * @since 2.2
         */
        public final Object performOperation(Object left, Object right) 
            throws ParseException
        {
            XBigInteger a;
            XBigInteger b;
            
            XDouble x;
            XDouble y;
            
            if (left == null)
            // handle unary case
            {
                checkUnary();
                
                if (ParserUtilities.isXIntegral(right)) {
                    a = ParserUtilities.toXBigInteger((XNumber)right);
                    
                    return unaryForXIntegral(a);
                }
                
                if (ParserUtilities.isXNumber(right)) {
                    x = ParserUtilities.toXDouble((XNumber)right);
                    
                    return unaryForXFloating(x);
                }
                
                throw new ParseException(
                    "Numeric operation "
                    + symbol
                    + " expects 1 numeric argument."
                    , 0);
            }
            else
            // handle binary case
            {
                checkBinary();
            
                if (ParserUtilities.isXIntegral(left) &&
                    ParserUtilities.isXIntegral(right))
                {
                    a = ParserUtilities.toXBigInteger((XNumber)left);
                    b = ParserUtilities.toXBigInteger((XNumber)right);
                    
                    return binaryForXIntegral(a, b);
                }
                
                if (ParserUtilities.isXNumber(left) &&
                    ParserUtilities.isXNumber(right))
                {
                    x = ParserUtilities.toXDouble((XNumber)left);
                    y = ParserUtilities.toXDouble((XNumber)right);
                    
                    return binaryForXFloating(x, y);
                }
                
                throw new ParseException(
                    "Numeric operation "
                    + symbol
                    + " expects 2 numeric arguments."
                    , 0);
            }
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a unary operation on an integral argument.
         *
         * @param a the argument
         */
        public Object unaryForXIntegral(XBigInteger a) {
            return null;
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a binary operation on integral arguments.
         *
         * @param a argument 1
         * @param b argument 2
         */
        public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
            return null;
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a unary operation on a floating argument.
         *
         * @param x the argument
         */
        public Object unaryForXFloating(XDouble x) {
            return null;
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a binary operation on floating arguments.
         *
         * @param x argument 1
         * @param y argument 2
         */
        public Object binaryForXFloating(XDouble x, XDouble y) {
            return null;
        }
        
    }
    
    /**
     * <P>Class used to store a boolean operation.</P>
     *
     * Replaces the class named BooleanOperator.
     *
     * @author  Jeff Raab
     * @author  Richard Rasala
     * @version 2.2
     * @since   2.2
     */
    public static class BooleanOperation extends Operation {
        
        /** Whether or not the operation can accept boolean or numeric arguments. */
        public boolean booleanOrNumeric = true;
        
        /** Constructs an operation with the default symbol. */
        public BooleanOperation() {}
        
        /**
         * Constructs an operation with the given symbol.
         *
         * @param s the symbol for the operation
         */
        public BooleanOperation(String s) { super(s); }
        
        /**
         * Constructs an operation with the given symbol and settings
         * for unary and binary usage.
         *
         * @param s      the symbol for the operation
         * @param unary  whether the operation may be unary
         * @param binary whether the operation may be binary
         */
        public BooleanOperation(String s, boolean unary, boolean binary) {
            super(s, unary, binary);
        }
        
        /**
         * Constructs an operation with the given symbol and settings
         * for unary and binary usage.
         *
         * @param s      the symbol for the operation
         * @param unary  whether the operation may be unary
         * @param binary whether the operation may be binary
         * @param b_or_n whether the operation can accept boolean or numeric arguments
         */
        public BooleanOperation
            (String s, boolean unary, boolean binary, boolean b_or_n)
        {
            super(s, unary, binary);
            
            booleanOrNumeric = b_or_n;
        }
        
        /**
         * Performs the operation on the given values and returns
         * the result.
         *
         * Replaces the method named operationPerformed.
         *
         * @param left  the left side operand for a binary operation
         *              or <CODE>null</CODE> for a unary operation
         * @param right the right side operand for a unary or binary
         *              operation
         * @since 2.2
         */
        public final Object performOperation(Object left, Object right) 
            throws ParseException
        {
            XBoolean p;
            XBoolean q;
            
            XBigInteger a;
            XBigInteger b;
            
            XDouble x;
            XDouble y;
            
            if (left == null)
            // handle unary case
            {
                checkUnary();
                
                
                if (ParserUtilities.isXBoolean(right)) {
                    p = (XBoolean)right;
                    
                    return unaryForXBoolean(p);
                }
                
                if (! booleanOrNumeric) {
                    throw new ParseException(
                        "Boolean operation "
                        + symbol
                        + " expects 1 boolean argument."
                        , 0);
                }
                
                if (ParserUtilities.isXIntegral(right)) {
                    a = ParserUtilities.toXBigInteger((XNumber)right);
                    
                    return unaryForXIntegral(a);
                }
                
                if (ParserUtilities.isXNumber(right)) {
                    x = ParserUtilities.toXDouble((XNumber)right);
                    
                    return unaryForXFloating(x);
                }
                
                throw new ParseException(
                    "Boolean operation "
                    + symbol
                    + " expects 1 boolean or 1 numeric argument."
                    , 0);
            }
            else
            // handle binary case
            {
                checkBinary();
            
                if (ParserUtilities.isXBoolean(left) &&
                    ParserUtilities.isXBoolean(right))
                {
                    p = (XBoolean)left;
                    q = (XBoolean)right;
                    
                    return binaryForXBoolean(p, q);
                }
                
                if (! booleanOrNumeric) {
                    throw new ParseException(
                        "Boolean operation "
                        + symbol
                        + " expects 2 boolean arguments."
                        , 0);
                }
                
                if (ParserUtilities.isXIntegral(left) &&
                    ParserUtilities.isXIntegral(right))
                {
                    a = ParserUtilities.toXBigInteger((XNumber)left);
                    b = ParserUtilities.toXBigInteger((XNumber)right);
                    
                    return binaryForXIntegral(a, b);
                }
                
                if (ParserUtilities.isXNumber(left) &&
                    ParserUtilities.isXNumber(right))
                {
                    x = ParserUtilities.toXDouble((XNumber)left);
                    y = ParserUtilities.toXDouble((XNumber)right);
                    
                    return binaryForXFloating(x, y);
                }
                
                throw new ParseException(
                    "Boolean operation "
                    + symbol
                    + " expects 2 boolean or 2 numeric arguments."
                    , 0);
            }
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a unary operation on a boolean argument.
         *
         * @param p the argument
         */
        public Object unaryForXBoolean(XBoolean p) {
            return null;
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a binary operation on boolean arguments.
         *
         * @param p argument 1
         * @param q argument 2
         */
        public Object binaryForXBoolean(XBoolean p, XBoolean q) {
            return null;
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a unary operation on an integral argument.
         *
         * @param a the argument
         */
        public Object unaryForXIntegral(XBigInteger a) {
            return null;
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a binary operation on integral arguments.
         *
         * @param a argument 1
         * @param b argument 2
         */
        public Object binaryForXIntegral(XBigInteger a, XBigInteger b) {
            return null;
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a unary operation on a floating argument.
         *
         * @param x the argument
         */
        public Object unaryForXFloating(XDouble x) {
            return null;
        }
        
        /**
         * Override this method to specify the result of this operation
         * acting as a binary operation on floating arguments.
         *
         * @param x argument 1
         * @param y argument 2
         */
        public Object binaryForXFloating(XDouble x, XDouble y) {
            return null;
        }
        
    }
    
    /////////////////
    // Debug tools //
    /////////////////
    
    /*
    
    // Prints the state of the <CODE>ObjectOperationPair</CODE>
    // in the console.
    private void showState(ObjectOperationPair ooPair) {
        console.err.println("data:           " + data);
        console.err.println("length:         " + data.length());
        console.err.println("next:           " + next);
        
        if (ooPair == null) {
            console.err.println("pair is null");
        }
        else {
            console.err.println("pair.value:     " + toString(ooPair.value));
            console.err.println("pair.operation: " + toSymbol(ooPair.operation));
        }
        
        console.err.println();
    }
    
    // Returns the value of the toString method on the given object
    // or <CODE>"null"</CODE> if the object is <CODE>null</CODE>.
    private String toString(Object s) {
        return (s != null) ? s.toString() : "null";
    }
    
    // Returns the symbol of the given operation 
    // or <CODE>"null"</CODE> if the operation is <CODE>null</CODE>.
    private String toSymbol(Operation operation) {
        return (operation != null) ? operation.symbol : "null";
    }
    
    */
}
