パソコン活用研究5番街(Visual
Basic、Excel(VBA)、BASIC プログラミング研究)
VB.NETで計算機を作る(とりあえず完成形2)
〜異常値入力の処理
(準備中)
前回(「VB.Netで計算機を作る(とりあえず完成形)」)では、小数にも対応し、べき乗も対応
できるようにしてみましたが、異常値の入力チェックは大甘なままでした。。
最後に総仕上げとして、異常入力値への対処もきちんとしてみます。
前回までの字句解析のルールは以下のようでした。
空欄のところは何も処理をしていないので、この個所で異常値の入力があってもそのままスルーしています。
isnum=0 | isnum=1 | isnum=2 | isnum=3 | |
数値0〜9 | 数値の始まりとして認識し、 isnum=1に変更する |
数値の続きとして処理 | 小数部が始まったとして処理。 isnum=3とする。 |
数値の続きとして処理 |
- | 負の記号として認識し、数値の始まりとして isnum=1 に変更する |
数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算してisnum=0に変更 |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算してisnum=0に変更 |
+ * / ^ | 数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算してisnum=0に変更 |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算してisnum=0に変更 |
|
( | (としてトークン化し、posを1つ加算する。 | err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
||
) | 数値の終了とみなし、posをひとつ加算する。 )としてトークン化する |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
数値の終了とみなし、posをひとつ加算する。 )としてトークン化する |
|
小数点 ( . ) | 小数点として処理 isnum=2とする |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
|
空白 | 読み飛ばす | 読み飛ばす | 読み飛ばす | 読み飛ばす |
その他記号 文字 |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
例えば、以下のような異常値(式)の入力があってもエラーにしていません。
空白は読み飛ばしているので、
1 2
のように数字の間に空白があっても、"12"という数字としてトークン化する。
演算子が連続して現れると、2個目以降の演算子は読み飛ばす。
このため、3+*5 は3+5 と解析して演算しています。
右括弧")"が余計にあってもエラーにならない。
(1))
はノーチェックで1という演算結果が出力される。
右括弧")"の後に演算子記号が続いて終了する式がエラーにならない。
2+3)*
は5という演算結果が出力され、特に式としてエラー判定していない。
左括弧"("の後に数字が続いて終わる場合
1+(2
演算処理の関数primary_expression()の中で、右括弧")"で閉じらていないことはチェックしているが
字句解析部分ではノーチェック
せっかくなので、仕上げとして異常値(式)が入力された時はエラー判定するようにしてみます。
エラー処理は、字句判定の部分で行うこともできますし、演算部(構文解析部)に実装することもできます。
ここでは、お勉強目的なので、両方で実装してみます。
異常値(式)のチェックのことは全く考えずにここまで作ってきたので、異常値(式)のチェックは案外面倒くさい
ことになってます。かなりスパゲッティな感じになってます。
異常値(式)のチェックまで考えた場合は、字句解析のルールはもう少し工夫したほうが、よかったです。
そうしたらもっとシンプルでスマートなエラーチェックになったでしょう。
なお、「プログラミング言語を作る」の本にあるサンプルプログラムでは結構シンプルかつスマートな
エラー処理をしています。
字句解析部のルールは以下の通りにしました。
isnum=0 | isnum=1 | isnum=2 | |
数値0〜9 | 数値の始まりとして認識し、 isnum=1に変更する |
数値の続きとして処理 | 小数部が始まったとして処理。 isnum=3とする。 |
- | 負の記号として認識し、数値の始まりとして isnum=1 に変更する |
数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算してisnum=0に変更 |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
+ * / ^ | トークンのtypeにtype.badを設定 err=1 Exit For |
数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算して、次のトークンのtypeにtype.badを設定 isnum=0に変更 |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
( | lp=lp*1 (としてトークン化し、posを1つ加算する。 |
トークンのtypeにtype.badを設定 err=1 Exit For |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
) | トークンのtypeにtype.badを設定 err=1 Exit For |
rp=rp+1 数値の終了とみなし、posをひとつ加算する。 )としてトークン化する isnum=5 |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
小数点 ( . ) | トークンのtypeにtype.badを設定 err=1 Exit For |
小数点として処理 isnum=2とする |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
空白 | 読み飛ばす | isnum=5 | err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
その他記号 文字 |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
isnum=3 | isnum=5 | |
数値0〜9 | 数値の続きとして処理 | トークンのtypeにtype.badを設定 err=1 Exit For |
- | 数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算してisnum=0に変更 |
数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算して、次のトークンのtypeにtype.badを設定 isnum=0に変更 |
+ * / ^ | 数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算して、次のトークンのtypeにtype.badを設定 isnum=0に変更 |
数値が終了したとみなし、posを一つ加算し、 演算子としてトークン化する。更にposを一つ 加算して、次のトークンのtypeにtype.badを設定 isnum=0に変更 |
( | トークンのtypeにtype.badを設定 err=1 Exit For |
トークンのtypeにtype.badを設定 err=1 Exit For |
) | rp=rp+1 数値の終了とみなし、posをひとつ加算する。 )としてトークン化する isnum=5 |
rp=rp+1 数値の終了とみなし、posをひとつ加算する。 )としてトークン化する isnum=5 |
小数点 ( . ) | err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
トークンのtypeにtype.badを設定 err=1 Exit For |
空白 | isnum=5 | 読み飛ばす |
その他記号 文字 |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
err=1(エラーフラグを立て) Exit For(Forループを抜ける) |
このルールで字句解析の個所でほとんどの異常値(式)入力を検出してエラーにできますが、
")"の閉じ忘れや括弧の数の不整合はは検出しません。
このため、")"が多い場合はForループの最後の行で
If lp < rp Then Console.WriteLine("too many rparen") : err = 1 : Exit For
としてチェックしています。
また、")"が少ない(閉じ忘れ)はForループ内でへ検出できないので、Forループから抜けた後に
lpとrpを比較して検出しています。
If lp <> rp Then Console.WriteLine("The number of parentheses does not match.") : err = 1
なお、ほとんどの異常値(式)入力には対応できるようになっていると思いますが、
想定してない異常値(式)のために、VBがエラーを起こす可能性もあるため、字句解析部分を
Try ...... Catch
End Try
を使って、VBの起こすエラーも捕捉するようにしています。
変更した字句解析のコードです。
pos = 0 : isnum = 0 For i = 0 To statement.Length - 1 Try r = checktype(statement.Chars(i)) If (isnum = 0 Or isnum = 5) And r = type.space Then GoTo L1 : If (isnum = 1 Or isnum = 3) And r = type.space Then isnum = 5 : GoTo L1 : If r = type.bad Then Console.WriteLine("{0}:Bad character", i) : tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = type.bad : err = 1 : Exit For If (isnum = 1 Or isnum = 3) And r = type.number Then tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).v = Double.Parse(tok(pos).str) : GoTo L1 If isnum = 1 And r = type.dot Then tok(pos).str = tok(pos).str + statement.Chars(i) : isnum = 2 : GoTo L1 If (isnum = 1 Or isnum = 3 Or isnum = 5) And r = type.rparen Then rp = rp + 1 : pos = pos + 1 : tok(pos).str = statement.Chars(i) : tok(pos).type = r : isnum = 5 : GoTo L1 If (isnum = 1 Or isnum = 3 Or isnum = 5) And (r = type.add_op Or r = type.sub_op Or r = type.mul_op Or r = type.div_op Or r = type.pow_op) Then pos = pos + 1 : tok(pos).str = statement.Chars(i) : tok(pos).type = r : pos = pos + 1 : tok(pos).type = type.bad : isnum = 0 : GoTo L1 If isnum = 2 And r = type.number Then tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).v = Double.Parse(tok(pos).str) : isnum = 3 : GoTo L1 If isnum = 2 And r <> type.number Then err = 1 : Console.WriteLine("{0}: Bad expression", i) : Exit For If isnum = 3 And r = type.dot Then err = 1 : Console.WriteLine("{0}: too many dot", i) : tok(pos).type = type.bad : Exit For If isnum = 0 And r = type.lparen Then lp = lp + 1 : tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = r : pos = pos + 1 : GoTo L1 If isnum = 0 And r = type.number Then tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = type.number : tok(pos).v = Integer.Parse(tok(pos).str) : isnum = 1 : GoTo L1 If isnum = 0 And r = type.sub_op Then tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = type.number : isnum = 1 : GoTo L1 Console.WriteLine("{0}:Bad expression", i) : tok(pos).str = tok(pos).str + statement.Chars(i) : tok(pos).type = type.bad : err = 1 : Exit For Catch ex As System.FormatException err = 1 Console.WriteLine("System.FormatException") Exit For End Try L1: If lp < rp Then Console.WriteLine("too many rparen") : err = 1 : Exit For Next If lp <> rp Then Console.WriteLine("The number of parentheses does not match.") : err = 1
次に、演算部部分(再帰下降パーサ)の個所でもエラーチェックを入れてみました。
関数term()の中では、
0による除算のチェックをしています。
なお、VB.Netでは0で割った場合、エラーを出さず∞という演算結果を返すので、ここでも
エラーフラグは立てず、divided by zero という警告を出すにとどめています。
If t.type = type.div_op And v2 = 0 Then Console.WriteLine("divided
by zero")
他のプログラム言語は0による割り算はエラーを発生するケースが多いので、そういう言語ではエラーとして処理すればよいでしょう。
関数primary_expression()
正常な式が入力されれば、関数primary_expressionはトークンとしては
数値(type.number)か左括弧"("、右括弧")"しか処理しないはずです。、
従って、それらの正常なトークンが来た場合はそれぞれの処理の最後に正常な値をリターン
しますが、それ以外のトークンが来た場合は最終行で、全てエラーとして処理しています。
関数の最後の行のエラー処理では、
err=err+10 として、エラーフラグに10を足しています。
これは字句解析の個所で起きたエラーフラグは1、演算部分(再帰下降パース)で検出したエラーは10以上とすることで
どこでおきたエラーかを後で判別するためです。
また、左括弧"("の処理が終わった後に、次のトークンとして右括弧")"が現れるかチェックしています。
If t.type = type.lparen Then
v = expression()
t = get_token()
If t.type = type.rparen Then Return v Else Console.WriteLine("rparen
is missing")
End If
右括弧")"が現れれば正常に値vをリターンしますが、
右括弧")"以外が現れた場合は、rparen is missing と表示して
関数の最後の行にあるエラー処理に移行します。
変更した関数primary_expressionです。
Function primary_expression() As Double Dim t As token Dim v As Double t = get_token() Console.WriteLine("primary_expression {0}: {1} : {2} ", t.str, t.type, t.v) If t.type = type.number Then Return t.v If t.type = type.lparen Then v = expression() t = get_token() If t.type = type.rparen Then Return v Else Console.WriteLine("rparen is missing") End If unget_token() : err = err + 10 : Console.WriteLine("illegal expression") : Return 0 End Function
以上、字句解析と演算部の両方にエラー処理を実装しましたが、ほとんどの異常値(式)の入力は、どちらのエラー処理でも
捕捉します。
比較すると、演算部のエラー処理のほうが少ないチェックで(ほとんどprimary_expressionの最終行だけで)処理できてしまいますので、
こちらにほうが洗練されたエラーチェックかもしれません。
また、字句解析をうまく設計しておくと、字句解析部でのエラーチェックでは複雑になりすぎるのでスルーしているチェックを演算部では
シンプルにチェックできそうです。
本ページの最初に例示した入力例でどう判定されるか試してみます。
1 2
数値のの間に空白がはさまるケース
字句判定の個所でも、演算部の個所でもエラー判定されます。
3+*5
演算子が連続しているケース
*の個所で、字句解析部でも演算部でもエラーとしている。
(1)) のように( )の対応に不整合のあるケース
( )の対応がとれていないことを検出した場合はエラーとします。
2+3)*のようなケース
字句解析部で、右括弧")"が多いことを検出した部分で、エラーとし処理を中止
字句解析部でエラーとし、最終的に括弧の不整合で再度エラー判定しています。
(2+3)*のようなケース
字句解析部ではエラーフラグはたたないが、演算部でエラーにしている
1+(2
字句解析部でも演算部でも括弧の不整合を検出しエラーとしている。
一応、異常値(式)入力に対するエラーチェックも出来たように思いますが、勉強用に試行錯誤で作成してきた
プログラムなので、出来の悪いプログラムです。
出来の悪いプログラムの例としてご参照下さい。
TopPage > Visual BasIc&Excel活用研究目次