S-JIS[2006-05-30/2007-03-02] 変更履歴

四則演算実行クラス(JDK1.4版)

ひしだま作のJava用四則演算の計算式の解釈・評価実行クラスです。もはや“四則”演算をはるかに超えてるけど(笑)

式を書いた文字列を解釈し、計算を行います。いわゆる電卓のようなものです。
long型またはdouble型、またはJavaのObject型で演算できます。

eval.jar (86.4kB) [/2007-03-02]  
eval_src.zip (86.3kB) [/2007-03-02] ←ソースはEclipseで添付できる形式
eval_test.zip (36.8kB) [/2007-03-02] JUnitテストケースとbuild.xml
Javadoc   [/2010-02-13] ←新バージョン向け
新バージョン(eval16) [2010-02-13]


当クラスで使用できる演算子(→Java標準の演算子
優先順位 演算子 概要 備考 更新日
識別子
"文字列"
'文字'
(式)
数値・変数
文字列
文字
括弧
変数名は、数字以外から始まる 演算子以外の文字列。
"文字列"'文字'はあまり使い道を考えていないので、けっこう手抜き(汗)
2007-02-21
  関数名(引数)
変数名[添字]
変数名.識別子
++ --
関数呼び出し
配列
フィールド
インクリメント
関数の引数はカンマ区切り(引数なしも可)。
配列は、基本的にJavaの配列を使用。
「変数名.メソッド()」も可。
このインクリメント・デクリメントは後置単項演算子(例:i++)。
2007-02-18
  ** 累乗(べき乗・指数) **」は、PerlやPL/Iの指数演算子。 →変更する方法 2007-02-18
  + - ~ ! ++ -- 符号・ビット否定・否定
インクリメント
否定演算子「!」は、0以外のとき0、0のとき1。
このインクリメント・デクリメントは前置単項演算子(例:++i)。
2006-10-31
  * / % 乗算・除算・余算    
  + - 加減算    
  << >> >>> シフト演算 doubleのときは2のべき乗による乗除算(>>>は符号を正にして>>)。 2006-06-07
  < <= > >= 大小比較演算 真のとき1、偽のとき0。 2006-06-01
  == != 等値比較演算 2006-06-01
  & ビット論理積 doubleのときはlongとして演算。 2006-05-31
  ^ ビット排他的論理和 2006-05-31
  | ビット論理和 2006-05-31
  && 論理積 左が0のとき0、それ以外のとき右の値。 2006-06-01
  || 論理和 左が0以外のとき左の値、0のとき右の値。 2006-06-01
  ? : 条件演算子 三項演算子。
第一項が0以外のとき第二項の値、0のとき第三項の値。
2006-06-01
  = += -= *= /= %=
<<= >>= >>>=
&= |= ^=
代入演算子 右結合(他の二項演算子は左結合)。
変数は大文字小文字の区別有り。
2006-06-02
, カンマ演算子 カンマで区切られた一番右の値が最終的な値。
(Javaにはこの演算子は存在しない)
2006-06-02

※Javaでは論理演算や条件にはboolean型しか使えないが、このクラスでは数値で扱う。と言うより、数値しか扱えない(苦笑)


主な使い方(詳細(?)はJavadoc参照)
クラス 主なメソッド 概要 更新日
Factory getDefaultRule() デフォルトの解析ルールを取得する。独自ルールのときは使わない。 2007-02-18
getJavaRule() Javaで使える演算子のみを使用するルールを取得する。 2007-02-21
getRule() ファクトリーの中に保持されているルールを取得する。 2007-02-18
Rule parse(式の文字列) 文字列の字句解析・構文解析を行い、構文解析木を作成する。 2007-02-18
Expression setVariable(変数I/F) 変数の初期値定義を行う。変数を使わないなら呼ぶ必要なし。 2006-06-02
setFunction(関数I/F) 関数定義を指定する。関数を使わないなら呼ぶ必要なし。 2006-06-06
evalLong() 構文解析木の演算をlong型で実施する。 2006-06-07
evalDouble() 構文解析木の演算をdouble型で実施する。 2006-06-07
eval() 構文解析木の演算をObject型で実施する。 2007-02-18
optimizeLong() 超簡易最適化を行う。 2007-02-18
refactorName(I/F) リファクタリング(識別子の名称変更)を行う。 2007-02-19
refactorFunc(I/F,ルール) リファクタリング(識別子の関数への変換)を行う。 2007-02-20
dup() 複製する。最適化前や演算子変更前の構文解析木を保存しておきたい時などに使用する。 2007-02-18
toString() 保持している式を文字列に整形する。 2006-10-27
Variable setValue(変数名,値)
getObject(変数名)
変数の値を管理(保持)する為のインターフェース。このメソッドはユーザーが実装する。
setVariable()でExpressionに指定してやることにより、式の中で変数が出てくると このインターフェースのevalXxx()が呼ばれる。
2007-02-18
Function evalLong(オブジェクト,関数名,引数の配列)
evalDouble(オブジェクト,関数名,引数の配列)
evalObject(オブジェクト,関数名,引数の配列)
関数の実体を定義する為のインターフェース。このメソッドはユーザーが実装する。
setFunction()でExpressionに指定してやることにより、式の中で関数が出てくると このインターフェースのevalXxx()が呼ばれる。
2007-02-18

四則演算クラスの使用例

import jp.hishidama.eval.*;

/**
 * 四則演算の例
 * @author ひしだま
 */
public class Calc {

	public static void main(String[] args){
		String str = args[0];
		System.out.println("式 :" + str);

		Rule rule = ExpRuleFactory.getDefaultRule();
		Expression exp = rule.parse(str);	//解析
		long result = exp.evalLong(); 	//計算実施
		System.out.println("結果:" + result);
	}
}
>java Calc "1+2 * (2 - 4) / -1"
式 :1+2 * (2 - 4) / -1
結果:5

変数の使用例

変数を使う例。[2006-06-02/2007-02-18]

変数は、先頭が数字以外の文字列。超手抜きなので、演算子以外の全ての文字・記号が変数名の一部として使用可能(爆)

Variableインターフェースを実装したクラスで変数と値を管理するが、MapVariableというクラスをデフォルトで用意してある。
これは、Mapに 変数名(String)をキーとして 値(Long)を放り込んで管理している単純なクラス。
式の中で初めて変数を参照する場合は、Mapにその変数名を入れておく必要がある。
式の中でいきなり代入する分には、事前定義(宣言)は不要。eval()の実行後、Mapの各変数に計算後の値が入る。

import java.util.*;

import jp.hishidama.eval.*;

/**
 * 変数の使用例
 * 
 * @author ひしだま
 */
public class VarSample {

	public static void main(String[] args) {
		example1();
		System.out.println();
		example2();
	}

	private static void example1() {
		Map varMap = new HashMap();
		varMap.put("aaa", new Long(2));
		dumpMap(varMap);

		String str = "1 + aaa * 3";
		System.out.println("式:" + str);

		Expression exp = ExpRuleFactory.getDefaultRule().parse(str);

		exp.setVariable(new MapVariable(varMap));

		System.out.println("結果:" + exp.evalLong());
	}

	private static void example2() {
		Map varMap = new HashMap();
		varMap.put("aaa", new Long(3));	//初めて使う変数だけは初期化が必要
		dumpMap(varMap);

		String str = "bbb=4, aaa+=bbb*5, aaa+bbb";
		System.out.println("式 :" + str);

		Expression exp = ExpRuleFactory.getDefaultRule().parse(str);

		exp.setVariable(new MapVariable(varMap));

		System.out.println("結果:" + exp.evalLong());
		dumpMap(varMap);
	}

	private static void dumpMap(Map map) {
		for (Iterator i = map.keySet().iterator(); i.hasNext();) {
			String key = (String) i.next();
			Long val = (Long) map.get(key);
			System.out.println(key + " = " + val);
		}
	}
}
>java VarSample
aaa = 2
式 :1 + aaa * 3
結果:7

aaa = 3
式 :bbb=4, aaa+=bbb*5, aaa+bbb
結果:27
aaa = 23
bbb = 4

配列の例

配列も使える。[2006-10-27/2007-02-18]

配列の仕様は、当初はJavaScriptの配列と似たような扱いをしていた。 すなわち、変数名と添字を結合して1つの文字列にして、それを変数名として扱っていた。(例えば通常の変数は「abc」というキーで値を保持し、配列は「a[1]」という文字列をキーとして値を保持していた)
最新版では、Javaの配列をそのまま使える。

	Long[] a = new Long[4];
	a[1] = new Long(11);
	Map map = new HashMap();
	map.put("a", a);
	
	Expression exp = ExpRuleFactory.getDefaultRule().parse("a[1]+=33");
	exp.setVariable(new MapVariable(map));

	System.out.println("演算:" + exp.evalLong());
	System.out.println("配列:" + a[1]);
演算:44
配列:44

Object型で演算を実行すると変数や値をJavaのオブジェクトとして扱うので、通常のJavaの配列と同じように配列オブジェクトの代入も行える。[2007-02-18]

	Long[] a = new Long[4];
	a[1] = new Long(11);
	Map map = new HashMap();
	map.put("a", a);
	
	Expression exp = ExpRuleFactory.getDefaultRule().parse("c=a, c[1]++");	//配列変数を代入して扱える
	exp.setVariable(new MapVariable(map));

	exp.eval();
	Long[] c = (Long[]) map.get("c");
	System.out.println("c[1]=" + c[1]);
	System.out.println("a[1]=" + a[1]);
c[1]=12
a[1]=12

関数の使用例

関数を使う例。[2006-06-06/2007-02-18]

Functionというインターフェースを実装したクラスを用意する(関数定義クラスと呼ぶことにする)。
このインターフェースにはeval(オブジェクト,関数名,引数の配列)というメソッドがあり、式の評価中に関数が見つかると、このメソッドが呼ばれる。
したがって、この実装クラスで演算をするようにプログラミングしておけば、関数が自由に使えることになる。

なお 準備作業として、用意した関数定義クラスのインスタンスをExpression#setFunction()というメソッドで渡す必要がある。 (このインスタンスはシングルトンでよい)

引数のオブジェクトに値が入っているときは、そのクラスのメソッドという構文であることを示す。
引数のオブジェクトがnullなら、グローバルな関数である。(Javaにはそんなの無いけど)

/**
 * 数値演算関数(long)サンプル.
 *
 * Mathの各関数のうち、引数がlong型の関数を呼び出すサンプル。
 */
public class MathFunction implements Function {

	public long evalLong(Object object, String name, Long[] args) throws Throwable {
		Class[] types = new Class[args.length];
		for (int i = 0; i < types.length; i++) {
			types[i] = long.class;
		}

		Method m = Math.class.getMethod(name, types);
		Object ret = m.invoke(null, args);
		// return Long.parseLong(ret.toString());
		return ((Number) ret).longValue();
	}

	//evalDouble()も同様
}
import jp.hishidama.eval.*;

/**
 * 関数の使用例
 * 
 * @author ひしだま
 */
public class FuncSample {

	public static void main(String[] args) {

		// java.lang.Math#max(long,long)(戻り値:long)を呼び出す
		String str = "max(2, 99)";

		System.out.println("式 :" + str);

		Expression exp = ExpRuleFactory.getDefaultRule().parse(str);

		Function func = new MathFunction(); // java.lang.Mathを呼び出すサンプルクラス
		exp.setFunction(func);

		System.out.println("結果:" + exp.evalLong());
	}
}
>java Func
式 :max(2, 99)
結果:99

オブジェクトのメンバーの例

オブジェクトのメンバー(フィールド・メソッド)も扱える。[2007-02-18]

リフレクションを用いてメソッドを呼ぶInvokeFunctionというクラスを用意した。これがデフォルトで使われるので、オブジェクトのメソッドが扱える。
変数を扱うMapVariableも(グローバルな変数はMapで管理しているが)、オブジェクトのフィールドはリフレクションを使うようになっている。
ただしリフレクションなので、publicなクラスのpublicなメンバーしかアクセスできない。
独自のFunctionクラスを作れば、この辺りは回避できるはず。

public class SampleClass {
	public long n = 10;

	public long get() { return 12; }
}
	SampleClass sc = new SampleClass();
	Map map = new HashMap();
	map.put("s", sc);

	Rule rule = ExpRuleFactory.getDefaultRule();
	Expression exp = rule.parse("s.n");
	exp.setVariable(new MapVariable(map));
	System.out.println("フィールド:" + exp.evalLong());

	exp = rule.parse("s.get()");
	exp.setVariable(new MapVariable(map));
	System.out.println("メソッド :" + exp.evalLong());
フィールド:10
メソッド :12

超簡易最適化

式を最適化する機能を試しに作ってみた。[2007-02-18]

ここで言う最適化とは、「定数と定数の演算は定数になるから、それをまとめてしまおう」というもの。
例:「1 + 1」→「2」

どれくらい「超簡易」かと言うと、「1 + 2 + a」は「3 + a」になるが、「a + 1 + 2」は最適化できない(爆)
なぜかと言うと、「a + 1 + 2」は木構造的には「(a + 1) + 2」であり、「a+1」は変数を含んでいるから最適化できず、次は「式 + 2」なので最適化できない。
木構造のまま最適化を考えるのは難しそうなので、あまり深入りしないことに…。

	Rule rule = ExpRuleFactory.getDefaultRule();
	Expression exp = rule.parse("1+2+a");
	Expression opt = exp.dup();
	opt.optimizeLong();
	System.out.println("最適化前:" + exp.toString());
	System.out.println("最適化後:" + opt.toString());
最適化前:1 + 2 + a
最適化後:3 + a

リファクタリング

識別子の名称変更

変数名/関数名、あるいはフィールド名(メソッド名)を変更する機能を追加してみた。[2007-02-19]

	Rule rule = ExpRuleFactory.getDefaultRule();
	Expression exp = rule.parse("aa+bb+1");

	System.out.println("変更前:" + exp.toString());
	exp.refactorName(new RefactorVarName(null, "bb", "foo"));	//変数名bbをfooに変更
	System.out.println("変更後:" + exp.toString());
変更前:aa + bb + 1
変更後:aa + foo + 1

変数の関数への変更

変数/フィールドの値取得関数(メソッド)呼び出しに変更する機能を追加してみた。[2007-02-20]

	Rule rule = ExpRuleFactory.getDefaultRule();
	Expression exp = rule.parse("a.x /2");

	MapVariable var = new MapVariable();
	var.setValue("a", new Object());
	exp.setVariable(var);

	System.out.println("変更前:" + exp.toString());
	exp.refactorFunc(new RefactorVarName(Object.class, "x", "getX()"), rule);
	System.out.println("変更後:" + exp.toString());
変更前:a.x / 2
変更後:a.getX() / 2

独自ルールの作成方法

デフォルトの構文解析ルールをカスタマイズできる。[2007-02-18]
ファクトリークラスでルールを生成するので、ファクトリーを継承してルールを作り直せばよい。
演算子の記号を変えたり、不要な演算子を削ったりするのは簡単に出来る。
特にチェックはしていないので、重複する記号を使ったりすると動作が変になるかもしれないけど…。

累乗の演算子をBASICの「^」にしてみる例:

class BasicPowerRuleFactory extends ExpRuleFactory {

	public BasicPowerRuleFactory() {
		super();
	}

	protected AbstractExpression createBitXorExpression() {
		// 「^」を排他的論理和では使わないようにする
		return null;
	}

	protected AbstractExpression createLetXorExpression() {
		// 「^=」を排他的論理和では使わないようにする
		return null;
	}

	protected AbstractExpression createPowerExpression() {
		// 「^」を指数演算子とする
		AbstractExpression e = new PowerExpression();
		e.setOperator("^");
		return e;
	}

	protected AbstractExpression createLetPowerExpression() {
		// 「^=」を指数演算の代入演算子とする
		AbstractExpression e = new LetPowerExpression();
		e.setOperator("^=");
		return e;
	}
}
	BasicPowerRuleFactory factory = new BasicPowerRuleFactory();
	Rule rule = factory.getRule();
	Expression exp = rule.parse("2 ^ 8");
	System.out.println("累乗:" + exp.evalLong());
累乗:256

文字列表現の例

単なるtoString()。保持している式を文字列に整形するようにしてみた。[2006-10-27]

import jp.hishidama.eval.*;

/**
 * 文字列の出力
 * 
 * @author ひしだま
 */
public class Print {

	public static void main(String[] args) {
		String str = args[0];
		System.out.println("入力:" + str);

		Expression exp = Expression.parse(str);
		System.out.println("保持:" + exp.toString());
	}
}
>java Print (1+2+3)*(4+5+6)/((7+8)+9)
入力:(1+2+3)*(4+5+6)/((7+8)+9)
保持:(1 + 2 + 3) * (4 + 5 + 6) / ((7 + 8) + 9)

後から演算子を変える例

一旦作成した構文木に対し、ちょっと技巧を要するが、演算子の種類を変えられる。[2007-02-18/2007-02-20]
例えば、最初は「**」で解析し、文字列で出力するときに「^」に変えることが出来る。

	Rule rule = ExpRuleFactory.getDefaultRule();
	Expression exp = rule.parse("2**3**4");

	exp.search(new SearchAdapter() {
		// 累乗演算子の「**」を「^」に変える
		public void search(AbstractExpression exp) {
			if (exp instanceof PowerExpression) {
				exp.setOperator("^");
			}
		}
	});

	System.out.println("結果:" + exp.toString());
結果:2 ^ 3 ^ 4

構文木の中を再帰的にサーチするsearch()というメソッドを用意した。
これを使って 目的の演算子を使っている箇所を探し、全ての構文木を目的の文字列に変えてやればよい。

これによって変更した構造と変更前の構造は、equals()ではtrueを返す(等しい)が、same()ではfalse(等しくない)となる。[2007-02-28]


参考


自作ソフトへ戻る / 技術メモへ行く
メールの送信先:ひしだま