/*
 * @(#)AbstractParser.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 edu.neu.ccs.util.*;
import java.text.ParseException;
import java.util.*;
import java.math.*;

/**
 * <P>Abstract superclass for classes of objects
 * that provide functionality for evaluating 
 * <CODE>{@link java.lang.String String}</CODE>s 
 * into primitive types and objects
 * using a language with simple syntactic structure.</P>
 *
 * @author  Jeff Raab
 * @author  Richard Rasala
 * @version 2.2
 * @since   1.0
 */
public abstract class AbstractParser implements Parser {

    /** 
     * Singleton operation object designating 
     * that a symbol is a prefix for a known operation.
     */
    protected static final Operation OPERATION_PREFIX = new Operation();

    /** Value designating string data that has form of an integer number. */
    protected static final int INTEGRAL = 100;
    
    /** Value designating string data that has form of a floating number. */
    protected static final int FLOATING = 101;
    
    /** String token representing the start of a nested expression. */
    protected String NESTED_EXPRESSION_START = "(";

    /** String token representing the end of a nested expression. */
    protected String NESTED_EXPRESSION_END = ")";

    /** String token representing the start of an argument list. */
    protected String ARGUMENT_LIST_START = "(";

    /** String token representing the end of an argument list. */
    protected String ARGUMENT_LIST_END = ")";

    /** String token representing the radix point. */
    protected String RADIX_POINT = ".";

    /** String token representing the argument separator. */
    protected String ARGUMENT_SEPARATOR = ",";
    
    /** Char with UNDERSCORE character for identifiers. */
    protected char UNDERSCORE = '_';

    /** 
     * Table of variable identifiers 
     * and their corresponding values. 
     */
    protected Hashtable environment = new Hashtable();
    
    /** Table of constant variable identifiers. */
    protected Hashtable constants = new Hashtable();
    
    /** 
     * Table of procedure identifiers 
     * and their corresponding procedures. 
     */
    protected Hashtable procedures = new Hashtable();

    /** 
     * Table of operation symbols 
     * and their corresponding operations. 
     */    
    protected Hashtable operations = new Hashtable();
    
    /** Table of prefixes of operation symbols. */    
    protected Hashtable prefixes = new Hashtable();
    
    /** 
     * List of hashtables storing the precedence relationship 
     * between operations.  Operations with the same precedence
     * are placed in the same hashtable.  The list is set up
     * from lowest precedence to highest.
     */
    protected Vector precedence = new Vector();
    
    /** 
     * The identity operation, equivalent to the function 
     * <I>f</I>(<I>x</I>,<I>y</I>) = <I>y</I>, 
     * inserted with the lowest level of precedence and 
     * used as the base for evaluation of an expression.
     */
    protected final Operation identity = new Operation("") {
        public Object performOperation(Object left, Object right)
            throws ParseException 
        {
            return right;
        }
    };
    
    /**
     * Add the identity operation to the operations and precedence
     * structures by inline initialization here.
     */
    {
        precedence.add(new Hashtable());
        addOperation(identity, 0);
    }
    
    /** <CODE>String</CODE> to be evaluated by this parser */
    protected String data = null;
    
    /** 
     * Index of the next character 
     * in the data <CODE>String</CODE>
     * to be parsed by this parser. 
     */
    protected int next = 0;
    
    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * Constructs a new parser by adding
     * the standard operations, procedures, and constants
     * available for this parser.
     */
    public AbstractParser() {
        addOperations();
        addProcedures();
        addConstants();
    }

    ////////////
    // Parser //
    ////////////

    public abstract Object parse(String data) 
        throws ParseException;
    
    ////////////////
    // Public API //
    ////////////////

    /**
     * Adds the given constant 
     * to the parser environment 
     * with the given identifier and value.
     *
     * @param id the identifier for the constant
     * @param value the value for the constant
     */
    public void addConstant(String id, Object value) {
        if (constants.containsKey(id))
            throw new JPTError("Constant name " + id + " already in use");
            
        assign(id, value);
        constants.put(id, value);
    }

    /**
     * Adds the given procedure 
     * to the table of recognized procedures.
     *
     * @param proc the procedure to be added
     */
    public void addProcedure(Procedure proc) {
        if (procedures.containsKey(proc.name))
            throw new JPTError("Procedure name " + proc.name + "already in use");
            
        procedures.put(proc.name, proc);
    }
    
    /**
     * Adds the given operation 
     * to the table of recognized operations,
     * at the same level of precedence 
     * as the given existing operation.
     *
     * Replaces the method named addOperatorAtPrecedenceOf.
     *
     * @param compare an existing operation 
     *      at the desired level of precedence
     * @param op the operation to be added
     * @return the operation object 
     *      that was added to the operation table
     * @throws JPTError if the given operation
     *      is not an existing operation for this parser
     * @since 2.2
     */
    public Operation addOperationAtPrecedenceOf(
        Operation compare, 
        Operation op) 
    {
        // find precedence of comparable operation
        int pos = precedenceOf(compare);
        if (pos == -1) {
            throw new JPTError(
                "Operation at comparable precedence " +
                "not recognized.");
        }
        
        // add the operation at the proper precedence
        addOperation(op, pos);
        
        // return the added operation
        return op;
    }

    /**
     * Adds the given operation 
     * to the table of recognized operations,
     * at the level of precedence 
     * immediately before the precendence 
     * as the given existing operation.  
     *
     * Replaces the method named addOperatorBeforePrecedenceOf.
     *
     * @param compare an existing operation 
     *      at the desired level of precedence
     * @param op the operation to be added
     * @return the operation object 
     *      that was added to the operation table
     * @throws JPTError if the given operation
     *      is not an existing operation for this parser
     * @since 2.2
     */
    public Operation addOperationBeforePrecedenceOf(
        Operation compare, 
        Operation op) 
    {
        // find precedence of comparable operation
        int pos = precedenceOf(compare);
        if (pos == -1) {
            throw new JPTError(
                "Operation at comparable precedence " + 
                "not recognized.");
        }
        else if (pos == 0) {
            throw new JPTError(
                "Operation cannot be added at a precedence " + 
                "before the identity operation.");
        }
        
        // add the operation at the proper precedence
        precedence.insertElementAt(new Hashtable(), pos - 1);
        addOperation(op, pos);
        
        // return the added operation
        return op;
    }

    /**
     * Adds the given operation 
     * to the table of recognized operations,
     * at the level of precedence 
     * immediately after the precendence 
     * of the given existing operation.  
     *
     * Replaces the method named addOperatorAfterPrecedenceOf.
     *
     * @param compare an existing operation 
     *      at the desired level of precedence
     * @param op the operation to be added
     * @return the operation object 
     *      that was added to the operation table
     * @throws JPTError if the given operation
     *      is not an existing operation for this parser
     * @since 2.2
     */
    public Operation addOperationAfterPrecedenceOf(
        Operation compare, 
        Operation op) 
    {
        // find precedence of comparable operation
        int pos = precedenceOf(compare);
        if (pos == -1) {
            throw new JPTError(
                "Operation at comparable precedence " + 
                "not recognized.");
        }
        
        // add the operation at the proper precedence
        precedence.insertElementAt(new Hashtable(), ++pos);
        addOperation(op, pos);
        
        // return the added operation
        return op;
    }

    ///////////////////////
    // Protected methods //
    ///////////////////////

    /**
     * Parses the next expression 
     * in the data <CODE>String</CODE>.
     *
     * This method is to be overridden
     * by subclasses of this class.
     *
     * 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
     * @since 2.2
     */
    protected abstract ObjectOperationPair parseExpression
        (ObjectOperationPair standing) throws ParseException;
    
    /**
     * Adds the standard operations for this parser 
     * to the operation table.
     *
     * Replaces the method named addOperators.
     *
     * This method is to be overridden
     * by subclasses of this class, if the subclass
     * makes use of operations.
     *
     * @since 2.2
     */
    protected void addOperations() {}

    /**
     * Adds the standard procedures for this parser
     * to the procedure table.
     *
     * This method is to be overridden
     * by subclasses of this class, if the subclass
     * makes use of procedures.
     */
    protected void addProcedures() {}
    
    /**
     * Adds the standard constants for this parser
     * to the environment.
     *
     * This method is to be overridden
     * by subclasses of this class, if the subclass
     * has constants defined in its environment.
     */
    protected void addConstants() {}

    /**
     * Sets the token 
     * representing the start of an argument list
     * in this parsing scheme
     * to the given <CODE>String</CODE> token.
     *
     * @param token the desired <CODE>String</CODE> token
     */
    protected void setLeftParenthesisToken(String token) {
        if ((token != null) && (token.length() > 0))
            ARGUMENT_LIST_START = token;
    }

    /**
     * Sets the token 
     * representing the end of an argument list
     * in this parsing scheme
     * to the given <CODE>String</CODE> token.
     *
     * @param token a <CODE>String</CODE> token
     */
    protected void setRightParenthesisToken(String token) {
        if ((token != null) && (token.length() > 0))
            ARGUMENT_LIST_END = token;
    }

    /**
     * Sets the token 
     * representing the radix point
     * in this parsing scheme
     * to the given <CODE>String</CODE> token.
     *
     * @param token a <CODE>String</CODE> token
     */
    protected void setRadixPointToken(String token) {
        if ((token != null) && (token.length() > 0))
            RADIX_POINT = token;
    }

    /**
     * Sets the token 
     * representing the argument separator
     * in this parsing scheme
     * to the given <CODE>String</CODE> token.
     *
     * @param token a <CODE>String</CODE> token
     */
    protected void setArgumentSeparatorToken(String token) {
        if ((token != null) && (token.length() > 0))
            ARGUMENT_SEPARATOR = token;
    }

    /**
     * Assigns the given value 
     * to the given identifier in the environment, 
     * replacing the previous value in the environment
     * with the same identifier.
     *
     * @param id the identifier for the variable
     * @param value the value of the variable
     */
    protected void assign(String id, Object value) {
        if (environment.containsKey(id))
            environment.remove(id);
            
        environment.put(id, value);
    }
    
    /**
     * Adds the given operation to the operation table 
     * at the given index in the precedence list.
     *
     * Replaces the method named addOperator.
     *
     * @param op an operation to be added
     * @param pos the position in the precedence list
     * @throws JPTError if the symbol for the given operation
     *      is already an operation for this parser
     * @since 2.2
     */
    protected void addOperation(Operation op, int pos) {
        if (operations.containsKey(op.symbol))
            throw new JPTError("Operation symbol " + op.symbol + " already in use");
        
        // add the operation information to the appropriate tables
        operations.put(op.symbol, op);
        ((Hashtable)precedence.get(pos)).put(op.symbol, op);
        
        // add all prefixes for this operation
        int n = op.symbol.length() - 1;
        
        for (int i = 1; i <= n; i++) {
            String prefix = op.symbol.substring(0, n);
            
            if (!prefixes.containsKey(prefix))
                prefixes.put(prefix, prefix);
        }
    }
    
    /**
     * Performs a lookup in the operation and prefix tables 
     * to determine if the given <CODE>String</CODE> symbol 
     * represents an existing operation 
     * or is a prefix of the symbol for an existing operation.
     *
     * Replaces the method named isOperatorOrPrefix.
     *
     * @param  symbol the symbol to look up in the operation table
     * @return the operation object if it is represented by the given symbol, 
     *         or <CODE>OPERATION_PREFIX</CODE> if the symbol is a prefix, 
     *         or <CODE>null</CODE> if the symbol is neither an operation or
     *            a prefix
     * @since 2.2
     */
    protected Operation isOperationOrPrefix(String symbol) {

        // try to find the given symbol in the operations table
        if (operations.containsKey(symbol))
            return (Operation)operations.get(symbol);
        
        // try to find the given symbol in the prefixes table
        if (prefixes.containsKey(symbol))
            return OPERATION_PREFIX;
        
        return null;
    }
    
    /**
     * If the next token in the data represents an operation
     * then return the corresponding Operation
     * otherwise return <CODE>null</CODE>.
     *
     * @since 2.2
     */
    protected Operation nextOperation() {
        skipWhitespace();
            
        int end = next;
        Operation temp = null;
        Operation last = null;

        // look for operation
        while (end < data.length()) {
            end++;
                
            // test if the current substring is an operation or prefix
            // and break from the loop if it is not
            temp = isOperationOrPrefix(data.substring(next, end));
                
            if (temp == null)
                break;
            
            // if we have an operation save it
            // but also seek a longer operation string that extends it
            if (temp != OPERATION_PREFIX)
                last = temp;
        }
        
        if (last != null)
            next += last.symbol.length();
        
        return last;
    }
    
    /**
     * Returns the index in the precedence list 
     * corresponding to the precedence of the given operation,
     * or -1 if the given operation is <CODE>null</CODE> or
     * is not in the precedence list.
     *
     * Replaces method with the same name due to changes in
     * the parameter type name.
     *
     * @param op the operation whose precedence is needed
     * @since 2.2
     */
    protected int precedenceOf(Operation op) {
        if (op == null)
            return -1;
        
        for (int i = 0; i < precedence.size(); i++) {
            if (((Hashtable)precedence.get(i)).containsKey(op.symbol))
                return i;
        }
        
        return -1;
    }

    /**
     * Returns <CODE>true</CODE> 
     * if the given <CODE>String</CODE> exactly matches
     * the next non-whitespace characters in the data,
     * or <CODE>false</CODE> if it does not.
     */
    protected boolean nextTokenIs(String pattern) {
        skipWhitespace();
        
        return nextTokenIs(pattern, next);
    }

    /**
     * Returns <CODE>true</CODE> 
     * if the given <CODE>String</CODE> exactly matches
     * the characters in the data starting at the given index,
     * or <CODE>false</CODE> if it does not.
     */
    protected boolean nextTokenIs(String pattern, int start) {
        if ((pattern == null)
            || (pattern.length() == 0)
            || ((start + pattern.length()) > data.length()))
                return false;
    
        // pattern match by contradiction
        for (int i = 0; i < pattern.length(); i++)
            if (pattern.charAt(i) != data.charAt(start + i))
                return false;
        
        // return true if no contradiction found
        return true;    
    }
    
    /**
     * Returns true if the next non-whitespace character starts a number.
     *
     * @since 2.2
     */
    protected boolean startsNumber() {
        skipWhitespace();
        
        if (next >= data.length())
            return false;
        
        return Character.isDigit(data.charAt(next)) || nextTokenIs(RADIX_POINT);
    }
    
    /**
     * Returns true if the next non-whitespace character starts an identifier.
     * By convention, this means the character is a letter or an underscore.
     *
     * @since 2.2
     */
    protected boolean startsIdentifier() {
        skipWhitespace();
        
        if (next >= data.length())
            return false;
        
        char c = data.charAt(next);
        
        return Character.isLetter(c) || (c == UNDERSCORE);
    }
    
    /**
     * Returns true if the next character is within an identifier.
     * By convention, this means the character is a letter or a digit
     * or an underscore.
     *
     * @since 2.2
     */
    protected boolean withinIdentifier() {
        if (next >= data.length())
            return false;
        
        char c = data.charAt(next);
        
        return Character.isLetter(c) || Character.isDigit(c) || (c == UNDERSCORE);
    }
    
    /**
     * Parses the next identifier in the data <CODE>String</CODE>
     * and returns it.
     */
    protected String parseIdentifier() {
        // return if no identifier
        if (! startsIdentifier())
            return "";
        
        int start = next;
        next++;
        
        // skip over identifier
        while (withinIdentifier())
            next++;
        
        return data.substring(start, next);
    }
    
    /**
     * Parses the next argument list 
     * in the data <CODE>String</CODE>
     * and returns an array of objects 
     * representing the list of arguments.
     *
     * By default, an argument list
     * begins with an argument list start token,
     * followed by a possibly zero-length list of expressions
     * separated by argument separator tokens,
     * ending with an argument list end token.
     *
     * @throws ParseException if the argument list is malformed
     */
    protected Object[] parseArgumentList() throws ParseException {
        Vector values = new Vector();
        
        skipWhitespace();
        
        // eat initial argument list start token
        if (!nextTokenIs(ARGUMENT_LIST_START))
        {
            throw new ParseException(
                "Expected start of argument list", next);
        }
        
        next += ARGUMENT_LIST_START.length();
        
        skipWhitespace();
        
        // parse individual argument expressions
        // unless this list has zero arguments
        
        boolean expecting = (!nextTokenIs(ARGUMENT_LIST_END));
        
        while (expecting) {
            
            // parse the individual argument expression
            ObjectOperationPair base = new ObjectOperationPair(null, identity);
            values.add((parseExpression(base)).value);
            
            skipWhitespace();
            
            // eat argument separator token if present
            // otherwise prepare to exit loop
            if (nextTokenIs(ARGUMENT_SEPARATOR))
                next += ARGUMENT_SEPARATOR.length();
            else
                expecting = false;
            
            skipWhitespace();
        }
            
        // test for argument list end token
        if (!nextTokenIs(ARGUMENT_LIST_END))
        {
            throw new ParseException(
                "Expected end of argument list", next);
        }

        // eat argument list end token
        next += ARGUMENT_LIST_END.length();
        
        skipWhitespace();
        
        // return the array of parsed argument values
        return values.toArray();
    }

    /**
     * Parses the next numeric token 
     * in the data <CODE>String</CODE>
     * and returns an object representation
     * of its value.
     * 
     * By default, a number is either of the form
     * <CODE>s(d*)</CODE>, where
     * <CODE>s</CODE> is an optional sign in <CODE>{+-}</CODE>
     * and each <CODE>d</CODE> is in <CODE>{0 .. 9}</CODE>
     * or of the form
     * <CODE>s(d*).(d*)etx</CODE> where
     * <CODE>s</CODE> is an optional sign in <CODE>{+-}</CODE>,
     * each <CODE>d</CODE> is in <CODE>{0 .. 9}</CODE>,
     * <CODE>.</CODE> is the radix point token,
     * <CODE>E</CODE> is a character in <CODE>{Ee}</CODE>,
     * <CODE>t</CODE> is an optional sign in <CODE>{+-}</CODE>,
     * and <CODE>x</CODE> is one to three characters
     * in <CODE>{0 .. 9}</CODE>.
     *
     * Integral values are represented by
     * <CODE>XBigInteger</CODE> objects, and
     * floating point values are represented by
     * <CODE>XDouble</CODE> objects.
     * 
     * @throws ParseException if the number is malformed
     */
    protected Object parseNumber() throws ParseException {
        skipWhitespace();
        
        int end  = next;
        int type = INTEGRAL;
  
        // optional leading sign
        // if sign is '+' advance both next and end
        // if sign is '-' advance only end
        if (isSignAt(next)) {
            if (data.charAt(next) == '+')
                next++;
            
            end++;
        }
        
        // optional digits before radix point
        end = afterDigits(end);
        
        // optional radix symbol
        if (nextTokenIs(RADIX_POINT, end)) {
            // if radix present then type must be floating
            type = FLOATING;
            end += RADIX_POINT.length();
            
            // optional digits after radix
            end = afterDigits(end);
        }
        
        // optional exponent symbol
        if (isExponentAt(end)) {
            // if exponent present then type must be floating
            type = FLOATING;
            end++;
            
            // optional leading sign for exponent
            end = afterSign(end);
            
            // digits after exponent symbol
            end = afterDigits(end);
        }
        
        // now extract the possible number string
        String number = data.substring(next, end);
        next = end;
        
        // if number is a string that looks integral
        // attempt to construct an XBigInteger
        if (type == INTEGRAL) {
            try {
                BigInteger i = new BigInteger(number);
                return new XBigInteger(i);
            }
            catch (NumberFormatException ex) {
                return new ParseException(
                    "Expected valid numeric value: "
                     + number
                     + " "
                     + ex.getMessage()
                     , next);
            }
        }
        
        // if number is a string that is in floating
        // point attempt to construct an XDouble
        try {
            Double d = new Double(number);
            return new XDouble(d.doubleValue());
        }
        catch (NumberFormatException ex) {
            return new ParseException(
                "Expected valid numeric value: "
                 + number
                 + " "
                 + ex.getMessage()
                 , next);
        }
    }

    /**
     * If the character at position start is <CODE>'+'</CODE>
     * or <CODE>'-'</CODE> return (start + 1)
     * otherwise return start.
     */
    protected int afterSign(int start) {
        return isSignAt(start) ? (start + 1) : start;
    }
    
    /**
     * Return true if the character at start is <CODE>'+'</CODE>
     * or <CODE>'-'</CODE>.
     */
    protected boolean isSignAt(int start) {
        if (start >= data.length())
            return false;
        
        char c = data.charAt(start);
        
        return (c == '+') || (c == '-');
    }
    
    /**
     * Return true if the character at start is <CODE>'E'</CODE>
     * or <CODE>'e'</CODE>.
     */
    protected boolean isExponentAt(int start) {
        if (start >= data.length())
            return false;
        
        char c = data.charAt(start);
        
        return (c == 'E') || (c == 'e');
    }
    
    /**
     * Return the first character position at or after start
     * that is not a digit.
     */
    protected int afterDigits(int start) {
        while (start < data.length()) {
            char c = data.charAt(start);
            
            if (Character.isDigit(c))
                start++;
            else
                break;
        }
        
        return start;
    }
    
    /**
     * Skips over whitespace characters 
     * starting at the current parse position, 
     * until a non-whitespace character 
     * or the end of the data <CODE>String</CODE> is found.
     *
     * By default, whitespace is defined as
     * a character considered to be whitespace
     * by the Java <CODE>Character</CODE> class.
     * 
     * @see java.lang.Character
     */
    protected void skipWhitespace() {
        while ((next < data.length()) && 
               (Character.isWhitespace(data.charAt(next))))
                    next++;
    }
    
    ///////////////////
    // Inner Classes //
    ///////////////////
    
    /**
     * <P>Pair class used by a 
     * <CODE>{@link Parser Parser}</CODE> 
     * to store a value and an associated operation.</P>
     *
     * Replaces the class named ObjectOpPair.
     *
     * @author  Jeff Raab
     * @author  Richard Rasala
     * @version 2.2
     * @since 2.2
     */
    public static class ObjectOperationPair {
        
        /** The object component of the pair. */
        public Object value = null;
        
        /** The operation component of the pair. */
        public Operation operation = null;
        
        /** 
         * Constructs a pair 
         * with the given value and operation.
         *
         * @param v the value component
         * @param o the operation component
         */
        public ObjectOperationPair(Object v, Operation o) {
            value = v;
            operation = o;
        }
    }
    
    /**
     * <P>Class encapsulating an operation
     * and its corresponding <CODE>String</CODE> symbol 
     * for use with a parser.</P>
     *
     * Replaces the class named Operator.
     *
     * @author  Jeff Raab
     * @author  Richard Rasala
     * @version 2.2
     * @since   2.2
     * @see Parser
     */ 
    public static class Operation {
        
        /** The symbol representing this operation. */
        public String symbol = "\0";
        
        /** Whether or not the operation can act as a unary operation. */
        public boolean isUnary = true;
        
        /** Whether or not the operation can act as a binary operation. */
        public boolean isBinary = true;
        
        /** Constructs an operation with the default initial settings. */
        public Operation() {}
        
        /**
         * Constructs an operation with the given symbol.
         *
         * @param s the symbol for the operation
         */
        public Operation(String s) {
            if (symbol != null)
                symbol = 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 Operation(String s, boolean unary, boolean binary) {
            if (symbol != null)
                symbol = s;
            
            isUnary  = unary;
            isBinary = 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 Object performOperation(Object left, Object right) 
            throws ParseException 
        {
            return null;
        }
        
        /**
         * Returns true if the operation may act as a unary operation.
         *
         * @since 2.2
         */
        public boolean isUnary() {
            return isUnary;
        }
        
        /**
         * Returns true if the operation may act as a binary operation.
         *
         * @since 2.2
         */
        public boolean isBinary() {
            return isBinary;
        }
        
        ///////////////////////
        // Protected Methods //
        ///////////////////////
        
        /** Throws parseException if operation cannot act as unary. */
        protected void checkUnary()
            throws ParseException 
        {
            if (!isUnary) {
                throw new ParseException(
                    "Operation "
                    + symbol
                    + " expects 2 arguments."
                    , 0);
            }
        }
        
        /** Throws parseException if operation cannot act as binary. */
        protected void checkBinary()
            throws ParseException 
        {
            if (!isBinary) {
                throw new ParseException(
                    "Operation "
                    + symbol
                    + " expects 1 argument."
                    , 0);
            }
        }
        
    }
    
    /**
     * <P>Class encapsulating a procedure
     * and its corresponding <CODE>String</CODE> symbol
     * for use with a parser.</P>
     *
     * @author  Jeff Raab
     * @author  Richard Rasala
     * @version 2.2
     * @since   1.0
     * @see Parser
     */ 
    public abstract static class Procedure {
        
        /** The name representing this procedure. */
        public String name = "UnnamedProcedure";
        
        /** The number of arguments expected by this procedure. */
        public int arguments = 1;
        
        /**
         * Constructs a procedure 
         * with the given identifier 
         * and number of expected arguments.
         *
         * @param symbol the symbol for this procedure
         * @param arguments the expected number of arguments
         */
        public Procedure(String name, int arguments) {
            if ((name != null) && (name.length() > 0))
                this.name = name;
            
            this.arguments = arguments;
        }
        
        /**
         * Applies this procedure 
         * using the provided array of argument values 
         * and returns the result.
         *
         * Replaces the method named procedureCalled.
         *
         * @param args the array of argument values
         * @since 2.2
         */
        public abstract Object procedureCall(Object[] args)
            throws ParseException;
        
        /**
         * Throws a ParseExpection if either
         *   the args are null
         * or
         *   the length of args is not equal to arguments.
         * 
         * The functionality of the 2.1 version of this method
         * is now captured in <CODE>checkArgsAsNumeric</CODE>.
         *
         * @param args the array of argument values
         * @see #checkArgsAsNumeric(Object[])
         * @since 2.2
         */
         protected void checkArgs(Object[] args)
            throws ParseException
         {
            if (args == null)
                throw new ParseException(
                    "Null arguments to procedure " + name + "."
                    , 0);
            
            if (args.length != arguments)
                throw new ParseException(
                    "Procedure " + name + " expects " + arguments + " arguments."
                    , 0);
         }
         
        /**
         * Throws a ParseExpection if either
         *   the args are null
         * or
         *   the length of args is not equal to arguments
         * or
         *   the elements in args are not of type XNumber.
         *
         * @param args the array of argument values
         * @see #checkArgs(Object[])
         * @since 2.2
         */
         protected void checkArgsAsNumeric(Object[] args)
            throws ParseException
         {
            checkArgs(args);
            
            for (int i = 0; i < arguments; i++)
                if (! (args[i] instanceof XNumber))
                    throw new ParseException(
                        "Procedure "
                        + name
                        + " expects numeric argument in position "
                        + i
                        + "."
                        , 0);
         }
    }
}
