続 : Javaで計算機作成(式記法変換)

この記事を書いた動機

前に書いたJavaの簡単な計算機(式記法変換編) - Mineneの物置で大変見苦しいコードを載せてしまったのでそのリベンジとして、見ていて気持ちの良い読みやすいコードを書きたくて、さらにそれを皆さんにお見せできればなと思いこの記事を書く。
なので、前の記事を見ていない人は見ることをお勧めします。(まあ見てもダメダメコードしか載ってませんが)


内容

計算機全体に必要な段取りと処理は前記事などで書いているのでここでは省略し、ここでは計算機の一部である中置記法の式(優先度は括弧による)を読み込み、のちに計算しやすいように後置記法に直すクラスの作成する。
中置記法から後置記法に直すことを二分木を作成し、その木をたどることで達成する。
前の記事ではやっつけ感のあるコードを書いてしまったので、今回は前回書いたコードをもう少し読みやすい丁寧なコードに書きかえる。
それと同時に、なぜそう書き直すのかを解説する。

前回からの更新点

・コメントの削除
・メソッドの分割
・アクセス修飾子によるクラスの役割の可視化

コード

import java.io.*;
import java.text.*;
import java.util.StringTokenizer;
import java.lang.NumberFormatException;

class Node{
    private String node;
    private String formula = "";
    private Node right = null;
    private Node left = null;

    Node(String str){
        this.node = str;
    }

    void createTree(){
        int height = 0;
        int opPlace = 0;
        char start;
        char finish;
        String tmp;

        removeSpace();

        if(node.length()>1){
            if(!correct()){
                System.out.println("式が正しくありません");
                System.exit(0);
            }
        }
        
        start = formula.charAt(0);
        finish = formula.charAt(formula.length()-1);
        
        if(start=='(' && finish==')'){
            removeOuterBracket();
        }

        opPlace = getOpPlace();
        
        if(opPlace==0){
            left = null;
            right = null;
            return;
        }
        left = new Node(formula.substring(0,opPlace));
        left.createTree();
        
        right = new Node(formula.substring(opPlace+1));
        right.createTree();
        
        node = formula.substring(opPlace,opPlace+1);
    }
    
    String traceTree(){
        String l = "";
        String r = "";
        if(left != null) l += left.traceTree()+" ";
        if(right != null) r += right.traceTree()+" ";
        return l + r + node;
    }

    private boolean correct(){
        char tmp;
        int numCount = 0;
        int charCount = 0;
        for(int i=0;i<node.length();i++){
            tmp = node.charAt(i);
            if(isNum(tmp)){
                numCount++;
            }else if(!(tmp=='(' || tmp==')')){
                charCount++;
            }
        }
        if(numCount==charCount+1) return true;
        return false;
    }

    private void removeSpace(){
        String tmp;
        StringTokenizer token;
        token = new StringTokenizer(node," ");
        while(token.hasMoreTokens()){
            tmp = token.nextToken();
            formula += tmp;
        }
    }

    private void removeOuterBracket(){
        int count = 0;
        int height = 0;
        char tmp;
        for(int i=0;i<formula.length();i++){
            tmp = formula.charAt(i);
            if(tmp=='(') height++;
            else if(tmp==')') height--;
            if(height==0) count++;
        }

		if(count==1) formula = formula.substring(1,formula.length()-1);
    }

    private int getOpPlace(){
        char tmp;
        int place = 0;
        int height = 0;
        for(int i=0;i<formula.length();i++){
            tmp = formula.charAt(i);
            if(tmp=='(') height++;
            else if(tmp==')') height--;
            else if((tmp=='+' || tmp=='-' || tmp=='*' || tmp=='/') && height==0){
                place = i;
                break;
            }
        }
        return place;
    }

    private boolean isNum(char num){
        String tmp = String.valueOf(num);
        try{
            Integer.parseInt(tmp);
            return true;
        }catch(NumberFormatException e){
            return false;
        }
    }
}


解説

コメントについて

更新点でも書いた通り今回のプログラムにはなんとコメントが一切書かれていない、なぜならコメントを書かずともメソッド名や変数名だけで読めばわかる(多分)コードになっているからだ。例えば"removeSpace"や"getOpPlace"を見ると、そこのメソッドでは何を行っているのかが一目でわかるようになっている。ただ、コメントが無さすぎると複数人で開発しているときに大変なので書くべきところには多少は書いておいた方がいい(無意味なのはダメ)。

メソッドの分割について

前のコードにはメソッドが"createTree"と"traceTree"の二つのみに分かれていたが、これではその中身を上から順にすべて読んでいかないと結局どのような処理で二分木を操作しているのかわからない。今回はメソッドを複数に分けた、これで上からコードを眺めるだけでcreateTreeは

  • >"空白を取り除き"
  • >"式が正しいか判別し"
  • >"一番外側の括弧を取り除き"
  • >"演算子の場所を得た後に"
  • >"ノードを作っている"ことがわかる。

コメントを書いても流れが一目でわかると言いたい人もいるだろうが、これはプログラムなのであって文章ではない。一目でわかる名前がついていればコメントなぞ邪魔なだけなのである。

アクセス修飾子について

今回メソッドを多数に分けて書いたわけだが、それぞれがどのような働きをし、どこから呼ばれうるのかそれぞれ一つずつ見ていては大変だ。そこでアクセス修飾子を使う。アクセス修飾子をつけているとそのクラス、メソッドの役割が明確にすることができ、他のところからうっかり使えないメソッドを呼べないようにすることができる。今回使われているのは同じクラスからしか呼ぶことができない"private"だけだが他にもpackage内から呼ばれうる"protected"などがある。mainメソッドに書く"public"もその一つだ。今回の例でいうとcreateTreeとtraceTreeは他の場所から呼ばれうるが他のメソッドはその二つのメソッドのどちらかからしか呼ばれることはないというのがわかる。ちなみに、アクセス修飾子を書かないと"protected"がコンパイルでつく。

おわりに

これで完璧とまでは言わないが前回より読む気になるコードになっていると思う。これを機にこれから各コードたちも美しく書いていけるように心がけたい。
メソッド分割やアクセス修飾子のところで可読性について偉そうに語っているがimportにワイルドカード( * )を使っている。ゆるして。
記事が長くなってしまうので詳しくは書かなかったが、前回から式の成立条件も更新しているのでぜひコードを読んでみてほしい。
最後に、参考図書としてコードを書き換えるにあたって使用した本を上げておく。
では、次の記事でまた会いましょう。

参考図書

・Brian W.Kernighan, Rob Pike (2003)『プログラミング作法』福崎俊博 訳,アスキー
・松浦佐江子 (2016)『ソフトウェア設計論ー役に立つUMLモデリングへ向けて』コロナ社

追記

あまりにもひどい処理(特に式が正しいかどうかの判定の部分)だったのでこちらに改善したコードを載せておきます。式が正しいかどうかの処理を書き換えているときに「(よくわかんないけど)これ構文規則から二分木作ると確実だな?」と思ったのですがせっかくひねり出したものを消すのももったいなくここで供養させていただきます。

package calculator;

import java.io.*;
import java.text.*;
import java.util.StringTokenizer;
import java.lang.NumberFormatException;

class Node{
    private String node;
    private String formula = "";
    private Node right = null;
    private Node left = null;

    Node(String str){
        this.node = str;
    }


    //前処理
    void mae(){
        String tmp = removeSpace();  //スペース除去

        for(int i=0;i<tmp.length();i++){
            char tnp;
            tnp = tmp.charAt(i);

            if(tnp=='-' && (i==0 
            || tmp.charAt(i-1)=='('
            || tmp.charAt(i-1)=='+'
            || tmp.charAt(i-1)=='-'
            || tmp.charAt(i-1)=='*'
            || tmp.charAt(i-1)=='/')){  //符号付き計算のマイナスの値があった時の処理
                formula += "0";
            }
            else if(tnp=='-' && i!=0){  //符号付き計算のマイナスの値があった時の処理
                formula += "+0";
            }

            formula += tnp;
        }
        node = formula;  //スペースを除去した式に更新
        return;
    }

    //中置記法から二分木を作成
    void createTree(){
        try{
            int opPlace = 0;  //演算子の位置
            char start;  //式のはじめ
            char finish;  //式の終わり
            int bflag = 1;  //()が外れているかどうかのフラグ

            System.out.println(formula);
            if(node.length()>0){
                if(!correct()){  //式が正しいかどうかの判定
                    System.out.println("式が正しくありません");
                    node = "";
                    return;
                    //System.exit(0);
                }
            }
            
            start = node.charAt(0);
            finish = node.charAt(node.length()-1);

            while(bflag==1){  //()を外す
                if(start=='(' && finish==')'){
                     bflag = removeOuterBracket();
                }
                else bflag = 0;
            }

            opPlace = getOpPlace();  //演算子の位置を取得
            
            if(opPlace==0){  //演算子がなければ数値
                left = null;
                right = null;
                return;
            }

            left = new Node(node.substring(0,opPlace));  //演算子の前半を再帰的に二分木作成
            left.createTree();
            
            right = new Node(node.substring(opPlace+1));  //演算子の後半を再帰的に二分木作成
            right.createTree();
            
            node = node.substring(opPlace,opPlace+1);  //このノードに演算子を挿入
        }catch(StringIndexOutOfBoundsException e){
            System.out.println("式を入力してください");
            //e.printStackTrace();
        }
    }
    
    //走査
    String traceTree(){
        String l = "";
        String r = "";
        if(left != null) l += left.traceTree()+" ";
        if(right != null) r += right.traceTree()+" ";
        return l + r + node;
    }

    //式が正しいかどうか
    private boolean correct(){
        char tmp1;
        int numCount = 0;
        int charCount = 0;
        int height = 0;
        char tmp;

        for(int i=0;i<node.length();i++){
            tmp = node.charAt(i);
            if(!isNum(tmp) 
                && tmp!='+'
                && tmp!='-'
                && tmp!='*'
                && tmp!='/'
                && tmp!='('
                && tmp!=')'){  //数値でなければこれらの記号のはず
                    return false;
            }
        }
        for(int i=0;i<node.length();i++){  //()の深さが正しいかどうか
            tmp = node.charAt(i);
            if(tmp=='(') height++;
            else if(tmp==')') height--;
        }
        if(height!=0) return false;

        int j;
        for(int i=0;i<node.length();i++){  //数値と演算子の数を取得
            tmp1 = node.charAt(i);
            if(isNum(tmp1)){
                numCount++;
                for(j=i+1;j<node.length();j++){  //i番目数値ならば記号まで読み飛ばし
                    tmp = node.charAt(j);
                    if(!isNum(tmp)){
                        tmp1 = tmp;
                        break;
                    }
                }
                i = j;
                
            }
            if(tmp1=='+'
            || tmp1=='-'
            || tmp1=='*'
            || tmp1=='/') charCount++;  //記号のうち演算子をカウント
        }
        if(numCount==charCount+1) return true;  //数値は演算子+1と同じ個数のはず
        return false;
    }

    //空白を除去する
    private String removeSpace(){
        String retmp="";
        String tmp;
        StringTokenizer token;
        token = new StringTokenizer(node," ");
        while(token.hasMoreTokens()){  //空白読み飛ばし
            tmp = token.nextToken();
            retmp += tmp;
        }
        return retmp;
    }

    //一番外側の()をはずす
    private int removeOuterBracket(){
        int count = 0;
        int height = 0;
        char tmp;
        for(int i=0;i<node.length();i++){
            tmp = node.charAt(i);
            if(tmp=='(') height++;
            else if(tmp==')') height--;
            if(height==0) count++;  //何回()が閉じられるか
        }

		if(count==1){  //一番外側同士で閉じられている
            node = node.substring(1,node.length()-1);
            return 1;
        }
        return 0;
    }

    //演算子の場所を取得
    private int getOpPlace(){
        char tmp;
        int place = 0;
        int height = 0;
        for(int i=node.length()-1;i>-1;i--){
            tmp = node.charAt(i);
            if(tmp==')') height++;
            else if(tmp=='(') height--;
            else if((tmp=='+' || tmp=='-' || tmp=='*' || tmp=='/') && height==0){  //深さが0の演算子の場所を後ろから取得
                place = i;
                break;
            }
        }
        return place;
    }

    //数値かどうか
    private boolean isNum(char num){
        String tmp = String.valueOf(num);
        try{
            Integer.parseInt(tmp);
            return true;
        }catch(NumberFormatException e){
            return false;
        }
    }
}