/* * @(#)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:
* *AbstractParser to
* BaseParser since the class is
* no longer an abstract class.parse method
* and other related algorithmic methods from the
* derived class JPTParser into this
* class. This is why this class is no longer an
* abstract class. Nevertheless this
* class has limited functionality since no constants,
* functions, or operations have been predefined. We
* have introduced various special forms: set, let, if,
* eval, and random. We treat random as a special form
* in order to allow 0, 1, or 2 arguments.
* parseWithArgumentList
* to enable the parser to set local variables for use
* during the parse operation.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 Form | *Typical Usage | *
|---|---|
set |
* set(identifier,expression) |
*
let |
* let(identifier,expression) |
*
if |
* if(test,expression-1,expression-2) |
*
eval |
* eval(expression-1,...,expression-n) |
*
random |
* random() or random(x)
* or random(x,y) |
*
The special forms are interpreted as follows.
* *set evaluates the expression and assigns it to
* the identifier in a persistant fashion that will be available
* across invocations of the parser.
* set returns the
* evaluated expression as its value.let evaluates the expression and assigns it to
* the identifier in a temporary fashion that will be available
* only in this invocation of the parser.
* let
* returns the evaluated expression as its value.if evaluates the test expression as a boolean.
* If the result is true,
* expression-1 is evaluated and returned
* and expression-2 is skipped.
* If the result is false,
* expression-2 is evaluated and returned
* and expression-1 is skipped.eval evaluates all expressions from left to right
* and returns the value of the last expression.random() returns a random double between 0 and 1.random(x) returns a random double between 0 and x. random(x,y) returns a random double between x and y.random is introduced as a special form since in
* our simple parsing scheme ordinary functions cannot be overloaded to
* take different numbers of arguments.
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 Name | *Typical Usage | *Interpretation | *
| random | *random() | *Random number between 0 and 1 | *
| random | *random(x) | *Random number between 0 and x | *
| random | *random(x,y) | *Random number between x and y | *
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:
* *initializeStructures()addReserved()addConstants()addFunctions()addOperations()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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
isPossibleIdentfierIf 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:
isPossibleIdentfierIf 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:
* *isPossibleIdentifier(id)
* is trueAssigns 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.
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.
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.
null and the other is not.null or
* fails to satisfy isPossibleVariableID.null.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.
Removes the function with the given name * from this parser.
* *Returns the removed AbstractFunction
* or null if no such function was
* found.
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.
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:
compare is not
* an existing operation for this parsercompare is at
* precedence 0 since it is illegal to
* insert an operation at precedence -1As 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.
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.
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.
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.
Returns whether or not the given name is
* associated with a function whose class
* does NOT extend 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:
* *Returns the function with the given name
* or null if no such function is
* defined.
Returns the operation with the given symbol
* or null if no such operation is
* defined.
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:
* *simpleFunctions()
* is called to obtain all simple functions.toString() method is called.
* This string is appended to a temporary
* StringBuffer object.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.
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.
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:
* *ASSIGNMENT_BY_SETASSIGNMENT_BY_LETIF_THEN_ELSEEVALRANDOMAdds 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.
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.
* *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.
Parse an expression that is introduced by * an identifier.
* *This includes in the following order:
* *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.
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.
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.
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.
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.
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.
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.
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.
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.
The "random" function is a special function * that takes 0, 1, or 2 numeric arguments and * has the following interpretation.
* *| Function Name | *Typical Usage | *Interpretation | *
| random | *random() | *Random number between 0 and 1 | *
| random | *random(x) | *Random number between 0 and x | *
| random | *random(x,y) | *Random number between x and y | *
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.
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:
* *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.
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.
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.
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.
Returns true
* if the given String exactly matches
* the next non-whitespace characters in the data,
* or false if it does not.
Returns true
* if the given String exactly matches
* the characters in the data starting at the given index,
* or false if it does not.
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.
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.
Return true if the character at start is
* '+' or '-'.
Return true if the character at start is
* 'E' or 'e'.
If the character at position start is
* '+' or '-'
* return (start + 1) otherwise return 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.
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 theObjectOperationPair
// 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";
}
*/