【入門編】VB.NETで作る独自の簡易数式計算エンジン:DataTable.Computeの限界を突破する安全な構文解析パーサーの自社開発 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

こんにちは!開発現場の最前線で、日々レガシーとモダンを行き来しているシニア・アーキテクトです。

さて、業務アプリケーションを作っていると、こんな要望に必ずぶぶつかりますよね。
「ユーザーが画面上で自由に入力した数式(例: `単価 数量 (1 + 税率)`)を、プログラム側で動的に計算したい!」

VB.NET初学者の頃、あるいは手っ取り早く動かしたい時、多くの人が`System.Data.DataTable.Compute`メソッドという「魔法の杖」に手を出します。

.net
‘ よくある安易な実装
Dim table As New DataTable()
Dim result = table.Compute(“100 5 + 200”, “”)

これ、一見すると動くので便利に見えますが、実務の現場では「絶対に使ってはいけない禁断の技」です。なぜなら、パフォーマンスが絶望的に重いだけでなく、SQLインジェクションならぬ「式インジェクション」的な脆弱性を孕んでおり、最悪の場合、OSのコマンド実行すら許してしまうセキュリティホールになり得るからです。

「じゃあ、安全で高速な計算エンジンを自社で作るなんて、compiler(コンパイラ)の専門知識が必要なんじゃ……?」

いいえ、心配いりません!
今回は、ここをクリアすればVB.NETのオブジェクト指向やアルゴリズムの基本が完全に自分のものになる「自製・簡易数式計算エンジンの設計手法」を、優しく、そして本質的に解説していきます。

—

1. なぜ `DataTable.Compute` ではダメなのか?

まず、敵を知ることから始めましょう。
`DataTable.Compute` は内部で.NETの古い式評価エンジンを呼び出しています。そのため、以下の致命的なデメリットがあります。

1. 重すぎる(オーバーヘッド): 計算するたびに内部でパーサーやリフレクションが走り、ループ内で大量に呼ぶとアプリがフリーズします。
2. 型安全ではない: 結果が`Object`型で返るため、パースエラーのハンドリングが面倒です。
3. 拡張性ゼロ: 独自の関数(例: `ROUND` や `IF` など)を挟み込む余地がありません。

プロのエンジニアなら、ここで「自作のパーサー(構文解析器)」をサクッと組んでしまいます。「難しそう」と思うかもしれませんが、「数式をトークン(単語)に分解し、演算子の優先順位を考慮してツリー状に評価する」というルールさえ分かれば、VB.NETの基本機能だけで美しく実装できます。

—

2. 安全な計算エンジン:3つのステップ

私たちが目指す計算エンジンは、以下の3段階でデータを処理します。

1. 字句解析(Tokenizer): 入力された文字列(例: `3 + 5 2`)を、数値や演算子などの「意味のある部品(トークン)」に切り分ける。
2. 構文解析(Parser / 逆ポーランド記法など): 演算子の優先順位(掛け算・割り算を先にやる等)を考慮して並べ替える。今回は、最もシンプルで確実な「再帰下降パーサー(Recursive Descent Parser)」を採用します。
3. 評価(Evaluator): 組み立てたルールに従って実際の計算を行う。

ここをクリアできれば、VB.NETの「文字列操作」「コレクション」「例外処理」の基本はバッチリです!

—

3. 実装コード:スジのいい簡易数式パーサー

それでは、Windows Formsアプリのボタンクリックイベントなどからそのまま呼び出せる、実用的な数式計算クラスのコードをプレゼントしましょう。

プロジェクトに新しいクラス(例: `SimpleMathParser.vb`)を追加し、以下のコードを貼り付けてみてください。

.net
Imports System
Imports System.Collections.Generic

Public Class SimpleMathParser
Private _expression As String
Private _pos As Integer

”’

”’ 数式文字列を安全に評価して結果を返します
”’

Public Function Evaluate(expression As String) As Double
If String.IsNullOrWhiteSpace(expression) Then
Throw New ArgumentException(“計算式が空です。”)
End If

_expression = expression.Replace(” “, “”) ‘ 空白を全て除去
_pos = 0

‘ 四則演算の優先順位の底辺(足し算・引き算)からパースを開始
Dim result As Double = ParseExpression()

‘ まだ文字が残っている場合は構文エラー
If _pos < _expression.Length Then Throw New FormatException($"予期しない文字があります: '{_expression(_pos)}' (位置: {_pos})") End If Return result End Function ' 1. 足し算 (+) と 引き算 (-) を処理する階層 Private Function ParseExpression() As Double Dim value As Double = ParseTerm() While _pos < _expression.Length Dim op As Char = _expression(_pos) If op <> “+” AndAlso op <> “-” Then Exit While

_pos += 1 ‘ 演算子をスキップ
Dim nextValue As Double = ParseTerm()

If op = “+” Then
value += nextValue
Else
value -= nextValue
End If
End While

Return value
End Function

‘ 2. 掛け算 () と 割り算 (/) を処理する階層(優先度高)
Private Function ParseTerm() As Double
Dim value As Double = ParseFactor()

While _pos < _expression.Length Dim op As Char = _expression(_pos) If op <> “” AndAlso op <> “/” Then Exit While

_pos += 1 ‘ 演算子をスキップ
Dim nextValue As Double = ParseFactor()

If op = “” Then
value = nextValue
Else
If nextValue = 0 Then Throw New DivideByZeroException(“ゼロによる割算が検出されました。”)
value /= nextValue
End If
End While

Return value
End Function

‘ 3. 数値と括弧 ‘()’ を処理する最も基礎的な階層
Private Function ParseFactor() As Double
If _pos >= _expression.Length Then
Throw New FormatException(“式の途中で数式が途切れています。”)
End If

Dim c As Char = _expression(_pos)

‘ 括弧の処理: ‘(‘ から始まっていれば、中の式を再帰的に計算する
If c = “(” Then
_pos += 1 ‘ ‘(‘ をスキップ
Dim value As Double = ParseExpression()
If _pos >= _expression.Length OrElse _expression(_pos) <> “)” Then
Throw New FormatException(“閉じ括弧 ‘)’ が見つかりません。”)
End If
_pos += 1 ‘ ‘)’ をスキップ
Return value
End If

‘ 数値のパース処理
Dim startPos As Integer = _pos
While _pos < _expression.Length AndAlso (Char.IsDigit(_expression(_pos)) OrElse _expression(_pos) = "."c) _pos += 1 End While If startPos = _pos Then Throw New FormatException($"数値または括弧が期待される場所で不正な文字に出会いました: '{c}'") End If Dim numberStr As String = _expression.Substring(startPos, _pos - startPos) Dim result As Double If Double.TryParse(numberStr, result) Then Return result Else Throw New FormatException($"有効な数値ではありません: {numberStr}") End If End Function End Class ---

4. Windows Formsでの使い方と動作確認

作ったこのクラスを、例えばWindows Formsのボタンから次のように呼び出してみましょう。

.net
Private Sub btnCalculate_Click(sender As Object, e As EventArgs) Handles btnCalculate.Click
Dim parser As New SimpleMathParser()

Try
‘ ユーザーがテキストボックスに入力した数式
Dim userInput As String = txtFormula.Text ‘ 例: “(10 + 2) 3 – 4 / 2″

Dim answer As Double = parser.Evaluate(userInput)

MessageBox.Show($”計算結果: {answer}”, “成功”, MessageBoxButtons.OK, MessageBoxIcon.Information)

Catch ex As Exception
‘ ユーザーが変な数式を入力してもアプリがクラッシュしない!
MessageBox.Show($”計算エラー: {ex.Message}”, “入力エラー”, MessageBoxButtons.OK, MessageBoxIcon.Warning)
End Try
End Sub

このコードの「ここが美しい!」ポイント

  • 再帰呼び出し(Recursion)の活用: 括弧 `( )` が現れたとき、自分自身(`ParseExpression`)をもう一度呼び出すことで、何重の入れ子になった括弧(例: `(2 + (3 4))`)であっても綺麗に解決できます。
  • 安全な例外ハンドリング: ゼロ除算や括弧の閉じ忘れなどを`Try…Catch`で捕捉できるため、業務アプリとして極めて頑健(ロバスト)です。

—

まとめ

いかがでしょうか?
一見難しそうに見える「独自の数式計算エンジン」も、役割ごとにメソッドを分割する(関心の分離)オブジェクト指向の基本を意識すれば、たった100行足らずの美しいVB.NETコードで実装できてしまいます。

`DataTable.Compute` のような「黒魔術」に頼るのをやめ、自分でロジックをコントロールできるようになると、VB.NETを書く腕前は一段とプロフェッショナルな領域へと跳ね上がります。

ここをクリアしたあなたなら、どんな複雑な業務要件が来ても怖くありません。ぜひ、自社のアプリにこのエンジンを組み込んで、安全で爆速な計算処理を実現してみてくださいね!

タイトルとURLをコピーしました