/* * @(#)BaseParser.java 2.5.0 18 May 2007 * * Copyright 2007 * 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.awt.geom.Point2D; import java.math.*; //debug // import edu.neu.ccs.console.*; /** *

The class BaseParser is the * base class for classes of objects that provide * functionality for evaluating strings into * primitive types and objects using a language * with a simple syntactic structure.

* *

Revisions in 2.5.0:

* * * *

Further comments:

* *

BaseParser contains all fundamental parsing algorithms * but has no installed abstract functions, operations, or constants.

* *

BaseParser has the ability to define special forms * and five such forms are introduced: * set, * let, * if, * eval, * and random. * Here is a brief synopsis of these special forms.

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Special FormTypical Usage
setset(identifier,expression)
letlet(identifier,expression)
ifif(test,expression-1,expression-2)
evaleval(expression-1,...,expression-n)
randomrandom() or random(x) * or random(x,y)
* *

The special forms are interpreted as follows.

* * * *

random is introduced as a special form since in * our simple parsing scheme ordinary functions cannot be overloaded to * take different numbers of arguments.

* * @author Richard Rasala * @author Jeff Raab * @version 2.5 */ public class BaseParser implements Parser // debug // ,ConsoleAware { /** *

The IDENTITY operation, equivalent to the function * f(x,y)=y.

* *

This operation is inserted with the lowest level of * precedence and is used as the base for evaluation of * an expression.

* *

This object is identical to Operation.IDENTITY.

*/ public static final Operation IDENTITY = Operation.IDENTITY; /** *

The singleton operation object which designates that * a symbol is a prefix for a known operation.

* *

This operation operates as a return flag and may not * be inserted into the precedence structure of this class.

* *

This object is identical to Operation.OPERATION_PREFIX.

*/ public static final Operation OPERATION_PREFIX = Operation.OPERATION_PREFIX; /** 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 = '_'; /** * The reserved keyword "set" to indicate variable assignment * that is persistent across invocations of the * parse method. */ protected final String ASSIGNMENT_BY_SET = "set"; /** * The reserved keyword "let" to indicate variable assignment * that is temporary, that is, one that affects only the * current invocation of the parse method. */ protected final String ASSIGNMENT_BY_LET = "let"; /** The reserved keyword "if" to indicate if-then-else. */ protected final String IF_THEN_ELSE = "if"; /** * The reserved keyword "eval" to indicate the special * function that evaluates its arguments from left to * right and then returns the value of the rightmost * argument. */ protected final String EVAL = "eval"; /** *

The reserved keyword "random" to indicate the special * function that takes 0, 1, or 2 numeric arguments and * has the following interpretation.

* * * * * * * * * * * * * * * * * * * * * * *
Function NameTypical UsageInterpretation
randomrandom()Random number between 0 and 1
randomrandom(x)Random number between 0 and x
randomrandom(x,y)Random number between x and y
*/ protected final String RANDOM = "random"; /** * Table of reserved identifiers * for special functions or operations. */ private Hashtable reserved = new Hashtable(); /** * Table of constant identifiers * and their corresponding values. */ private Hashtable constants = new Hashtable(); /** * Table of variable identifiers defined by "set" * and their corresponding values. */ private Hashtable set_variables = new Hashtable(); /** * Table of variable identifiers defined by "let" * in the current invocation of * the parse method * and their corresponding values. */ private Hashtable let_variables = new Hashtable(); /** * Table of function names * and their corresponding functions. */ private Hashtable functions = new Hashtable(); /** * Table of operation symbols * and their corresponding operations. */ private Hashtable operations = new Hashtable(); /** * Table of prefixes of operation symbols. */ private 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 ordered * from lowest precedence to highest. */ private Vector precedence = new Vector(); /** * The vector that operates as a stack to save and restore * the parser context in each call of parse. */ private Vector parserContextStack = new Vector(); /** * The current String to be evaluated * by the current call of parse. */ protected String data = ""; /** * The index of the next character to examine * in the current String to be evaluated * by the current call of parse. */ protected int next = 0; /** *

The member data to control whether the parser should be * evaluating operations, functions, and special functions * or should rather be parsing formally in order to extract * the next expression that is to be parsed as a string.

* *

The default value is 0.

* *

A value of 0 or negative signals that * the parser should proceed to evaluate.

* *

A positive value signals that * the parser should suspend evaluation and parse formally.

* *

Normally, methods that wish to suspend evaluation should * increment suspend on entry and decrement suspend prior to * exit. This protocol will make it easy for such methods to * be recursive and to interact well with similar methods.

*/ protected int suspend = 0; ///////////////// // Constructor // ///////////////// /** *

Constructs a new parser by initializing structures * and by adding the standard operations, functions, * and constants available for this parser.

* *

Calls the following 5 methods in turn:

* * * *

Normally the last 3 methods should be overridden * in a derived class.

*/ public BaseParser() { initializeStructures(); addReserved(); addConstants(); addFunctions(); addOperations(); } //////////// // Parser // //////////// /** *

Parses the given string d and * returns the Object it represents, * given this parsing scheme.

* *

This implementation proceeds as follows.

* *

Checks that the data is not null.

* *

To enable recursive calls of this method, pushes * the current parsing context on a stack.

* *

Calls the recursive method * parseExpression together with a default * ObjectOperationPair to initiate the * parsing operation on this data.

* *

Checks for errors:

* *

Requires the value returned by the call to * parseExpression to be * non-null.

* *

Requires that the parse operation consumed * all of the characters in the data.

* *

Restores the original parsing context.

* *

Returns the value returned by the call to * parseExpression.

* *

Implementation note: This method simply * calls parseWithArgumentList * with the last 2 parameters null.

* * @param d the String to be parsed * @throws ParseException if the data is malformed */ public final Object parse(String d) throws ParseException { return parseWithArgumentList(d, null, null); } /** *

Parses the given string d in the context of * a symbolic argument list and associated values * and returns the Object represented * by the string d given this parsing scheme.

* *

This implementation proceeds as follows.

* *

To enable recursive calls of this method, pushes * the current parsing context on a stack.

* *

Checks that the data is not null.

* *

Assigns the values to corresponding ids. Will * ignore the lists ids and values if both lists are * null.

* *

Calls the recursive method * parseExpression together with a default * ObjectOperationPair to initiate the * parsing operation on this data.

* *

Checks for errors.

* *

Requires the value returned by the call to * parseExpression to be * non-null.

* *

Requires that the parse operation consumed * all of the characters in the data.

* *

Restores the original parsing context.

* *

Returns the value returned by the call to * parseExpression.

* * @param d the string data to be parsed * @param ids the argument list ids * @param values the argument list values * @throws ParseException if the data is malformed */ public final Object parseWithArgumentList (String d, String[] ids, Object[] values) throws ParseException { // error message if exception is trapped try { // push the current context and // initialize a new context // to parse the string d pushContext(d); // sanity check for null data if (d == null) throw new ParseException("Data to parse was null", -1); // assign arguments assignArgumentList(ids, values); // evaluate the expression Object value = parseExpression(); // 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); // restore the previous context popContext(); return value; } catch (Throwable ex) { throwAgainAndPop(ex.getMessage(), d); return null; // never reached } } //////////////// // Public API // //////////////// /** *

Call the function with the given name * that is installed in this parser * using the given double data as arguments * and return a double value.

* *

The function should in effect * expect doubles as arguments * and should return a number.

* *

The length of the arguments array must * equal the number of arguments expected by * the function.

* *

Throws ParseException if * an error occurs.

* * @param name the name of the function * installed in this parser * @param arguments the arguments given * as an array of double * @throws ParseException */ public final double call(String name, double[] arguments) throws ParseException { String message = null; if (name == null) { message = "Null name passed to call"; throw new ParseException(message, 0); } AbstractFunction f = getFunction(name); if (f == null) { message = "Function " + name + " is not installed in this parser"; throw new ParseException(message, 0); } return call(f, arguments); } /** *

Call the function with the given name * that is installed in this parser * using the given double x as the argument * and return a double value.

* *

The function should in effect * expect 1 double as an argument * and should return a number.

* *

Throws ParseException if * an error occurs.

* * @param name the name of the function * of 1 argument * installed in this parser * @param x argument 1 * @throws ParseException */ public final double call(String name, double x) throws ParseException { return call(name, new double[] {x} ); } /** *

Call the function with the given name * that is installed in this parser * using the given doubles x,y as arguments * and return a double value.

* *

The function should in effect * expect 2 doubles as an arguments * and should return a number.

* *

Throws ParseException if * an error occurs.

* * @param name the name of the function * of 2 arguments * installed in this parser * @param x argument 1 * @param y argument 2 * @throws ParseException */ public final double call(String name, double x, double y) throws ParseException { return call(name, new double[] {x, y} ); } /** *

Call the function with the given name * that is installed in this parser * using the given doubles x,y,z as arguments * and return a double value.

* *

The function should in effect * expect 3 doubles as an arguments * and should return a number.

* *

Throws ParseException if * an error occurs.

* * @param name the name of the function * of 3 arguments * installed in this parser * @param x argument 1 * @param y argument 2 * @param z argument 3 * @throws ParseException */ public final double call(String name, double x, double y, double z) throws ParseException { return call(name, new double[] {x, y, z} ); } /** *

Call the function with the given name * that is installed in this parser * using the given doubles x,y,z,w as arguments * and return a double value.

* *

The function should in effect * expect 4 doubles as an arguments * and should return a number.

* *

Throws ParseException if * an error occurs.

* * @param name the name of the function * of 4 arguments * installed in this parser * @param x argument 1 * @param y argument 2 * @param z argument 3 * @param w argument 4 * @throws ParseException */ public final double call (String name, double x, double y, double z, double w) throws ParseException { return call(name, new double[] {x, y, z, w} ); } /** *

Views the given String as a comma separated * list of names of AbstractFunction * objects installed in this parser; * for each such function object f, * makes an XPoint2D array of pairs (x,f(x)) * where x is sampled on the given interval * divided into the given number of divisions; * all such arrays are collected and returned in * a 2-dimensional array.

* *

If names is null, * returns null.

* *

Otherwise, assume that the string names * contains N function names for some N>=0.

* *

If divisions is less than 1, it will be set * to 1.

* *

The 2 dimensions of the output array * will be N by (divisions+1).

* *

Throws ParseException if * an error occurs.

* *

See also the makeTable methods in * DataTables2D.

* * @param names a comma separated list of names * of functions installed in this parser * @param endpointA an endpoint of the interval * @param endpointB an endpoint of the interval * @param divisions the number of subdivisions of the interval * @return the 2-dimensional array of (x,f(x)) pairs * @throws ParseException */ public final XPoint2D[][] makeTable (String names, double endpointA, double endpointB, int divisions) throws ParseException { if (names == null) return null; String message = null; String[] list = Strings.splitCommaList(names); int N = list.length; XPoint2D[][] result = new XPoint2D[N][]; if (divisions < 1) divisions = 1; for (int i = 0; i < N; i++) { AbstractFunction f = getFunction(list[i]); if (f == null) { message = "Function " + list[i] + " is not installed in this parser"; throw new ParseException(message, 0); } result[i] = makeTable(f, endpointA, endpointB, divisions); } return result; } /** *

Views the given String as a comma separated * list of names of AbstractFunction * objects installed in this parser; * for each such function object f, * makes an XPoint2D array of pairs (x,f(x)) * where x is sampled on the given interval * divided into the given number of divisions; * all such arrays are collected and returned in * a 2-dimensional array.

* *

If names is null * or limits is null, * returns null.

* *

Otherwise, assume that the string names * contains N function names for some N>=0.

* *

If divisions is less than 1, it will be set * to 1.

* *

The 2 dimensions of the output array * will be N by (divisions+1).

* *

Throws ParseException if * an error occurs.

* *

See also the makeTable methods in * DataTables2D.

* * @param names a comma separated list of names * of functions installed in this parser * @param limits the interval * @param divisions the number of subdivisions of the interval * @return the 2-dimensional array of (x,f(x)) pairs * @throws ParseException */ public final XPoint2D[][] makeTable (String names, XInterval limits, int divisions) throws ParseException { if (limits == null) return null; double endpointA = limits.getMinimum(); double endpointB = limits.getMaximum(); return makeTable(names, endpointA, endpointB, divisions); } /** *

Call the given abstract function * using the given double data as arguments * and return a double value.

* *

The function should in effect * expect doubles as arguments * and should return a number.

* *

The length of the arguments array must * equal the number of arguments expected by * the function.

* *

Throws ParseException if * an error occurs.

* * @param f the abstract function * @param arguments the arguments given * as an array of double * @throws ParseException */ public static double call (AbstractFunction f, double[] arguments) throws ParseException { String message = null; if (f == null) { message = "Null AbstractFunction passed to call"; throw new ParseException(message, 0); } if (arguments == null) { message = "Null arguments passed to call"; throw new ParseException(message, 0); } String name = f.name(); int args = f.arguments(); int size = arguments.length; if (args != size) { message = "Function " + name + " requires " + args + " arguments" + " but provided " + size + " arguments"; throw new ParseException(message, 0); } XDouble[] data = new XDouble[size]; for (int i = 0; i < size; i++) data[i] = new XDouble(arguments[i]); Object result = f.functionCall(data); if (! (result instanceof XNumber)) { message = "Function " + name + " failed to return a numeric value"; throw new ParseException(message, 0); } XNumber v = (XNumber) result; return v.doubleValue(); } /** *

Call the given abstract function * using the given double x as the argument * and return a double value.

* *

The function should in effect * expect 1 double as an argument * and should return a number.

* *

Throws ParseException if * an error occurs.

* * @param f the abstract function * of 1 argument * @param x argument 1 * @throws ParseException */ public static double call (AbstractFunction f, double x) throws ParseException { return call(f, new double[] {x} ); } /** *

Call the given abstract function * using the given doubles x,y as arguments * and return a double value.

* *

The function should in effect * expect 2 doubles as an arguments * and should return a number.

* *

Throws ParseException if * an error occurs.

* * @param f the abstract function * of 2 arguments * @param x argument 1 * @param y argument 2 * @throws ParseException */ public static double call (AbstractFunction f, double x, double y) throws ParseException { return call(f, new double[] {x, y} ); } /** *

Call the given abstract function * using the given doubles x,y,z as arguments * and return a double value.

* *

The function should in effect * expect 3 doubles as an arguments * and should return a number.

* *

Throws ParseException if * an error occurs.

* * @param f the abstract function * of 3 arguments * @param x argument 1 * @param y argument 2 * @param z argument 3 * @throws ParseException */ public static double call (AbstractFunction f, double x, double y, double z) throws ParseException { return call(f, new double[] {x, y, z} ); } /** *

Call the given abstract function * using the given doubles x,y,z,w as arguments * and return a double value.

* *

The function should in effect * expect 4 doubles as an arguments * and should return a number.

* *

Throws ParseException if * an error occurs.

* * @param f the abstract function * of 4 arguments * @param x argument 1 * @param y argument 2 * @param z argument 3 * @param w argument 4 * @throws ParseException */ public static double call (AbstractFunction f, double x, double y, double z, double w) throws ParseException { return call(f, new double[] {x, y, z, w} ); } /** *

Make an XPoint2D array of pairs (x,f(x)) * where x is sampled on the given interval * divided into the given number of divisions.

* *

If the given function is null, * returns null.

* *

If divisions is less than 1, it will be set to 1. The size * of the output array will be (divisions+1).

* *

See also the makeTable methods in * DataTables2D.

* * @param f the function of one argument to evaluate * @param endpointA an endpoint of the interval * @param endpointB an endpoint of the interval * @param divisions the number of subdivisions of the interval * @return the array of (x,f(x)) pairs * @throws ParseException */ public static XPoint2D[] makeTable (AbstractFunction f, double endpointA, double endpointB, int divisions) throws ParseException { String message; if (f == null) { return null; } String name = f.name(); int args = f.arguments(); if (args != 1) { message = "Method makeTable requires " + "a function of one argument but was passed " + name + " which needs " + args + " arguments"; throw new ParseException(message, 0); } if (divisions < 1) divisions = 1; int d = divisions; XPoint2D[] result = new XPoint2D[d + 1]; result[0] = new XPoint2D(endpointA, call(f, endpointA)); result[d] = new XPoint2D(endpointB, call(f, endpointB)); if (d == 1) return result; double delta = (endpointB - endpointA) / d; for (int i = 1; i < d; i++) { double x = endpointA + i * delta; result[i] = new XPoint2D(x, call(f, x)); } return result; } /** *

Make an XPoint2D array of pairs (x,f(x)) * where x is sampled on the given interval * divided into the given number of divisions.

* *

If the given function is null * or limits is null, * returns null.

* *

If divisions is less than 1, it will be set to 1. The size * of the output array will be (divisions+1).

* *

See also the makeTable methods in * DataTables2D.

* * @param f the function of one argument to evaluate * @param limits the interval * @param divisions the number of subdivisions of the interval * @return the array of (x,f(x)) pairs * @throws ParseException */ public static XPoint2D[] makeTable (AbstractFunction f, XInterval limits, int divisions) throws ParseException { if (limits == null) return null; double endpointA = limits.getMinimum(); double endpointB = limits.getMaximum(); return makeTable(f, endpointA, endpointB, divisions); } /** *

Add this identifier to the table of * reserved identifiers so it may not be * used for a constant or variable id or * a function name.

* *

A reserved identifier is intended to * be the identifier for a special function * that may have an indefinite number of * arguments and may not fully evaluate the * arguments.

* *

Reserving an identifier does not define * the special function or implement the test * that makes that behavior accessible to the * parser. For examples of how to do this, * see the source for this class and examine * how the special functions set, let, if, * eval, and random have been defined.

* *

Throws IllegalArgumentException * in the following cases:

* * * *

If a reserved id is introduced after the * definition of the id as a variable id, then * the variable will become inaccessible.

* *

It is recommended that reserved id's be * set before any other id's are introduced.

* * @param id the id to set as reserved */ public final void reserveID(String id) { String message = null; if (! isPossibleIdentifier(id)) { message = id + " is not a valid identifier" + " for defining a reserved ID"; } else if (isConstantID(id)) { message = id + " may not define a reserved ID" + " since it already defines a constant ID"; } else if (isFunctionName(id)) { message = id + " may not define a reserved ID" + " since it already defines a function name"; } if (message != null) throw new IllegalArgumentException(message); if (!reserved.containsKey(id)) reserved.put(id,id); } /** *

Adds the given constant to the table * of constants using the given identifier * and value.

* *

Does nothing if either argument is * null.

* *

A constant identifier once installed * may not be removed or redefined.

* *

Throws IllegalArgumentException * in the following cases:

* * * *

If a constant id is introduced after the * definition of the id as a variable id, then * the variable will become inaccessible.

* *

It is recommended that constant id's be * set before variable id's are introduced.

* * @param id the identifier for the constant * @param value the value for the constant * @throws IllegalArgumentException */ public final void addConstant(String id, Object value) { if ((id == null) || (value == null)) return; String message = null; if (! isPossibleIdentifier(id)) { message = id + " is not a valid identifier" + " for defining a constant"; } else if (isReservedID(id)) { message = id + " may not define a constant ID" + " since it already defines a reserved ID"; } else if (isConstantID(id)) { message = id + " may not define a constant ID" + " since it already defines a constant ID"; } else if (isFunctionName(id)) { message = id + " may not define a constant ID" + " since it already defines a function name"; } if (message != null) throw new IllegalArgumentException(message); constants.put(id, value); } /** *

Returns true if the given string is a possible * variable id in the current parser context.

* *

Specifically, returns true if:

* * * * @param id the string to test as a variable id */ public final boolean isPossibleVariableID(String id) { if (id == null) return false; if (! isPossibleIdentifier(id)) return false; if (isReservedID(id)) return false; if (isConstantID(id)) return false; if (isFunctionName(id)) return false; return true; } /** *

Assigns the given value * to the given identifier * as a persistant "set" variable for this parser, * replacing any previous "set" variable * associated with the same identifier.

* *

Does nothing if id is null.

* *

Assigns the variable if value is * non-null * and removes the variable otherwise.

* *

Returns value.

* *

Throws IllegalArgumentException if * isPossibleVariableID(id) is false.

* * @param id the identifier for the variable * @param value the value of the variable * @throws IllegalArgumentException */ public final Object assignSetVariable (String id, Object value) { return assignVariable(id, value, set_variables); } /** *

Assigns the given value * to the given identifier * as a temporary "let" variable * in the context of the current call * to the parse method of this parser, * replacing any previous "let" variable * associated with the same identifier in this same * context.

* *

If there is no invocation of a parse method * that is pending, this method call will be useless.

* *

Does nothing if id is null.

* *

Assigns the variable if value is * non-null * and removes the variable otherwise.

* *

Returns value.

* *

Throws IllegalArgumentException if * isPossibleVariableID(id) is false.

* * @param id the identifier for the variable * @param value the value of the variable * @throws IllegalArgumentException */ public final Object assignLetVariable (String id, Object value) { return assignVariable(id, value, let_variables); } /** *

This method takes id-value pairs from corresponding * positions in the lists ids and values and performs the * assignment using assignLetVariable.

* *

If there is no invocation of a parse method * that is pending, this method call will be useless.

* *

Does nothing if both lists are null.

* *

Otherwise throws IllegalArgumentException * in any of the following circumstances.

* * * * @param ids the list of identifiers to assign * @param values the list of values to assign to the identifiers * @throws IllegalArgumentException */ public void assignArgumentList (String[] ids, Object[] values) { if ((ids == null) && (values == null)) return; String message = null; if (ids == null) { message = "Null id list passed to " + "assignArgumentList"; } else if (values == null) { message = "Null value list passed to " + "assignArgumentList"; } else if (ids.length != values.length) { message = "In assignArgumentList " + "the id list and the value list " + "do not have the same length"; } else { int length = ids.length; for (int i = 0; i < length; i++) { if (ids[i] == null) { message = "In assignArgumentList " + "the id in position " + i + " is null"; break; } if (values[i] == null) { message = "In assignArgumentList " + "the value in position " + i + " is null"; break; } if (! isPossibleVariableID(ids[i])) { message = "In assignArgumentList " + "the id in position " + i + ", namely, " + ids[i] + ", is not a possible variable id"; break; } } } if (message != null) throw new IllegalArgumentException(message); int length = ids.length; for (int i = 0; i < length; i++) assignLetVariable(ids[i], values[i]); } /** *

Adds the given function * to the table of recognized functions.

* *

Does nothing if the function is * null.

* *

As of 2.5.0, permits the replacement of * an installed function by adding a new * function whose name is the same as the * older installed function. This allows * experimentation with implementation.

* *

Throws IllegalArgumentException * in the following case:

* * * *

If the function name is the same as the * id of a previously defined variable, then * the variable will become inaccessible.

* *

A function may be removed based on its name * using removeFunction.

* * @param function the function to be added * @throws IllegalArgumentException */ public final void addFunction(AbstractFunction function) { if (function == null) return; String name = function.name(); String message = null; if (isReservedID(name)) { message = name + " may not define a function name" + " since it already defines a reserved ID"; } else if (isConstantID(name)) { message = name + " may not define a function name" + " since it already defines a constant ID"; } if (message != null) throw new IllegalArgumentException(message); functions.put(name, function); } /** *

Removes the function with the given name * from this parser.

* *

Returns the removed AbstractFunction * or null if no such function was * found.

* * @param name the function name */ public final AbstractFunction removeFunction(String name) { if (name == null) return null; return (AbstractFunction) functions.remove(name); } /** *

Adds the given operation op * to the table of recognized operations, * at the same level of precedence as the * existing operation compare * installed in this parser.

* *

The operation IDENTITY is * initially installed in the parser at * precedence level 0 and may be used as the * base for adding additional operators.

* *

Throws IllegalArgumentException * if the operation compare is not * an existing operation for this parser.

* *

As of 2.5.0, permits the replacement of * an installed operation by an operation * with the same symbol.

* *

This method will work correctly if the * compare operation has the same * symbol as the operation op. * The precedence of the compare * operation will be determined and then the * compare operation will be * replaced by the operation op.

* * @param compare an existing operation * at the desired level of precedence * @param op the operation to be added * @return the operation object op * that was added to the operation table * @throws IllegalArgumentException */ public final Operation addOperationAtPrecedenceOf( Operation compare, Operation op) { // find the precedence of comparable operation int pos = precedenceOf(compare); if (pos == -1) { String message = "addOperation... error: " + "compare operator not installed"; throw new IllegalArgumentException(message); } // add the operation at the proper precedence addOperation(op, pos); // return the added operation return op; } /** *

Adds the given operation op * to the table of recognized operations, * at a level of precedence * immediately before the precendence of the * existing operation compare * installed in this parser.

* *

A new precedence level is inserted * between that of the compare * operation and all lower precedence * operations.

* *

The operation IDENTITY is * initially installed in the parser at * precedence level 0 and may be used as the * base for adding additional operators. * However, you may NOT install an operation * before the IDENTITY.

* *

Throws IllegalArgumentException * if:

* * * *

As of 2.5.0, permits the replacement of * an installed operation by an operation * with the same symbol.

* *

This method will "work" if the * compare operation has the same * symbol as the operation op but * it may not do what is desired. The problem * is that it is likely that you want to keep * the current precedence level and not insert * a new level in this case. Therefore, it is * recommended that you use the method * addOperationAtPrecedenceOf to * perform the replacement of an operator.

* * @param compare an existing operation * at the desired level of precedence * @param op the operation to be added * @return the operation object op * that was added to the operation table * @throws IllegalArgumentException */ public final Operation addOperationBeforePrecedenceOf( Operation compare, Operation op) { // find precedence of comparable operation int pos = precedenceOf(compare); if (pos == -1) { String message = "addOperation... error: " + "compare operator not installed"; throw new IllegalArgumentException(message); } if (pos == 0) { String message = "addOperation... error: " + "may not install before precedence 0"; throw new IllegalArgumentException(message); } // add new precedence level before level pos precedence.insertElementAt(new Hashtable(), pos); // add the operation at level pos which will // be the level just constructed above addOperation(op, pos); // return the added operation return op; } /** *

Adds the given operation op * to the table of recognized operations, * at a level of precedence * immediately after the precendence of the * existing operation compare * installed in this parser.

* *

A new precedence level is inserted * between that of the compare * operation and all higher precedence * operations.

* *

The operation IDENTITY is * initially installed in the parser at * precedence level 0 and may be used as the * base for adding additional operators.

* *

Throws IllegalArgumentException * if the operation compare is not * an existing operation for this parser.

* *

As of 2.5.0, permits the replacement of * an installed operation by an operation * with the same symbol.

* *

This method will "work" if the * compare operation has the same * symbol as the operation op but * it may not do what is desired. The problem * is that it is likely that you want to keep * the current precedence level and not insert * a new level in this case. Therefore, it is * recommended that you use the method * addOperationAtPrecedenceOf to * perform the replacement of an operator.

* * @param compare an existing operation * at the desired level of precedence * @param op the operation to be added * @return the operation object op * that was added to the operation table * @throws IllegalArgumentException */ public final Operation addOperationAfterPrecedenceOf( Operation compare, Operation op) { // find precedence of comparable operation int pos = precedenceOf(compare); if (pos == -1) { String message = "addOperation... error: " + "compare operator not installed"; throw new IllegalArgumentException(message); } // add new precedence level after level pos precedence.insertElementAt(new Hashtable(), pos + 1); // add the operation at level (pos + 1) which // will be the level just constructed above addOperation(op, pos + 1); // return the added operation return op; } /** *

Returns whether or not the given id is * associated with a constant.

* * @param id the constant id */ public final boolean isReservedID(String id) { if (id == null) return false; return reserved.containsKey(id); } /** *

Returns whether or not the given id is * associated with a constant.

* * @param id the constant id */ public final boolean isConstantID(String id) { if (id == null) return false; return constants.containsKey(id); } /** *

Returns whether or not the given id is * associated with a "set" variable.

* * @param id the variable id */ public final boolean isSetVariableID(String id) { if (id == null) return false; return set_variables.containsKey(id); } /** *

Returns whether or not the given id is * associated with a "let" variable * in the current invocation of the * parse method.

* * @param id the variable id */ public final boolean isLetVariableID(String id) { if (id == null) return false; return let_variables.containsKey(id); } /** *

Returns whether or not the given id is * associated with a constant or variable.

* * @param id the constant or variable id */ public final boolean isEnvironmentID(String id) { if (id == null) return false; return isConstantID(id) || isSetVariableID(id) || isLetVariableID(id); } /** *

Returns whether or not the given name is * associated with a function.

* * @param name the function name */ public final boolean isFunctionName(String name) { if (name == null) return false; return functions.containsKey(name); } /** *

Returns whether or not the given name is * associated with a function whose class * is or extends SimpleFunction.

* * @param name the function name */ public final boolean isSimpleFunctionName(String name) { AbstractFunction function = getFunction(name); if (function == null) return false; return (function instanceof SimpleFunction); } /** *

Returns whether or not the given name is * associated with a function whose class * does NOT extend SimpleFunction.

* * @param name the function name */ public final boolean isOrdinaryFunctionName(String name) { AbstractFunction function = getFunction(name); if (function == null) return false; return ! (function instanceof SimpleFunction); } /** *

Returns whether or not the given symbol is * associated with an operation.

* * @param symbol the operation symbol */ public final boolean isOperationSymbol(String symbol) { if (symbol == null) return false; return operations.containsKey(symbol); } /** *

Returns the value associated with a * constant or variable with the given id * or null if no such value * is defined.

* *

Looks for values in the following * order:

* * * * @param id the constant or variable id */ public final Object getValue(String id) { if (id == null) return null; if (isConstantID(id)) return constants.get(id); else if (isLetVariableID(id)) return let_variables.get(id); else if (isSetVariableID(id)) return set_variables.get(id); else return null; } /** *

Returns the function with the given name * or null if no such function is * defined.

* * @param name the function name */ public final AbstractFunction getFunction(String name) { if (isFunctionName(name)) return (AbstractFunction) functions.get(name); else return null; } /** *

Returns the operation with the given symbol * or null if no such operation is * defined.

* * @param symbol the operation symbol */ public final Operation getOperation(String symbol) { if (isOperationSymbol(symbol)) return (Operation) operations.get(symbol); else return null; } /** *

Returns an array of the reserved id's.

* *

The id's are sorted alphabetically.

*/ public final String[] reserved() { Enumeration e = reserved.keys(); Vector list = new Vector(); while (e.hasMoreElements()) list.add(e.nextElement()); int size = list.size(); String[] result = (String[]) list.toArray(new String[size]); Arrays.sort(result); return result; } /** *

Returns an array of the constant id's.

* *

The id's are sorted alphabetically.

*/ public final String[] constants() { Enumeration e = constants.keys(); Vector list = new Vector(); while (e.hasMoreElements()) list.add(e.nextElement()); int size = list.size(); String[] result = (String[]) list.toArray(new String[size]); Arrays.sort(result); return result; } /** *

Returns an array of the set variable id's.

* *

The id's are sorted alphabetically.

*/ public final String[] set_variables() { Enumeration e = set_variables.keys(); Vector list = new Vector(); while (e.hasMoreElements()) list.add(e.nextElement()); int size = list.size(); String[] result = (String[]) list.toArray(new String[size]); Arrays.sort(result); return result; } /** *

Returns an array of all function names.

* *

The names are sorted alphabetically.

*/ public final String[] functionNames() { Enumeration e = functions.keys(); Vector list = new Vector(); while (e.hasMoreElements()) list.add(e.nextElement()); int size = list.size(); String[] result = (String[]) list.toArray(new String[size]); Arrays.sort(result); return result; } /** *

Returns an array of the function names * that correspond to functions whose class * is or extends SimpleFunction.

* *

The names are sorted alphabetically.

*/ public final String[] simpleFunctionNames() { Enumeration e = functions.keys(); Vector list = new Vector(); while (e.hasMoreElements()) { String name = (String)e.nextElement(); if (isSimpleFunctionName(name)) list.add(name); } int size = list.size(); String[] result = (String[]) list.toArray(new String[size]); Arrays.sort(result); return result; } /** *

Returns an array of the function names * that correspond to functions whose class * does NOT extend SimpleFunction.

* *

The names are sorted alphabetically.

*/ public final String[] ordinaryFunctionNames() { Enumeration e = functions.keys(); Vector list = new Vector(); while (e.hasMoreElements()) { String name = (String)e.nextElement(); if (isOrdinaryFunctionName(name)) list.add(name); } int size = list.size(); String[] result = (String[]) list.toArray(new String[size]); Arrays.sort(result); return result; } /** *

Returns an array of all functions.

* *

The functions are sorted by name.

*/ public final AbstractFunction[] functions() { String[] names = functionNames(); int length = names.length; AbstractFunction[] fcns = new AbstractFunction[length]; for (int i = 0; i < length; i++) { fcns[i] = (AbstractFunction) functions.get(names[i]); } return fcns; } /** *

Returns an array of the simple functions, * that is, functions whose class is or extends * SimpleFunction.

* *

The functions are sorted by name.

*/ public final SimpleFunction[] simpleFunctions() { String[] names = simpleFunctionNames(); int length = names.length; SimpleFunction[] fcns = new SimpleFunction[length]; for (int i = 0; i < length; i++) { fcns[i] = (SimpleFunction) functions.get(names[i]); } return fcns; } /** *

Returns an array of the ordinary functions, * that is, functions whose class does NOT * extend SimpleFunction.

* *

The functions are sorted by name.

*/ public final AbstractFunction[] ordinaryFunctions() { String[] names = ordinaryFunctionNames(); int length = names.length; AbstractFunction[] fcns = new AbstractFunction[length]; for (int i = 0; i < length; i++) { fcns[i] = (AbstractFunction) functions.get(names[i]); } return fcns; } /** *

Returns an array of the operation symbols.

*/ public final String[] operationSymbols() { Enumeration e = operations.keys(); Vector list = new Vector(); while (e.hasMoreElements()) list.add(e.nextElement()); int size = list.size(); return (String[]) list.toArray(new String[size]); } /** *

Returns an array of the operations.

*/ public final Operation[] operations() { String[] symbols = operationSymbols(); int length = symbols.length; Operation[] ops = new Operation[length]; for (int i = 0; i < length; i++) { ops[i] = (Operation) operations.get(symbols[i]); } return ops; } /** *

Returns a String representation * of all Simple Function objects * currently installed in this parser.

* *

This representation is constructed as follows:

* * * *

As a consequence, the string returned by this * method will contain 3*N lines where N is the * number of simple functions installed and the * 3 lines for each function will consist of its * function name, the comma separated list of its * function parameters, and its function body.

* *

If a function has 0 parameters, the line for * its parameters will contain a blank to act as a * placeholder for parsing purposes.

*/ public final String simpleFunctionsToString() { StringBuffer buffer = new StringBuffer(); SimpleFunction[] fcns = simpleFunctions(); int length = fcns.length; for (int i = 0; i < length; i++) buffer.append(fcns[i].toString()); return buffer.toString(); } /** *

Treats the given String * as a representation for a list of * SimpleFunction objects, * builds those objects, and installs them * in this parser. Existing simple functions * with the same name as one in the list will * be replaced.

* *

Expectations:

* * * *

Throws IllegalArgumentException * if an error is detected. Those definitions * that have been successful before the error was * detected will stand.

* * @param string the string representation of a list * of simple functions * @throws IllegalArgumentException */ public void stringToSimpleFunctions(String string) { String message = null; String me = "stringToSimpleFunctions"; if (string == null) { message = "Null string passed to " + me; throw new IllegalArgumentException(message); } String[] lines = Strings.tokenize(string, "\n", true); int length = lines.length; if ((length % 3) != 0) { message = "The string passed to " + me + "\nfails to consist of 3*N lines for some N" + "\nbut rather has " + length + " lines"; throw new IllegalArgumentException(message); } int N = length / 3; int i = 0; int k = 0; try { while (i < N) { k = 3 * i; String name = lines[k]; String parameters = lines[k+1]; String body = lines[k+2]; new SimpleFunction(this, name, parameters, body); i++; } } catch (IllegalArgumentException iae) { message = "In " + me + "\nerror in function definition " + i + "\nat lines " + k + " to " + (k+2) + "\n" + iae.getMessage(); throw new IllegalArgumentException(message); } } /** *

Returns true if the given id is a possible * identifier for a constant, variable, function * or reserved function.

* *

More precisely:

* * * *

This method is static and does not consider * the relation of this id to any identifiers * installed in any particular parser.

* * @param id the id to test */ public static final boolean isPossibleIdentifier(String id) { if ((id == null) || (id.length() == 0)) return false; int length = id.length(); char c; c = id.charAt(0); if (! (Character.isLetter(c) || (c == '_'))) return false; for (int i = 1; i < length; i++) { c = id.charAt(i); if (! (Character.isLetter(c) || Character.isDigit(c) || (c == '_'))) return false; } return true; } /** *

Returns true if the given string is a possible symbol for * an operation.

* *

More precisely, the following punctuation characters will * be considered valid for an operation symbol:

* *
    + - * / % = < > ! ? : @ # $ ^ & ' " ` ~ | \
* *

A string that consists of one or more of these characters * will be considered valid.

* *

In addition, to support the built-in Operation * objects IDENTITY and OPERATION_PREFIX, * the strings "" and "\0" will be considered valid via an * ad hoc check.

* * @param string the string to test */ public static final boolean isPossibleOperation(String string) { if (string == null) return false; if (string.equals("")) return true; if (string.equals("\0")) return true; int length = string.length(); char c; for (int i = 0; i < length; i++) { c = string.charAt(i); switch (c) { case '+': case '-': case '*': case '/': case '%': case '=': case '<': case '>': case '!': case '?': case ':': case '@': case '#': case '$': case '^': case '&': case '\'': case '"': case '`': case '~': case '|': case '\\': break; default: return false; } } return true; } /////////////////////// // Protected methods // /////////////////////// /** *

Installs the reserved identifiers for * the special functions.

* *

This method may be overridden * by a derived class if the derived class * defines additional special functions. * In that case, this method should be * called first by the override method in * order that the reserved identifiers * defined by this parser are installed.

* *

This method reserves:

* * */ protected void addReserved() { reserveID(ASSIGNMENT_BY_SET); reserveID(ASSIGNMENT_BY_LET); reserveID(IF_THEN_ELSE); reserveID(EVAL); reserveID(RANDOM); } /** *

Adds the standard constants for this parser * to the environment.

* *

This method must be overridden * by a derived class if the derived class * has constants defined in its environment.

* *

This method does nothing at present.

*/ protected void addConstants() {} /** *

Adds the standard functions for this parser * to the function table.

* *

This method must be overridden * by a derived class if the derived class * makes use of functions.

* *

This method does nothing at present.

*/ protected void addFunctions() {} /** *

Adds the standard operations for this parser * to the operation table.

* *

This method must be overridden * by a derived class if the derived class * makes use of operations.

* *

This method does nothing at present.

*/ protected void addOperations() {} /** *

Returns the next term in the data string * with all leading unary operations already applied * or throws a ParseException * if no term is present.

* * @throws ParseException */ protected Object nextTerm() throws ParseException { Operation[] operations = nextUnaryOperations(); int length = operations.length; Object term = nextSimpleTerm(); // apply unary operations from right to left if (evaluate()) { for (int i = (length - 1); i >= 0; i--) term = operations[i].performOperation(null, term); } return term; } /** *

Returns the next simple term in the * data string * or throws a ParseException * if no term is present.

* *

A simple term is a term that is not preceded * by unary operations that will need to be applied * later. The work of collecting and applying unary * operations is done in the method * nextTerm().

* *

This method orchestrates the dispatch to the * methods that handle important special cases.

* * * * @throws ParseException */ protected Object nextSimpleTerm() throws ParseException { skipWhitespace(); Object term = null; if (nextTokenIs(NESTED_EXPRESSION_START)) term = parseNestedExpression(); else if (startsNumber()) term = parseNumber(); else if (startsIdentifier()) { term = parseIdentifierExpression(); } else { throw new ParseException("Expected term", next); } skipWhitespace(); return term; } /** *

Returns the array of unary operations that occur * after the current position in the data string and * throws a ParseException if an operation is * encountered that is binary but not unary.

* *

The array returned may be empty but will not be * null.

* * @throws ParseException */ 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 a pending unary operation", next); } return (Operation[]) operations.toArray(new Operation[0]); } /** *

Parse an expression that is introduced by * an identifier.

* *

This includes in the following order:

* * * * @throws ParseException */ protected Object parseIdentifierExpression() throws ParseException { String identifier = nextIdentifier(); Object term = null; if (isReservedID(identifier)) term = parseSpecialFunction(identifier); else if (isFunctionName(identifier)) term = parseFunctionCall(identifier); else if (isEnvironmentID(identifier)) term = evaluateIdentifier(identifier); else { String message = "Unrecognized identifier: " + identifier; throw new ParseException(message, next); } return term; } /** *

Parse a special function whose parameter * list may not be completely evaluated. The * id's of such special functions should be * installed as reserved id's.

* *

Throws ParseException if:

* * * *

To add additional special functions this * method must be suitably overridden in a * derived class. To facilitate an override, * this method returns null if * the identifier is a reserved id but there * is no code in this method to handle that id. * A method that overrides this method should * first call this method and should intervene * only if this method returns null; * otherwise the override method should simply * return what this method has returned to it.

*

* *

As a consequnce of this design, it is * entirely valid for this method to return * null. This is what permits a * sequence of derived classes to add reserved * identifiers and the parsing code for them. * Of course, each derived class should follow * the same design and provide parsing code * for each new reserved word that it adds but * return null if it encounters a * reserved word that it does not know how to * parse.

* *

Note that if you build a derived class * based on JPTParser then take * care not to introduce a conflict with the * function names and constant id's defined * in that class.

* * @throws ParseException */ protected Object parseSpecialFunction(String identifier) throws ParseException { // test if the identifier represents a special function if (! isReservedID(identifier)) throw new ParseException ("Unrecognized special function " + identifier, next); if (identifier.equals(ASSIGNMENT_BY_SET)) { return parseAssignment(set_variables); } else if (identifier.equals(ASSIGNMENT_BY_LET)) { return parseAssignment(let_variables); } else if (identifier.equals(IF_THEN_ELSE)) { return parseIfThenElse(); } else if (identifier.equals(EVAL)) { return parseEval(); } else if (identifier.equals(RANDOM)) { return parseRandom(); } return null; } /** *

Save the current parser context and * initialize a new context to parse * the string d.

* *

This method sets the new string to * parse to d, sets the start position * to zero, and creates a new context * for "let" variables.

* *

Use popContext() to * restore the previous context.

* * @param d the string to parse */ protected final void pushContext(String d) { parserContextStack.add (new ParserContext(data, next, let_variables)); data = (d == null) ? "" : d; next = 0; let_variables = new Hashtable(); } /** *

Save the current parser context and * prepare to parse the string d using * the current "let" variable context.

* *

This method sets the new string to * parse to d and sets the start position * to zero but does not change the context * for the "let" variables.

* *

This method permits the parsing of * d to be carried out in the context of * its caller rather than in a new "let" * variable context of its own.

* *

This method is provided to support * unusual situations and should be used * with great care.

* *

Use popContext() to * restore the previous context.

* * @param d the string to parse */ protected final void specialPush(String d) { parserContextStack.add (new ParserContext(data, next, let_variables)); data = (d == null) ? "" : d; next = 0; } /** Pop and restore the previous parser context. */ protected final void popContext() { int size = parserContextStack.size(); ParserContext context = (ParserContext) parserContextStack.remove(size - 1); data = context.data(); next = context.next(); let_variables = context.let_variables(); } /** *

Throws a ParseException with information * about the string d being parsed and the parser position * appended to the given exception message.

* *

Nothing is appended if d is null.

* *

Restores the previous parser context just before the * exception is thrown. This will enable the parser * methods to recursively accumulate error information and * report all information in a manner similar to a stack * trace.

* * @param message the current exception message * @param d the parsed string at this level of recursion */ protected final void throwAgainAndPop(String message, String d) throws ParseException { if (message == null) message = ""; if (d != null) message += "\nat position " + next + "\n" + d + "\n" + Strings.prefixRepeatChar("^", '.', next); ParseException pe = new ParseException(message, next); // restore the previous context popContext(); throw pe; } /** *

Recursive method that parses the next expression * in the data string.

* *

The given ObjectOperationPair object * should represent a binary operation together with * its left operand as the value component.

* *

It is permitted for the left operand to be * null if it is unused by the operation. * This is in fact the case with the default pair whose * operation is Operation.IDENTITY.

* *

The given binary operation must NOT be * null.

* *

In this implementation, the following additional * constraints are true.

* *

As the parse proceeds, all valid unary operators * will be collected and applied to a subsequent term.

* *

As the parse proceeds, all binary operations at * the same or higher precedence will be processed and * the results will be accumulated in the value slot of * the return pair.

* *

The ObjectOperationPair pair * returned will have the following characteristics.

* *

Normally, the value component of pair will * be non-null and will represent the final * result of applying the various binary operations, * functions, and special functions encountered * along the way. However, if this method is called by * the method extractExpression then the * parser will suspend evaluation and so the return * value within pair may be null.

* *

The operation component of pair will either * be null or be an operation of lower * precedence than the incoming operation.

* *

In particular, if the input pair has the default * operation, Operation.IDENTITY, then the * operation in the return pair must be null.

* * @param pending the ObjectOperationPair * object whose value is the left operand * and whose operation is the operation * immediately preceding the expression * to be parsed * @return the ObjectOperationPair object * with the value of the parsed expression and * the lower precedence operation that * immediately follows it or * the null operation * @throws ParseException if the expression is malformed * or if the incoming operation is null * @since 2.2 */ protected final ObjectOperationPair parseExpression (ObjectOperationPair pending) throws ParseException { // debug // showState(pending); Object left = pending.value(); Object term = null; Operation oldOp = pending.operation(); Operation newOp = null; int oldPr = precedenceOf(oldOp); int newPr = -1; if (oldOp == null) throw new ParseException ("Expected a pending 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 a pending binary operation", next); // inner loop that uses recursion // to handle any higher levels of precedence while (newPr > oldPr) { ObjectOperationPair ooPair = parseExpression(new ObjectOperationPair(term, newOp)); term = ooPair.value(); newOp = ooPair.operation(); newPr = precedenceOf(newOp); } // handle this level of precedence if (evaluate()) term = oldOp.performOperation(left, term); if (newPr == oldPr) { // continue the outer loop left = term; oldOp = newOp; } else { // exit when the next operation // has a lower level of precedence return new ObjectOperationPair(term, newOp); } } } /** *

Returns the result of parsing the next expression * in the data string.

* *

Shorthand for:

* *
    parseExpression(new ObjectOperationPair()).value()
* * @throws parseException */ protected final Object parseExpression() throws ParseException { return parseExpression(new ObjectOperationPair()).value(); } /** *

Extracts the next pending expression as a string * without performing any operations or evaluations. * The string will be stripped of leading and trailing * white space.

* *

The parser will be positioned at the end of the * expression parsed.

* *

Throws ParseException if parsing * detects errors even when evaluation is turned off. * In other words, the expression must be valid in a * formal sense.

* *

This method is supplied as a helper method for * the definition of special functions that may not * evaluate all of their arguments.

* *

This method adheres to the protocol specified in * the comments on the suspend member data, * that is, it increments suspend on entry * and decrements suspend on exit.

* * @throws ParseException */ protected final String extractExpression() throws ParseException { suspend++; int start = next; parseExpression(); int finish = next; suspend--; return data.substring(start,finish).trim(); } /** *

Parse an expression in parentheses, that is, a nested * expression, and return its value.

* *

Returns null if the next token in the data * is NOT the start of a nested expression.

* * @throws ParseException */ protected final Object parseNestedExpression() throws ParseException { Object term = null; if (nextTokenIs(NESTED_EXPRESSION_START)) { // eat left parenthesis next += NESTED_EXPRESSION_START.length(); // parse contained expression term = parseExpression(); skipWhitespace(); if ((next == data.length()) || (! nextTokenIs(NESTED_EXPRESSION_END))) throw new ParseException ("Expected end of nested expression", next); // eat right parenthesis next += NESTED_EXPRESSION_END.length(); skipWhitespace(); } return term; } /** *

Evaluate a constant or variable identifier * and return its value.

* *

If this method is called when the member method * evaluate() returns false * then this method will return null.

* * @throws ParseException */ protected final Object evaluateIdentifier(String identifier) throws ParseException { if (!evaluate()) return null; // test that this variable is in the environment if (!isEnvironmentID(identifier)) { String message = "Unrecognized constant or variable " + identifier; throw new ParseException(message, next); } // obtain variable from environment return getValue(identifier); } /** *

Parse assignment, that is, "set" or "let".

* *

The syntax for "set" is:

* *
    set(identifier,expression)
* *

The syntax for "let" is:

* *
    let(identifier,expression)
* *

A variable defined by "set" will be installed in this * parser in a persistant way so it may be used over the * course of many invocations of the parse * method.

* *

A variable defined by "let" will be installed in this * parser for use in the current invocation of the * parse method but will be unavailable in any * other invocation including any recursive invocations. * Thus, a "let" variable is effectively a local variable.

* *

The method parseSpecialFunction calls this * method and selects the appropriate hash table in which to * store the binding information.

* *

The identifier must be a valid identifier and must not * overlap with a constant or reserved id or with a * function name. In that case, the expression is * evaluated, assigned to the identifier in the given binding, * and also returned as the value of this method.

* *

This method assumes that the "set" or "let keyword has * already been extracted and that it is the () expression * that must be parsed and evaluated.

* * @param binding the binding used to store the id-value expression * @throws ParseException */ protected final Object parseAssignment(Hashtable binding) throws ParseException { Object[] identifierExpression = parseIdentifierExpressionList(); String identifier = (String) identifierExpression[0]; Object expression = identifierExpression[1]; try { if (evaluate()) assignVariable(identifier, expression, binding); } catch (IllegalArgumentException ex) { throw new ParseException(ex.getMessage(), next); } return expression; } /** *

Assigns the given value * to the given identifier * in the given binding environment, * replacing any previous value in the environment * associated with the same identifier.

* *

Does nothing if id is null * or binding is null.

* *

Assigns the variable if value is * non-null * and removes the variable otherwise.

* *

Returns value.

* *

Throws IllegalArgumentException if * isPossibleVariableID(id) is false.

* * @param id the identifier for the variable * @param value the value of the variable * @param binding the hash table to store the id-value binding * @throws IllegalArgumentException */ protected final Object assignVariable (String id, Object value, Hashtable binding) { if ((id == null) || (binding == null)) return null; String message = null; if (! isPossibleIdentifier(id)) { message = id + " is not a valid identifier" + " for defining a variable";; } if (isReservedID(id)) { message = id + " may not define a variable ID" + " since it already defines a reserved ID"; } if (isConstantID(id)) { message = id + " may not define a variable ID" + " since it already defines a constant ID"; } if (isFunctionName(id)) { message = id + " may not define a variable ID" + " since it already defines a function name"; } if (message != null) throw new IllegalArgumentException(message); if (value == null) binding.remove(id); else binding.put(id, value); return value; } /** *

Parse a function with arguments and return its value.

* * @throws ParseException */ protected final Object parseFunctionCall(String identifier) throws ParseException { // test if the identifier represents a function if (! isFunctionName(identifier)) throw new ParseException ("Unrecognized function " + identifier, next); // get the function AbstractFunction function = getFunction(identifier); Object term = null; // get the argument values and evaluate the function try { Object[] values = parseArgumentList(); if (evaluate()) term = function.functionCall(values); } catch (ParseException exception) { throw new ParseException(exception.getMessage(), next); } return term; } /** *

Parse if-then-else, that is, "if".

* *

The syntax is:

* *
    if(boolean,expression1,expression2)
* *

If evaluation is on, this special function will first * evaluate the boolean expression to obtain a true-false * value. Evaluates either expression1 or expression2 but * not both and returns the result. Returns expression1 if * the boolean is true otherwise returns expression2.

* *

If evaluation is off, then all three parts of this * form will be extracted but not evaluated.

* *

This method assumes that the "if" keyword has already * been extracted and that it is the () expression that must * be parsed and evaluated.

* * @throws ParseException */ protected final Object parseIfThenElse() throws ParseException { Object result = null; // eat initial argument list start token if (!nextTokenIs(ARGUMENT_LIST_START)) { String message = "Expected start of argument list"; throw new ParseException(message, next); } next += ARGUMENT_LIST_START.length(); skipWhitespace(); if (evaluate()) { Object test = parseExpression(); boolean first = true; if (test instanceof XBoolean) { XBoolean bool = (XBoolean) test; first = bool.getValue(); } else { throw new ParseException ("Expected boolean test in if", next); } // eat argument separator token if (!nextTokenIs(ARGUMENT_SEPARATOR)) { String message = "Expected argument separator in list"; throw new ParseException(message, next); } next += ARGUMENT_SEPARATOR.length(); skipWhitespace(); if (first) { result = parseExpression(); } else { extractExpression(); } // eat argument separator token if (!nextTokenIs(ARGUMENT_SEPARATOR)) { String message = "Expected argument separator in list"; throw new ParseException(message, next); } next += ARGUMENT_SEPARATOR.length(); skipWhitespace(); if (first) { extractExpression(); } else { result = parseExpression(); } } else { extractExpression(); // eat argument separator token if (!nextTokenIs(ARGUMENT_SEPARATOR)) { String message = "Expected argument separator in list"; throw new ParseException(message, next); } next += ARGUMENT_SEPARATOR.length(); skipWhitespace(); extractExpression(); // eat argument separator token if (!nextTokenIs(ARGUMENT_SEPARATOR)) { String message = "Expected argument separator in list"; throw new ParseException(message, next); } next += ARGUMENT_SEPARATOR.length(); skipWhitespace(); extractExpression(); } // test for argument list end token if (!nextTokenIs(ARGUMENT_LIST_END)) { String message = "Expected end of argument list"; throw new ParseException(message, next); } // eat argument list end token next += ARGUMENT_LIST_END.length(); skipWhitespace(); return result; } /** *

Parse the "eval" operation.

* *

The syntax is:

* *
    eval(expression-1,...,expression-n)
* *

If evaluation is on, this special function will * evaluate each expression from left to right and * then will return the value of the rightmost * expression, that is, expression-n.

* *

If evaluation is off, then the expressions will * be extracted but not evaluated and the return * value will be null.

* *

This method assumes that the "eval" keyword has already * been extracted and that it is the () expression that must * be parsed and evaluated.

* *

An important application of "eval" is to permit * the definition of "let" variables which are then * used in the final expression. A simple example of * this is as follows.

* *
    eval(let(a,3),let(b,5),let(c,2),b*b-4*a*c)
* *

In this example, a, b, c are temporary variables * used in the final expression b*b-4*a*c.

* * @throws ParseException */ protected final Object parseEval() throws ParseException { Object[] values = parseArgumentList(); int length = values.length; if (length == 0) return null; else return values[length - 1]; } /** *

The "random" function is a special function * that takes 0, 1, or 2 numeric arguments and * has the following interpretation.

* * * * * * * * * * * * * * * * * * * * * * *
Function NameTypical UsageInterpretation
randomrandom()Random number between 0 and 1
randomrandom(x)Random number between 0 and x
randomrandom(x,y)Random number between x and y
* * @throws ParseException */ protected final Object parseRandom() throws ParseException { String message = null; Object[] values = parseArgumentList(); if (values == null) { message = "Null arguments to random"; throw new ParseException(message, 0); } int length = values.length; if (length > 2) { message = "random requires 0, 1, or 2 arguments"; throw new ParseException(message, 0); } for (int i = 0; i < length; i++) { if (! (values[i] instanceof XNumber)) { message = "random expects numeric argument in position " + i; throw new ParseException(message, 0); } } XDouble x = null; double vx = 0; XDouble y = null; double vy = 0; double r = Math.random(); switch(length) { case 0: return new XDouble(r); case 1: x = ParserUtilities.toXDouble((XNumber) values[0]); vx = x.getValue(); return new XDouble(r * vx); case 2: x = ParserUtilities.toXDouble((XNumber) values[0]); vx = x.getValue(); y = ParserUtilities.toXDouble((XNumber) values[1]); vy = y.getValue(); return new XDouble(vx + r * (vy - vx)); default: return null; } } /** *

Controls whether or not the parser evaluates or * simply parses formally without evaluation.

* *

Returns true if the internal variable named * suspend is zero or negative.

*/ protected final boolean evaluate() { return suspend <= 0; } /** *

Parses the next argument list in the data String * assuming that each argument is to be evaluated and * returns an array of objects representing the list.

* *

By default, an argument list:

* * * * @throws ParseException if the argument list is * malformed */ protected final Object[] parseArgumentList() throws ParseException { Vector values = new Vector(); // eat initial argument list start token if (!nextTokenIs(ARGUMENT_LIST_START)) { String message = "Expected start of argument list"; throw new ParseException(message, 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 values.add(parseExpression()); 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)) { String message = "Expected end of argument list"; throw new ParseException(message, 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 argument list that consists of * an identifier that is to be returned literally * and a value expression to be evaluated.

* *

If the argument list ends are parentheses * and the argument list separator is a comma * then the argument list should look like:

* *
    (identifier,expression)
* *

The method returns a 2-element array with * the identifier and the evaluated expression.

* *

Throws ParseException if the * next position in the data does not contain a * list with the required properties.

* * @throws ParseException */ protected final Object[] parseIdentifierExpressionList() throws ParseException { // eat initial argument list start token if (!nextTokenIs(ARGUMENT_LIST_START)) { String message = "Expected start of argument list"; throw new ParseException(message, next); } next += ARGUMENT_LIST_START.length(); skipWhitespace(); // obtain identifier Object identifier = nextIdentifier(); if (identifier.equals("")) { String message = "Expected identifier in list"; throw new ParseException(message, next); } // eat argument separator token if (!nextTokenIs(ARGUMENT_SEPARATOR)) { String message = "Expected argument separator in list"; throw new ParseException(message, next); } next += ARGUMENT_SEPARATOR.length(); skipWhitespace(); // obtain expression Object expression = parseExpression(); // eat argument list end token if (!nextTokenIs(ARGUMENT_LIST_END)) { String message = "Expected end of argument list"; throw new ParseException(message, next); } next += ARGUMENT_LIST_END.length(); skipWhitespace(); return new Object [] { identifier, expression }; } /** * Initialize the operation and precedence structures * prior to any additions defined in derived classes * by adding the IDENTITY operation at precedence 0. */ protected final void initializeStructures() { Hashtable hashtable = new Hashtable(); precedence.add(hashtable); operations.put("", IDENTITY); hashtable.put("", IDENTITY); } /** *

Adds the given operation to the operation table * at the given index in the precedence list.

* *

If an operation with the same symbol is already * in the operation table, that operation is removed * from both the operation table and the precedence * list before the insertion. This is a change in * policy introduced in 2.5.0 to permit users to make * changes to a given parser more easily.

* *

Does nothing if the given operation is * null.

* *

Does nothing if pos is negative or * is beyond the last valid index in the precedence * list.

* *

Does nothing if the symbol for this operation * is "" or "\0" to prevent user defined operators * from overriding IDENTITY or OPERATION_PREFIX.

* *

Note that IDENTITY is installed directly in * the constructor and that OPERATION_PREFIX is a * flag object that is never installed.

* * @param op an operation to be added * @param pos the position in the precedence list */ protected final void addOperation(Operation op, int pos) { if (op == null) return; if ((pos < 0) || (pos >= precedence.size())) return; String symbol = op.symbol(); if ((symbol.equals("")) || (symbol.equals("\0"))) return; // if an operation with the same symbol is installed // remove it from the precedence list here and below // from the operation table Operation oldop = (Operation) operations.get(symbol); if (oldop != null) { for (int i = 0; i < precedence.size(); i++) { Hashtable hashtable = (Hashtable)precedence.get(i); if (hashtable.containsKey(symbol)) hashtable.remove(symbol); } } // add the operation to the operation table // this will remove oldop if present operations.put(symbol, op); // add the operation to the precedence list Hashtable hashtable = (Hashtable)precedence.get(pos); hashtable.put(symbol, op); // add all prefixes for this operation int n = symbol.length() - 1; for (int i = 1; i <= n; i++) { String prefix = symbol.substring(0, i); if (!prefixes.containsKey(prefix)) prefixes.put(prefix, prefix); } // debug // console.out.println("Symbol: " + symbol + " @ " + pos); } /** *

Returns the index in the precedence list * corresponding to the precedence of the given operation, * or -1 if the given operation is null or * is not in the precedence list.

* * @param op the operation whose precedence is needed */ protected final int precedenceOf(Operation op) { if (op == null) return -1; for (int i = 0; i < precedence.size(); i++) { Hashtable hashtable = (Hashtable)precedence.get(i); if (hashtable.containsKey(op.symbol())) return i; } return -1; } /** *

Performs a lookup in the operation and prefix tables * to determine if the given String symbol * represents an existing operation * or is a prefix of the symbol for an existing operation.

* *

The return value OPERATION_PREFIX acts as * a flag to signal that the symbol is a prefix but is not an * operation.

* * @param symbol the symbol to look up in the operation table * @return the Operation object * if it is represented by the given symbol, * or the flag OPERATION_PREFIX * if the symbol is a prefix, * or null if the symbol is * neither an operation nor a prefix */ protected final 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 null.

*/ protected final 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(); skipWhitespace(); return last; } /** *

Returns true * if the given String exactly matches * the next non-whitespace characters in the data, * or false if it does not.

*/ protected final boolean nextTokenIs(String pattern) { skipWhitespace(); return nextTokenIs(pattern, next); } /** *

Returns true * if the given String exactly matches * the characters in the data starting at the given index, * or false if it does not.

*/ protected final 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; } /** *

Gets the next identifier in the data * String and returns it.

* *

Returns "" if the next non-whitespace * entity in the data String does not begin * with the start of an identifier.

*/ protected final String nextIdentifier() { // return empty string if no identifier if (! startsIdentifier()) return ""; int start = next; next++; // skip over identifier while (withinIdentifier()) next++; return data.substring(start, next); } /** *

Returns true if the next non-whitespace character * starts an identifier.

* *

By convention, this means the character is * a letter or an underscore.

*/ protected final 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.

*/ protected final 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 numeric token * in the data String * and returns an object representation * of its value.

* *

By default, a number is either of the form * s(d*), where * s is an optional sign in {+-} * and each d is in {0 .. 9} * or of the form * s(d*).(d*)etx where * s is an optional sign in {+-}, * each d is in {0 .. 9}, * . is the radix point token, * E is a character in {Ee}, * t is an optional sign in {+-}, * and x is one to three characters * in {0 .. 9}.

* *

Integral values are represented by * XBigInteger objects and * floating point values are represented * by XDouble objects.

* * @throws ParseException if the number is malformed */ protected final 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); } } /** *

Returns true if the next non-whitespace * character starts a number.

* *

Although parseNumber can deal * with leading signs, this method tests whether * the next non-whitespace character is a digit * or the RADIX_POINT. Therefore, * if this method is called, it is assumed that * any leading signs have been collected prior * to reaching this point in the data and will * be handled once the rest of the number is * parsed.

*/ protected final boolean startsNumber() { skipWhitespace(); if (next >= data.length()) return false; return Character.isDigit(data.charAt(next)) || nextTokenIs(RADIX_POINT); } /** *

Return true if the character at start is * '+' or '-'.

*/ protected final 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 * 'E' or 'e'.

*/ protected final boolean isExponentAt(int start) { if (start >= data.length()) return false; char c = data.charAt(start); return (c == 'E') || (c == 'e'); } /** *

If the character at position start is * '+' or '-' * return (start + 1) otherwise return start.

*/ protected final int afterSign(int start) { return isSignAt(start) ? (start + 1) : start; } /** *

Return the first character position * at or after start that is not a digit.

*/ protected final 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 String * is found.

* *

By default, whitespace is defined as * a character considered to be whitespace * by the Java Character class.

*/ protected final void skipWhitespace() { while ((next < data.length()) && (Character.isWhitespace(data.charAt(next)))) next++; } /** *

Sets the token * representing the start of an argument list * in this parsing scheme * to the given String token.

* * @param token the desired String token */ protected final 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 String token.

* * @param token a String token */ protected final 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 String token.

* * @param token a String token */ protected final 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 String token.

* * @param token a String token */ protected final void setArgumentSeparatorToken(String token) { if ((token != null) && (token.length() > 0)) ARGUMENT_SEPARATOR = token; } ///////////////// // Debug tools // ///////////////// /* // Prints the state of the ObjectOperationPair // 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 "null" if the object is null. private String toString(Object s) { return (s != null) ? s.toString() : "null"; } // Returns the symbol() of the given operation // or "null" if the operation is null. private String toSymbol(Operation operation) { return (operation != null) ? operation.symbol() : "null"; } */