LoginSignup
1
0

More than 1 year has passed since last update.

四則演算パーサーを実装してみたのでその時に知ったことを書いてみる

Last updated at Posted at 2022-12-18

最終的に作りたいもの

普段見ている 1+2*(3+4)を15と解釈してくれるような計算機を作りたい。
今回の記事では文字列から数字や演算子などの部品に分ける実装について説明していきたいと思います。

文字列から数字を切り出す実装を書く

この実装ではChar型のしようをうまく使って  if i == '0' | i == '1'...というような実装を避けています。そのChar型の仕様とは

  • char型は指定しなくてもintなどの整数型になることができる(逆にintからも宣言すればchar型に変換可能)

実際Char型は16進数のためint digit = i - '0'業内のiでは以下の表の通りに解釈がされています。

16進数のコード 10進数換算 実際の数
EFBC90 FF10 263593149853952 0
EFBC91 FF11 263593149853953 1
EFBC92 FF12 263593149853954 2
EFBC93 FF13 263593149853955 3
EFBC94 FF14 263593149853956 4
EFBC95 FF15 263593149853957 5
EFBC96 FF16 263593149853958 6
EFBC97 FF17 263593149853959 7
EFBC98 FF18 263593149853960 8
EFBC99 FF19 263593149853961 9

Token列に分けるというアイデア

上記の実装でも確かに数字は取れますが、実際の電卓では文字を入力する →[+-*/]の入力がある→文字の入力がある
この動作の繰り返しによってできています。このそれぞれの区切り(Token)ごとに数字を分けると嬉しくなるでしょう。
具体的には以下のメリットを得ることができます。

  • 入力値を受ける処理とロジックの処理の分離
    • デバック時に入力の処理で止まったのか、コアロジックで止まったのかの原因が分かりやすくなる。
    • 改修の時に影響を受ける範囲を切り分けることができる。
public static List<string> torknize(String input){
    List<string> raw_tokens= new List<string>{}; 
    int x = 0;
    while(input.Length >= 1){
        if(input[x] == '+'){
            raw_tokens.Add(input[..x]);
            raw_tokens.Add("+");
            input = input[(x+1)..];
            x = 0;
        }else if(input[x] == '-'){
            raw_tokens.Add(input[..x]);
            raw_tokens.Add("-");
            input = input[(x+1)..];
            x = 0;

        }else{
            if(input.Length <= x+1){
                raw_tokens.Add(input);
                break;
            }
            x++;
        }
    }
}

ここでは似たような処理の繰り返しがありますね。
+やーといった記号を見つけるまで配列を確認していき、+やーができた時点で以下の2つが決定するのでそれらを一旦文字列として処理していきます。

  • +やーの前までは全て数字である。
  • +やーは完結するトークンである。

なぜ、ここで文字列にするのかそれは「処理を決定する」という処理と「それぞれの単語を区切る」という処理は区別しておくことでどちらに間違いがあっても区別しやすくなるからです。例えば
「123+-123」が動かなくなった時

[123,+,-,123]

と区切られたことが原因か

 [123,+-.123]

と区切られたことが原因かでは全く原因箇所が異なることになり、ごちゃごちゃにしてあると原因箇所を追うのも難しくなりますね。区切ることでデバックの容易性を担保しているのが上記のコードとなっているのです。

文字列としてのトークンわけが終わったところで次に、文字列から扱いやすいrecordというものの配列として分けていきます。

public abstract record Token();
public abstract record TokenSymble():Token;
public record TokenNumber(int Number):Token;
public record TokenPlus():TokenSymble;
public record TokenMinus():TokenSymble;

上記のように実態のある数字以外は処理を定義するためのレコードを立てておくことで型での強力なパターンマッチングを作ることができます。
先ほどのListにした関数を以下のコードに食わせることでトークンというレコードの配列に作り替えることができます。
これにより、のちの処理のパターンマッチングがより簡単につくることができます。

public static Token[] lex( List<string> input){
   List<Token> tokens = new List<Token>{};
   foreach(string raw_token in raw_tokens){
       switch(raw_token){
           case string  i when raw_token == "+" :
               Token t_p = new TokenPlus();
               tokens.Add(t_p);
               break;
           case string  i when raw_token == "-" :
               tokens.Add(new TokenMinus());
               break;
           case string  i when raw_token == "*" :
               break;
           case string  i when raw_token == "/" :
               break;
           case string  i when raw_token == "" :
               break;
           default:
               (int target, int _) = lexInt(raw_token);
               Token t_n = new TokenNumber(target);
               tokens.Add(t_n);
               break;
       }
   }
   return tokens.ToArray();
}

また、演算子をとりあえず追加しておいて、中身を実装しないみたいな形にしておくと、実装の見立てが立てやすく、これができるのも分離して処理を描く魅力と言えるでしょう。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0