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); }