S-JIS[2010-02-13/2010-02-16] 変更履歴

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

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

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

JDK1.4用に作っていたeval.jarをJDK1.6用に変更したと同時に多少仕様変更・仕様追加しました。

eval16.jar (272kB) [/2010-02-16] ←ソース付き
eval16_test.jar (127kB) [/2010-02-16] JUnitテストケースとサンプルとbuild.xml
Javadoc   [/2010-02-16]  


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

※「/**/」でブロックコメント、「//〜」で行コメント。 コメントは空白と同じく字句解析でスキップするので、解釈結果には残らない。
※Javaでは論理演算や条件にはboolean型しか使えないが、このクラスでは数値で扱う。と言うより、数値しか扱えない(苦笑)

主な使い方(詳細(?)はJavadoc参照)
クラス 主なメソッド 概要 更新日
Factory getDefaultRule() デフォルトの解析ルールを取得する。独自ルールのときは使わない。  
getJavaRule() Javaで使える演算子のみを使用するルールを取得する。  
getRule() ファクトリーの中に保持されているルールを取得する。  
Rule parse(式の文字列) 文字列の字句解析・構文解析を行い、構文解析木を作成する。  
Expression setVariable(変数I/F) 変数の初期値定義を行う。変数を使わないなら呼ぶ必要なし。  
setFunction(関数I/F) 関数定義を指定する。関数を使わないなら呼ぶ必要なし。  
setOperator(演算I/F) 演算群を設定する。
指定されない場合のデフォルトは、実行するeval()メソッドによって異なる。
 
setEvalLog(ログI/F) ログ出力インターフェースを設定する。デフォルトではログ出力なし。 eval16
eval() 構文解析木の演算をObject型で実施する。
演算I/Fが指定されていない場合、JavaExOperatorを使用する。
 
evalInt()
evalLong()
evalDouble()
構文解析木の演算をint・long・double型で実施する。
(eval16では専用の実装は廃止した。IntOperator・LongOperator・DoubleOperatorを使用する)
 
optimize(変数I/F, 演算I/F) 超簡易最適化を行う。(eval16から、メソッド名と引数が変わった) eval16
refactorName(変換I/F) リファクタリング(識別子の名称変更)を行う。  
refactorFunc(変換I/F, ルール) リファクタリング(識別子の関数への変換)を行う。  
dup() 複製する。最適化前や演算子変更前の構文解析木を保存しておきたい時などに使用する。  
toString() 保持している式を文字列に整形する。  
Variable getValue(変数名)
setValue(変数名, 値)
getArrayValue(配列, 添字)
setArrayValue(配列, 添字, 値)
getFieldValue(OBJ, フィールド名)
setFieldValue(OBJ, フィールド名, 値)
変数の値を管理(保持)する為のインターフェース。このメソッドはユーザーが実装する。
setVariable()でExpressionに指定することにより、式の中で変数が出てくると このインターフェースのメソッドが呼ばれる。
具象クラス:MapVariable(デフォルト)、DefaultVariable
変数の使用例
 
Function eval(オブジェクト, 関数名, 引数の配列) 関数の実体を定義する為のインターフェース。このメソッドはユーザーが実装する。
setFunction()でExpressionに指定することにより、式の中で関数が出てくると このインターフェースのeval()が呼ばれる。
(eval16では、どの型の演算でもeval()が呼ばれる)
具象クラス:InvokeFunction(デフォルト)、VoidFunction
関数の使用例
 
Operator   演算を実行する為のインターフェース。このメソッドはユーザーが実装する。
setOperator()でExpressionに指定することにより、各演算ではこのインターフェースのメソッドが呼ばれる。
具象クラス:IntOperatorLongOperatorDoubleOperatorJavaExOperator(デフォルト)
 
EvalLog   演算中のログを出力する為のインターフェース。このメソッドはユーザーが実装する。
setEvalLog()でExpressionに指定することにより、各演算の途中でこのインターフェースのメソッドが呼ばれる。
主にデバッグ目的。
具象クラス:EvalLogAdapter
eval16


四則演算クラスの使用例

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

変数の使用例

変数を使う例。

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

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

import jp.hishidama.eval.Expression;
import jp.hishidama.eval.ExpRuleFactory;
import jp.hishidama.eval.var.MapVariable;

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

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

	private static void example1() {
		MapVariable<String, Long> varMap = new MapVariable<String, Long>(String.class, Long.class);
		varMap.put("aaa", 2L);
		dumpMap(varMap);

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

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

		exp.setVariable(varMap);

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

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

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

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

		exp.setVariable(varMap);

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

	private static void dumpMap(MapVariable<String, Long> varMap) {
		for (String key : varMap.getMap().keySet()) {
			Long val = varMap.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

配列・マップの例

Javaの配列を使う例。
MapVariableでは、Javaの配列を扱うことが出来る。

	MapVariable<String, Object> map = new MapVariable<String, Object>(String.class, Object.class);
	int[] a = new int[4];
	a[1] = 11;
	map.put("a", a);
	
	Expression exp = ExpRuleFactory.getDefaultRule().parse("a[1]+=33");
	exp.setVariable(map);

	System.out.println("演算:" + exp.evalInt());
	System.out.println("配列:" + Arrays.toString(a));
演算:44
配列:[0, 44, 0, 0]

デフォルトのeval()では変数や値はJavaのオブジェクトとして扱うので、通常のJavaの配列と同じように配列オブジェクトの代入も行える。

	Long[] a = new Long[4];
	a[1] = 11L;
	Map<String, Long[]> map = new HashMap<String, Long[]>();
	map.put("a", a);

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

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

eval16では、配列形式で、マップ(連想配列)を扱うことも出来る。

	Map<String, Integer> m = new TreeMap<String, Integer>();
	m.put("abc", 123);
	m.put("def", 456);

	MapVariable<String, Object> varMap = new MapVariable<String, Object>(String.class, Object.class);
	varMap.put("m1", m);

	String str = "m1[\"zzz\"] = m1[\"abc\"] + m1[\"def\"]";	//m1["zzz"] = m1["abc"] + m1["def"]
	Expression exp = ExpRuleFactory.getDefaultRule().parse(str);
	exp.setVariable(varMap);

	System.out.println("演算 :" + exp.eval());
	System.out.println("マップ:" + m);
演算 :579
マップ:{abc=123, def=456, zzz=579}

配列形式の場合、演算実行時に変数インターフェースのgetArrayValue()およびsetArrayValue()が呼ばれる。
MapVariable(厳密にはその親クラスであるDefalutVariable)クラスでそれらのメソッドを実装しており、上記のような処理を行っている。

なお、getArrayValue()およびsetArrayValue()の添字を表す引数の型は、旧バージョンではintだった(配列の添字はintだから)。
eval16では引数の型がObjectになったので、数値以外も扱えるようになった(つまりマップ(連想配列)が使えるようになった)。


関数の使用例

関数を使う例。

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

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

Functionには2つのメソッドがある。[2010-02-16]
第1引数がObjectのものは、「そのオブジェクトのメソッド」という構文であることを示す。
第1引数がObjectでない方は、グローバルな関数である。(Javaにはそんなの無いけど)
(オブジェクトがnullの場合にメソッド系なのかグローバル関数系なのか判別できないので、eval()を2種類用意することにした)

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

	@Override
	public Object eval(String name, Object[] args) throws Exception {
		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();
	}

	@Override
	public long eval(Object object, String name, Object[] args) throws Exception {
		return eval(name, args);
	}
}
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

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

オブジェクトのメンバー(フィールド・メソッド)を操作する例。

メソッド
リフレクションを用いてメソッドを呼ぶInvokeFunctionというクラスを用意した。これがデフォルトで使われるので、オブジェクトのメソッドが扱える。
フィールド
変数を扱うMapVariableも(グローバルな変数はMapで管理しているが)、オブジェクトのフィールドはリフレクションを使うようになっている。
変数インターフェースのgetFieldValue()およびsetFieldValue()が呼ばれる が、デフォルトでリフレクションを使っている)

ただしリフレクションなので、publicなクラスのpublicなメンバーしかアクセスできない。
独自のFunctionクラスを作れば、この辺りは回避できるはず。

public class SampleClass {
	public int n = 10;

	public int get() { return 12; }
}
	SampleClass sc = new SampleClass();
	MapVariable<String, SampleClass> map = new MapVariable<String, SampleClass>(String.class, SampleClass.class);
	map.put("s", sc);

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

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

超簡易最適化

式を最適化する機能を試しに作ってみた。

ここで言う最適化とは、「定数と定数の演算は定数になるから、それをまとめてしまおう」というもの。
例:「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.optimize(null, new IntOperator());
	System.out.println("最適化前:" + exp.toString());
	System.out.println("最適化後:" + opt.toString());
最適化前:1 + 2 + a
最適化後:3 + a

リファクタリング

識別子の名称変更

変数名/関数名、あるいはフィールド名(メソッド名)を変更する例。

import jp.hishidama.eval.ref.RefactorVarName;
	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

変数の関数への変更

変数/フィールドの値取得関数(メソッド)呼び出しに変更する例。

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

	MapVariable<String, Object> var = new MapVariable<String, Object>();
	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

独自ルールの作成方法

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

BASIC風のルールにしてみる例

累乗の演算子を「^」にし、コメントを「'」にする例。

class BasicPowerRuleFactory extends ExpRuleFactory {

	public BasicPowerRuleFactory() {
		super();
	}

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

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

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

	@Override
	protected AbstractExpression createLetPowerExpression() {
		// 「^=」を指数演算の代入演算子とする
		AbstractExpression e = new LetPowerExpression();
		e.setOperator("^=");
		return e;
	}

	@Override
	protected LexFactory getLexFactory() {
		// 「'」を行コメントとする
		List<CommentLex> list = new ArrayList<CommentLex>();
		list.add(new LineComment("'"));

		LexFactory factory = super.getLexFactory();
		factory.setDefaultCommentLexList(list);
		return factory;
	}
}
	BasicPowerRuleFactory factory = new BasicPowerRuleFactory();
	Rule rule = factory.getRule();

	String str = "256 ^ 2 '二乗";
	System.out.println("式:" + str);

	Expression exp = rule.parse(str);
	long ret = exp.evalLong();

	System.out.println("= " + ret);
式:256 ^ 2 '二乗
= 65536

シングルクォーテーションで囲まれた文字列も文字列として扱う例

フィルターを利用して、シングルクォーテーションをダブルクォーテーションと同じ扱いにする例。
デフォルトのJavaExOperatorでは、シングルクォーテーションで囲まれた文字列は、先頭1文字のCharactorインスタンスとして扱う。
これを、ダブルクォーテーションで囲まれた文字列と同様に、囲まれた文字列そのものとする。

public class StringOperator extends JavaExOperator {

	//シングルクォーテーションで囲まれた文字列
	@Override
	public Object character(String word, AbstractExpression exp) {
		return word;
	}

	//ダブルクォーテーションで囲まれた文字列
	@Override
	public Object string(String word, AbstractExpression exp) {
		return word;
	}
}
	MapVariable<String, String> var = new MapVariable<String, String>();
	Rule rule = ExpRuleFactory.getDefaultRule();
	Expression exp = rule.parse("v1='ABC', v2=\"DEF\"");
	exp.setVariable(var);

	// デフォルトでは、シングルクォーテーションで囲んだ文字列は、解釈時は先頭1文字を切り出している
	exp.eval();
	System.out.println("デフォルト:" + var.getMap());

	exp.setOperator(new StringOperator());
	exp.eval();
	System.out.println("変更後  :" + var.getMap());
デフォルト:{v1=A, v2=DEF}
変更後  :{v1=ABC, v2=DEF}

文字列表現の例

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

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)

後から演算子を変える例

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

	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(等しくない)となる。


変更履歴

前バージョンからの主な変更点


更新日 変更内容
2010-02-13 eval16を公開。
2010-02-15 Functionのメソッドを2つに分割。
2010-02-16 InvokeFunctionの実装を、新設したInvokeUtilを使用するよう変更。
これは、引数のオブジェクトをメソッドの型に合うように変換してメソッドを実行するユーティリティー。
オーバーロードとかの対応は怪しいが…。

参考


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