かとじゅんの技術日誌

技術の話をするところ

数値がBigDecimal型な計算式言語を作ってみた(2)

http://d.hatena.ne.jp/j5ik2o/20091107/1257598591
の続きです。

まず、Calculatorのソースから。

/**
 * 計算式を渡すと指定した丸め方式で計算する計算機クラス。
 * 
 * @author j5ik2o
 * 
 */
public class Calculator {

	private final EvaluateContext evaluateContext;

	/**
	 * 初期化する。
	 * 
	 * @param scale
	 *            有効桁数
	 * @param roundingMode
	 *            {@link RoundingMode}
	 */
	public Calculator(int scale, RoundingMode roundingMode) {
		evaluateContext = new EvaluateContext(scale, roundingMode);
	}

	/**
	 * 初期化する。
	 * <p>
	 * scaleを10桁, roundingModeをHALF_UPで初期化する。
	 * </p>
	 */
	public Calculator() {
		this(10, RoundingMode.HALF_UP);
	}

	/**
	 * 計算式を解析し計算結果を{@link BigDecimal}で返します。
	 * 
	 * @param script
	 *            計算式
	 * @return {@link BigDecimal}
	 * @throws ParseException
	 *             BNFに規定しない構文があった場合
	 */
	public BigDecimal eval(String script) throws ParseException {
		CalcParser calcParser = new CalcParser(new StringReader(script));
		Expression expression = calcParser.expression();
		BigDecimal result = Evaluator.eval(evaluateContext, expression);
		return result;
	}

}

Calculator#evalで

		Expression expression = calcParser.expression();
		BigDecimal result = Evaluator.eval(evaluateContext, expression);

まず、AST(抽象構文木)であるExpressionを受け取ります。
scriptが"1 + 2"だと、Expressionは、new Add(new Value("1"), new Value("2"))となります。
それを次の行で意味を解析して計算の処理を行います。
ちなみに、Expressionを実装したモデルクラスやEvaluatorはしげる塾のものを流用しています。

まず、Expressionはこんな感じ。

/**
 * 式。
 * @version 0
 * @author Suguru ARAKAWA (Gluegent, Inc.)
 */
public interface Expression {

    /**
     * 指定のビジタを受け入れ、対応するビジタ内のメソッドを呼び出す。
     * @param <R> ビジタの実行結果
     * @param <C> ビジタに渡す引数の型
     * @param <E> ビジタの例外型
     * @param visitor 対象のビジタ
     * @param context ビジタに渡す引数(省略可)
     * @return ビジタの実行結果
     * @throws E ビジタ内で例外が発生した場合
     * @throws IllegalArgumentException {@code visitor}に{@code null}が指定された場合
     */
    <R, C, E extends Throwable> R accept(
            ExpressionVisitor<R, C, E> visitor, C context) throws E;
}

Expressionの実装クラスであるAddやValueクラスのacceptは内部でvisitorの対応するvisit????メソッドを呼びます。Value#acceptは以下。

    public <R, C, E extends Throwable> R accept(
            ExpressionVisitor<R, C, E> visitor, C context) throws E {
        if (visitor == null) {
            throw new IllegalArgumentException("visitor is null"); //$NON-NLS-1$
        }
        return visitor.visitValue(this, context);
    }

ビジターのほうのExpressionVisitorはこちら。

/**
 * {@link Expression}の構造を渡り歩くビジタ。
 * @version 0
 * @author Suguru ARAKAWA (Gluegent, Inc.)
 * @param <R> それぞれのメソッドの戻り値型
 * @param <C> それぞれのメソッドの引数型
 * @param <E> それぞれのメソッドの例外型
 */
public interface ExpressionVisitor<R, C, E extends Throwable> {
    
    /**
     * {@code Value#accept(ExpressionVisitor, Object)}が呼び出された際に実行される。
     * @param model
     *     {@code Expression#accept(ExpressionVisitor, Object)}が
     *     呼び出されたモデルオブジェクト
     * @param context
     *     {@code Expression#accept(ExpressionVisitor, Object)}の
     *     第2引数に渡された値
     * @return
     *     このメソッドの実行結果
     * @throws E
     *     このメソッドで指定の例外がスローされた場合
     */
    R visitValue(Value model, C context) throws E;

    /**
     * {@code Add#accept(ExpressionVisitor, Object)}が呼び出された際に実行される。
     * @param model
     *     {@code Expression#accept(ExpressionVisitor, Object)}が
     *     呼び出されたモデルオブジェクト
     * @param context
     *     {@code Expression#accept(ExpressionVisitor, Object)}の
     *     第2引数に渡された値
     * @return
     *     このメソッドの実行結果
     * @throws E
     *     このメソッドで指定の例外がスローされた場合
     */
    R visitAdd(Add model, C context) throws E;
    
    /**
     * {@code Subtract#accept(ExpressionVisitor, Object)}が呼び出された際に実行される。
     * @param model
     *     {@code Expression#accept(ExpressionVisitor, Object)}が
     *     呼び出されたモデルオブジェクト
     * @param context
     *     {@code Expression#accept(ExpressionVisitor, Object)}の
     *     第2引数に渡された値
     * @return
     *     このメソッドの実行結果
     * @throws E
     *     このメソッドで指定の例外がスローされた場合
     */
    R visitSubtract(Subtract model, C context) throws E;
    
    /**
     * {@code Multiply#accept(ExpressionVisitor, Object)}が呼び出された際に実行される。
     * @param model
     *     {@code Expression#accept(ExpressionVisitor, Object)}が
     *     呼び出されたモデルオブジェクト
     * @param context
     *     {@code Expression#accept(ExpressionVisitor, Object)}の
     *     第2引数に渡された値
     * @return
     *     このメソッドの実行結果
     * @throws E
     *     このメソッドで指定の例外がスローされた場合
     */
    R visitMultiply(Multiply model, C context) throws E;
    
    /**
     * {@code Divide#accept(ExpressionVisitor, Object)}が呼び出された際に実行される。
     * @param model
     *     {@code Expression#accept(ExpressionVisitor, Object)}が
     *     呼び出されたモデルオブジェクト
     * @param context
     *     {@code Expression#accept(ExpressionVisitor, Object)}の
     *     第2引数に渡された値
     * @return
     *     このメソッドの実行結果
     * @throws E
     *     このメソッドで指定の例外がスローされた場合
     */
    R visitDivide(Divide model, C context) throws E;
    
    /**
     * {@code Parenthesized#accept(ExpressionVisitor, Object)}が呼び出された際に実行される。
     * @param model
     *     {@code Expression#accept(ExpressionVisitor, Object)}が
     *     呼び出されたモデルオブジェクト
     * @param context
     *     {@code Expression#accept(ExpressionVisitor, Object)}の
     *     第2引数に渡された値
     * @return
     *     このメソッドの実行結果
     * @throws E
     *     このメソッドで指定の例外がスローされた場合
     */
    R visitParenthesized(Parenthesized model, C context) throws E;
    
    /**
     * {@code Parenthesized#accept(ExpressionVisitor, Object)}が呼び出された際に実行される。
     * @param model
     *     {@code Expression#accept(ExpressionVisitor, Object)}が
     *     呼び出されたモデルオブジェクト
     * @param context
     *     {@code Expression#accept(ExpressionVisitor, Object)}の
     *     第2引数に渡された値
     * @return
     *     このメソッドの実行結果
     * @throws E
     *     このメソッドで指定の例外がスローされた場合
     */
    R visitPlus(Plus model, C context) throws E;
    
    /**
     * {@code Parenthesized#accept(ExpressionVisitor, Object)}が呼び出された際に実行される。
     * @param model
     *     {@code Expression#accept(ExpressionVisitor, Object)}が
     *     呼び出されたモデルオブジェクト
     * @param context
     *     {@code Expression#accept(ExpressionVisitor, Object)}の
     *     第2引数に渡された値
     * @return
     *     このメソッドの実行結果
     * @throws E
     *     このメソッドで指定の例外がスローされた場合
     */
    R visitMinus(Minus model, C context) throws E;
}

ExpressionVisitorを実装したEvaluatorはこちら。

/**
 * {@link Expression}を実際に数式として評価し、結果の数値を返す。
 * 
 * @version 0.1
 * @author j5ik2o
 * @author Suguru ARAKAWA (Gluegent, Inc.)
 */
public enum Evaluator implements ExpressionVisitor<BigDecimal, EvaluateContext, NoThrow> {

    /**
     * 唯一のインスタンス。
     */
    INSTANCE,
    ;
    
    /**
     * 指定の式を数式として評価した結果を返す。
     * @param expression 対象の数式
     * @return 評価結果
     */
    public static BigDecimal eval(EvaluateContext evaluateContext, Expression expression) {
        if (evaluateContext == null) {
            throw new IllegalArgumentException("evaluateContext is null");
        }
        if (expression == null) {
            throw new IllegalArgumentException("expression is null");
        }
        return expression.accept(INSTANCE, evaluateContext);
    }
    
    public BigDecimal visitValue(Value model, EvaluateContext context) {
        return new BigDecimal(model.getToken());
    }

    public BigDecimal visitAdd(Add model, EvaluateContext context) {
    	BigDecimal first = model.getFirst().accept(this, context);
    	BigDecimal second = model.getSecond().accept(this, context);
        return first.add(second);
    }

    public BigDecimal visitSubtract(Subtract model, EvaluateContext context) {
        BigDecimal first = model.getFirst().accept(this, context);
        BigDecimal second = model.getSecond().accept(this, context);
        return first.subtract(second);
    }

    public BigDecimal visitMultiply(Multiply model, EvaluateContext context) {
    	BigDecimal first = model.getFirst().accept(this, context);
    	BigDecimal second = model.getSecond().accept(this, context);
        return first.multiply(second);
    }

    public BigDecimal visitDivide(Divide model, EvaluateContext context) {
    	BigDecimal first = model.getFirst().accept(this, context);
    	BigDecimal second = model.getSecond().accept(this, context);
        return first.divide(second, context.getScale(), context.getRoundingMode());
    }

    public BigDecimal visitParenthesized(Parenthesized model, EvaluateContext context) {
    	BigDecimal expression = model.getExpression().accept(this, context);
        return expression;
    }
    
    public BigDecimal visitPlus(Plus model, EvaluateContext context) {
    	BigDecimal operand = model.getOperand().accept(this, context);
        return operand.multiply(BigDecimal.ONE);
    }
    
    public BigDecimal visitMinus(Minus model, EvaluateContext context) throws NoThrow {
        BigDecimal operand = model.getOperand().accept(this, context);
        return operand.multiply(BigDecimal.valueOf(-1));
    }
}

たとえば、new Add(new Value("1"), new Value("2"))の場合は、

expression.accept(INSTANCE, evaluateContext);

で、Add#acceptからEvaluator#visitAddが呼ばれます。
visitAddでは、

BigDecimal first = model.getFirst().accept(this, context);
BigDecimal second = model.getSecond().accept(this, context);

となり、Value#acceptからEvaluator#visitValuleが呼ばれます。

        return first.add(second);

最終的にfirstに対してsecondをaddして足し算の完了です。
インタープリターはデザインパターンでもありますが、ビジターパターンで実装したほうが変更に対して強くできてよいですね。さすが 塾長 です!

ちなみに、visitDivideではEvaluateContextのプロパティを使ってBigDecimal#divideを実行します。

    public BigDecimal visitDivide(Divide model, EvaluateContext context) {
    	BigDecimal first = model.getFirst().accept(this, context);
    	BigDecimal second = model.getSecond().accept(this, context);
        return first.divide(second, context.getScale(), context.getRoundingMode());
    }

デフォルトでは、有効桁数は10桁で、丸め方式はHALF_UPです。

	public Calculator() {
		this(10, RoundingMode.HALF_UP);
	}