Androidで電卓を作ってみる その1

 SableCC 3.2を使ってAndroidで電卓を作ってみようかと思います。これだとgrammerファイルを書き終わった後のコーディングは殆ど無いってレベルです。

簡単な仕様

  • 計算式を入力してボタンを押すと答えが表示される
  • 計算式は半角数字と半角演算記号だけ
  • 改行コードとタブ、半角スペースは無視する。
  • 数値は整数系32bit
  • 使える演算子は以下
    • () 優先順位の変更
    • +-*/ の二項演算子

SableCCのgrammarファイル

ソースファイル

Package org.x68k.android.scalc.parser;
//------+-------+-------+-------+-------+-------+-------+-------+-------+-------
Helpers /* Regular Expressions */
    cr          = 0x000D;               // carriage return
    lf          = 0x000A;               // line feed
    ht          = 0x0009;               // horizontal tab
    space       = 0x0020;               // space
    digit       = ['0'..'9'];           // digit

//------+-------+-------+-------+-------+-------+-------+-------+-------+------
States
    normal;

//------+-------+-------+-------+-------+-------+-------+-------+-------+------
Tokens /* Lexer */
    number      = digit+;               // decimal number.
    l_paren     = '(';                  // left parenthesis
    r_paren     = ')';                  // right parenthesis
    add         = '+';                  // addition operator
    sub         = '-';                  // subtraction operator
    mul         = '*';                  // multiplication operator
    div         = '/';                  // division operator

    blank       = ht | space;           // blank
    eol         = cr lf | cr | lf;      // end of line

//------+-------+-------+-------+-------+-------+-------+-------+-------+------
Ignored Tokens
    blank, eol;

//------+-------+-------+-------+-------+-------+-------+-------+-------+------
Productions /* parser definitions */

expr =
   {term} term
 | {add}  [left]:expr add [right]:term
 | {sub}  [left]:expr sub [right]:term
;

term =
   {factor} factor
 | {mul}    [left]:term mul [right]:factor
 | {div}    [left]:term div [right]:factor
;

factor =
   {number} number
 | {parens} l_paren expr r_paren
; 

解説

Package

 Parserのクラスを出力するJavaのパッケージを指定。

Helpers

 ここでは改行コードとか半角とか数字を定義しています。これは後でTokens節で使います。

States

 今回は特に語句の解析を状況によって切り替えたりしないので、normalだけ定義して終わり。

Tokens

 ここで定義した単語が一つずつ認識されてプログラムから利用する事が可能になります。

 連なった数字を見つけたらnumberという語句として認識。digitはHelpersで定義した正規表現を利用しています。解析結果のTreeではTNumberというクラスのオブジェクトとして現れます。

 l_parenとr_paren。括弧です。
この場合クラス名はアンダースコアの直後の文字も大文字になりますのでTLParenとTRParenというクラスになります。

 add,sub,mul,div。加減乗除の演算子です。
それぞれTAdd,TSub,TMul,TDiv。

 blank,eol。それぞれスペース、タブと改行です。

 という事でこの文法ファイルでは上記が入力対象として認識できる語句という事になります。ここで定義されていないような文字が出てきたらエラーになります。

Ignored Tokens

 blankとeolは特に文法上は必要ない語句です。ここで指定された語句は入力の中で見つけても何もせず捨てられるようになります。

Productions

 ここから文法を定義します。

  • 入力されるのは一番最初に書いてある「式(Expression)」です。
  • 式 PExprは、「ただ一つの項である」ATermExpr、「式と項の足し算」AAddExpr、「式と項の引き算」ASubExprです。
  • 項 PTermは、「ただ一つの要素」AFactorTerm、「項と要素の掛け算」AMulTerm、「項と要素の割り算」ADivTermです。
  • 要素 PFactorは、「ただの数字」ANumberFactor、「括弧で括られた式」AParensFactorのどちらかです。

Parserの生成

 これで入力文章からCSTを作成するParserを生成することができます。sableccのコマンドを使ってソースを生成します。

C:\android\workspace\Scalc\parser\sablecc scalc.sablecc3

SableCC version 3.5
Copyright (C) 1997-2012 Etienne M. Gagnon <egagnon @j-meg.com> and
others.  All rights reserved.

This software comes with ABSOLUTELY NO WARRANTY.  This is free software,
and you are welcome to redistribute it under certain conditions.

Type 'sablecc -license' to view
the complete copyright notice and license.


 -- Generating parser for scalc.sablecc3 in C:\android\workspace\Scalc\parser
Adding productions and alternative of section AST.
Verifying identifiers.
Verifying ast identifiers.
Adding empty productions and empty alternative transformation if necessary.
Adding productions and alternative transformation if necessary.
computing alternative symbol table identifiers.
Verifying production transform identifiers.
Verifying ast alternatives transform identifiers.
Generating token classes.
Generating production classes.
Generating alternative classes.
Generating analysis classes.
Generating utility classes.
Generating the lexer.
 State: NORMAL
 - Constructing NFA.
..........................
 - Constructing DFA.
.........................................
.............
 - resolving ACCEPT states.
Generating the parser.
................
................
................
..
................

Parserの利用

 生成されたParserを使う場合は以下のようにします。

Parser parser = new Parser(new Lexer(new PushbackReader(new StringReader("1+1*(1+2)"))));
Start start = parser.parse();
start.apply(ここにVisitorクラスのインスタンスを渡す);

 PushbackReaderからLexer、Parserとインスタンスを作り、その後parseを実行してTreeの開始点となるStartを得ます。
木をたどるVisitorをStartのインスタンスにapplyする事で構文木を処理できます。

Visitorの実装

 Visitorはanalysis.DepthFirstAdapter継承して作ります。

 Visitorは勝手に木をたどっていきますので、Visitorのクラスで実装すべき事は与えられたノードの出入りで必要な処理を記述するだけです。
具体的にはinAProduction(AProduction node);とoutAProduction(AProduction node);という感じの関数がすでに定義されているので、それらをOverrideしていきます。

 この簡易電卓を実現するのに必要な実装は以下だけです。

  • 最終結果を格納して取り出すためのインターフェースを準備する
  • 計算結果を途中結果格納するためのStackを準備する
  • 数字ANumberFactorを見つけたら(inでもoutでも良い)値をStackにPush
  • 演算子のノードから出るタイミング(たとえば掛け算ならoutAMulTerm)で、Stackから2つ取り出して計算して結果をスタックに積む。

ソース

package org.x68k.android.scalc;

import java.util.ArrayDeque;

import org.x68k.android.scalc.parser.analysis.DepthFirstAdapter;
import org.x68k.android.scalc.parser.node.AAddExpr;
import org.x68k.android.scalc.parser.node.ADivTerm;
import org.x68k.android.scalc.parser.node.AMulTerm;
import org.x68k.android.scalc.parser.node.ANumberFactor;
import org.x68k.android.scalc.parser.node.ASubExpr;
import org.x68k.android.scalc.parser.node.Start;

public class IntegerCalculator extends DepthFirstAdapter implements Calculator {
    private ArrayDeque<Integer> stack = new ArrayDeque<Integer>();

    private String result;

    // 二項演算子の操作インターフェース
    static interface BinaryOperator<T> {
        T op(T left, T right);
    }

    // 二項演算子の実行ルーチン
    private void operation(BinaryOperator<Integer> op) {
        final Integer right = stack.pop();
        final Integer left = stack.pop();

        stack.push(op.op(left, right));
    }

    // 各演算子の動作を定義
    private final static BinaryOperator<Integer> ADD = new BinaryOperator<Integer>() {
        @Override
        public Integer op(Integer left, Integer right) {
            return left + right;
        }
    };

    private final static BinaryOperator<Integer> DIV = new BinaryOperator<Integer>() {
        @Override
        public Integer op(Integer left, Integer right) {
            return left / right;
        }
    };

    private final static BinaryOperator<Integer> MUL = new BinaryOperator<Integer>() {
        @Override
        public Integer op(Integer left, Integer right) {
            return left * right;
        }
    };

    private final static BinaryOperator<Integer> SUB = new BinaryOperator<Integer>() {
        @Override
        public Integer op(Integer left, Integer right) {
            return left - right;
        }
    };

    // 演算処理を見つけたときの処理
    @Override
    public void outAAddExpr(AAddExpr node) {
        operation(ADD);
    }

    @Override
    public void outADivTerm(ADivTerm node) {
        operation(DIV);
    }

    @Override
    public void outAMulTerm(AMulTerm node) {
        operation(MUL);
    }

    @Override
    public void outASubExpr(ASubExpr node) {
        operation(SUB);
    }

    // 数字を見つけたときの処理
    @Override
    public void outANumberFactor(ANumberFactor node) {
        String s = node.getNumber().getText();
        stack.push(Integer.parseInt(s));
    }

    // 入力をすべて処理し終わったときの処理
    @Override
    public void outStart(Start node) {
        setResult(stack.pop().toString());
    }

    // 結果を格納するプロパティ
    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

}

 Parser部分の実装はほぼこれで終了です。ちょっと長くなったので、今回はここまでにして次回は これをGUIと紐づけてやって電卓を完成させます。

Leave a Reply

Your email address will not be published. Required fields are marked *