/*
* Calculator.java
*
* Brazil project web application toolkit,
* export version: 2.3
* Copyright (c) 2001-2009 Sun Microsystems, Inc.
*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is included as the file "license.terms",
* and also available at http://www.sun.com/
*
* The Original Code is from:
* Brazil project web application toolkit release 2.3.
* The Initial Developer of the Original Code is: drach.
* Portions created by drach are Copyright (C) Sun Microsystems, Inc.
* All Rights Reserved.
*
* Contributor(s): drach, suhler.
*
* Version: 2.5
* Created by drach on 01/06/29
* Last modified by suhler on 09/01/30 16:19:31
*
* Version Histories:
*
* 2.5 09/01/30-16:19:31 (suhler)
* make inner class static
*
* 2.4 05/06/17-15:49:54 (suhler)
* added stringsValid() to alter the way non-numeric strings are
* converted to numbers
*
* 2.3 04/11/30-15:19:45 (suhler)
* fixed sccs version string
*
* 2.2 03/07/10-09:24:20 (suhler)
* Use common "isTrue/isFalse" code in utin/format.
*
* 2.1 02/10/01-16:37:02 (suhler)
* version change
*
* 1.4 01/08/17-16:57:57 (drach)
* Add getValue(String, Dictionary) method
*
* 1.3 01/07/16-16:54:36 (suhler)
* monro fix to main
*
* 1.2 01/07/13-16:39:38 (drach)
* Add comments and a test driver.
*
* 1.2 01/06/29-11:04:56 (Codemgr)
* SunPro Code Manager data about conflicts, renames, etc...
* Name history : 1 0 util/Calculator.java
*
* 1.1 01/06/29-11:04:55 (drach)
* date and time created 01/06/29 11:04:55 by drach
*
*/
package sunlabs.brazil.util;
import java.text.DecimalFormat;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Properties;
/**
*
* Calculator implements a simple arithmetic expression evaluator. It
* can evaluate typical expressions with the "normal" operators and
* precedence. Formally, the BNF for the supported grammar is:
*
* A <letter> is defined as a Java
* <stmt> ::= <var> = <expr> | <expr>
* <expr> ::= <rexpr> | <expr> <bool op> <rexpr>
* <bool op> ::= && | <or>
* <or> ::= ||
* <rexpr> ::= <aexpr> | <rexpr> <rel op> <aexpr>
* <rel op> ::= < | <= | > | >= | == | !=
* <aexpr> ::= <term> | <aexpr> <add op> <term>
* <add op> ::= + | -
* <term> ::= <factor> | <term> <mult op> <factor>
* <mult op> ::= * | / | %
* <factor> ::= <var> | <num> | ! <factor> | ( <expr> )
* <var> ::= <letter> | <var> <var2>
* <var2> ::= <letterordigit> | . | _
* <num> ::= <unum> | + <unum> | - <unum>
* <unum> ::= <int> | <int> . | <int> . <int> | . <int>
* <int> ::= <digit> | <int> <digit>
*
char
for which
* Char.isLetter(char)
is true
. A
* <letterordigit> is defined as a Java char
for which
* Char.isLetterOrDigit(char)
is true
. A digit
* is defined as a Java char
for which
* Char.isDigit(char)
is true
.
*
* Values for <var>
are looked up in the supplied
* Dictionary
. If <var>
can not be found,
* it is assumed to have the value zero. If the value found is "true" or
* "yes" (case insensitive), it is assumed to be one. Similarly, if the
* value found is "false" or "no", it is assumed to be zero. Assignment
* to <var>
stores the computed value in the same
* Dictionary
.
*
* The period in <unum>
, if there is one, must be
* immediately adjacent to surrounding <int>
s.
*
* @author Steve Drach <drach@sun.com>
* @version 2.5
*/
public class Calculator {
private Token t;
private Tokenizer tknizr;
private String error;
private Dictionary symbols;
private DecimalFormat decimalFormat;
private boolean allStringsValid = false;
/**
*
* The no argument constructor will create an internal
* Hashtable
in which it looks up and stores values
* associated with variables.
*
* @see java.util.Hashtable
*/
public Calculator() {
this(new Hashtable());
}
/**
*
* This constructor will use the Dictionary
parameter to
* lookup and store values associated with variables.
*
* @param d the Dictionary
object
* that serves as a symbol table
*
* @see java.util.Dictionary
*/
public Calculator(Dictionary d) {
symbols = d;
decimalFormat = new DecimalFormat();
decimalFormat.setGroupingUsed(false);
}
/**
* Normally, variables whose values are "on", "yes", or
* "true" and converted to "1.0", while the values "off", "no", and
* "false" are converted to "0.0". All other values are considered
* an error. By passing "true", all normally invalid strings are
* given a value of "1.0".
*
*/
public void stringsValid(boolean allStringsValid) {
this.allStringsValid = allStringsValid;
}
/**
*
* Computes the value of the statement passed in the parameter
* string and returns a string representation of the result.
* If the input statement consists only of a variable name and
* the result of the computation is zero, null
is
* returned.
*
* @param stmt a string representation of
* an arithmetic expression or
* assignment
*
* @exception ArithmeticException occurs when a result is
* improper (e.g. infinity) or
* when the input statement can
* not be parsed
*
* @return a string representation of
* the computed result or
* null
*/
public String getValue(String stmt) throws ArithmeticException {
tknizr = new Tokenizer(stmt);
t = tknizr.next();
String value = stmt();
if (error != null) {
String s = error;
error = null;
throw new ArithmeticException(s);
}
return value;
}
/**
*
* Computes the value of the statement passed in the parameter
* string and returns a string representation of the result.
* If the input statement consists only of a variable name and
* the result of the computation is zero, null
is
* returned. The second parameter is used as a symbol table
* for the duration of this method call. Note this method is
* not thread safe!
*
* @param stmt a string representation of
* an arithmetic expression or
* assignment
*
* @param d the temporary symbol table
*
* @exception ArithmeticException occurs when a result is
* improper (e.g. infinity) or
* when the input statement can
* not be parsed
*
* @return a string representation of
* the computed result or
* null
*/
public String getValue(String stmt, Dictionary d) throws ArithmeticException {
Dictionary symbols = this.symbols;
this.symbols = d;
String value = null;
try {
value = getValue(stmt);
} finally {
this.symbols = symbols;
}
return value;
}
/*
* true
for debug output. The output probably won't
* make sense to anyone other than the author.
*/
public boolean debugging;
private void debug(String msg) {
if (debugging)
System.out.println(msg);
}
private void debug(Token t) {
if (debugging)
System.out.println(t);
}
private static class Token {
int type;
double value;
String name;
public String toString() {
StringBuffer sb = new StringBuffer("type=" + type);
if (type == VAR)
sb.append("\nname=" + name);
if (type >= VAR)
sb.append("\nvalue=" + value);
return sb.toString();
}
}
private class Tokenizer {
private Token[] token;
private char[] chars;
private int i, t;
Tokenizer(String str) {
chars = str.trim().toCharArray();
i = 0;
token = new Token[2];
token[0] = new Token();
token[1] = new Token();
t = 1;
}
private boolean pb;
Token pushback() {
pb = true;
t = 1 - t;
debug("token is free");
debug(token[t]);
return token[t];
}
Token next() {
if (pb) {
pb = false;
t = 1 - t;
debug("token is free");
debug(token[t]);
return token[t];
}
debug("token costs $$");
t = 1 - t;
while (i < chars.length && chars[i] == ' ')
i++;
if (i >= chars.length) {
token[t].type = END;
debug(token[t]);
return token[t];
}
debug("char=" + chars[i]);
int tmp = 0;
switch (chars[i]) {
case '+':
tmp = PLUS;
break;
case '-':
tmp = MINUS;
break;
case '*':
tmp = STAR;
break;
case '/':
tmp = SLASH;
break;
case '%':
tmp = MOD;
break;
case '(':
tmp = LPAREN;
break;
case ')':
tmp = RPAREN;
break;
case '=':
tmp = ASSIGN;
if (++i < chars.length && chars[i] == '=')
tmp = EQ;
else
--i;
break;
case '!':
tmp = NOT;
if (++i < chars.length && chars[i] == '=')
tmp = NE;
else
--i;
break;
case '<':
tmp = LT;
if (++i < chars.length && chars[i] == '=')
tmp = LE;
else
--i;
break;
case '>':
tmp = GT;
if (++i < chars.length && chars[i] == '=')
tmp = GE;
else
--i;
break;
case '&':
if (++i < chars.length && chars[i] == '&')
tmp = AND;
else
--i;
break;
case '|':
if (++i < chars.length && chars[i] == '|')
tmp = OR;
else
--i;
break;
}
if (tmp != 0) {
i++;
token[t].type = tmp;
token[t].name = signs[tmp];
debug(token[t]);
return token[t];
}
int j = i;
while ((Character.isDigit(chars[i]) || chars[i] == '.')
&& ++i < chars.length);
if (j != i) {
String s = new String(chars, j, i-j);
try {
token[t].value = (new Double(s)).doubleValue();
} catch (NumberFormatException e) {
if ((s = e.getMessage()).equals("."))
s = "Single point";
error(s + ": " + remainder(j));
}
token[t].type = NUM;
token[t].name = s;
debug(token[t]);
return token[t];
}
if (Character.isLetter(chars[i])) {
j = i;
while ((Character.isLetterOrDigit(chars[i]) || chars[i] == '.'
|| chars[i] == '_') && ++i < chars.length);
String name = new String(chars, j, i-j);
Object o;
if (symbols instanceof Properties)
o = ((Properties)symbols).getProperty(name);
else
o = symbols.get(name);
if (o == null) {
token[t].value = 0.0; // do not store name=0 in symbols
} else {
String s = o.toString().trim();
try {
if (s.length() == 0) {
token[t].value = 0.0;
} else if (Character.isLetter(s.charAt(0))) {
if (Format.isFalse(s)) {
token[t].value = 0.0;
} else if (allStringsValid || Format.isTrue(s)) {
token[t].value = 1.0;
} else {
s = "Invalid value '" + s + "' from";
throw new NumberFormatException(s);
}
} else {
token[t].value = (new Double(s)).doubleValue();
}
} catch (NumberFormatException e) {
if ((s = e.getMessage()).equals("."))
s = "Single point";
error(s + ": " + remainder(j));
}
}
token[t].type = VAR;
token[t].name = name;
debug(token[t]);
return token[t];
}
error("Unrecognized token: " + remainder());
token[t].type = END;
debug(token[t]);
return token[t];
}
String remainder() {
return new String(chars, i, chars.length-i);
}
String remainder(int i) {
return new String(chars, i, chars.length-i);
}
}
/**
*
* A test driver for the calculator. Type in arithmetic expressions
* or assignments and see the results. Use "dump" to see contents of
* all assigned variables.
*
* @param args required signature for
* main
method, not used
*/
public static void main(String[] args) {
Properties p = new Properties();
Calculator c = new Calculator(p);
java.io.BufferedReader in = new java.io.BufferedReader(
new java.io.InputStreamReader(System.in));
while (true) {
System.err.print(": ");
try {
String line = in.readLine();
if ("dump".equals(line)) {
p.list(System.out);
continue;
}
String value = c.getValue(line);
if (value == null)
value = "0";
System.out.println(value);
} catch (ArithmeticException e) {
System.out.println(e.getMessage());
} catch (Exception e) {};
}
}
private static final int END = -1;
private static final int PLUS = 1;
private static final int MINUS = 2;
private static final int STAR = 3;
private static final int SLASH = 4;
private static final int LPAREN = 5;
private static final int RPAREN = 6;
private static final int NOT = 7;
private static final int AND = 8;
private static final int OR = 9;
private static final int ASSIGN = 10;
private static final int MOD = 11;
private static final int EQ = 12;
private static final int NE = 13;
private static final int LT = 14;
private static final int LE = 15;
private static final int GT = 16;
private static final int GE = 17;
private static final int VAR = 20;
private static final int NUM = 21;
private static final String[] signs = {
"", "+", "-", "*", "/", "(", ")", "!", "&&", "||",
"=", "%", "==", "!=", "<", "<=", ">", ">="
};
}