パソコン活用研究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活用研究目次