最終的に作りたいもの
普段見ている 1+2*(3+4)を15と解釈してくれるような計算機を作りたい。
今回の記事では文字列から数字や演算子などの部品に分ける実装について説明していきたいと思います。
文字列から数字を切り出す実装を書く
-
https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/char
文字列から数字だけを切り出すコードはこのようになります。static (int) getNumber(String input){ int number = 0; foreach (char i in input){ int digit = i - '0'; if (0 <= digit && digit < 10){ number = number* 10 + digit; }else{ break; } } return(number); }
この実装では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();
}
また、演算子をとりあえず追加しておいて、中身を実装しないみたいな形にしておくと、実装の見立てが立てやすく、これができるのも分離して処理を描く魅力と言えるでしょう。