DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

DOBON.NET

文字列の計算式の計算結果を取得する

例えば、"(1+6)*5/(7-4)"のような計算式を表す文字列から、その計算結果を取得する様々な方法を紹介します。

自分で解析する方法

正攻法で行けば、計算式を解析し、計算するコードを自分で書くということになります。そのために参考になりそうなコードは、例えば、「A Math Expression Evaluator」です。この「A Math Expression Evaluator」は簡単な計算はもちろん、cos、sin、logなどにも対応しています。(ただし、コードはVB.NETのみです。)

さらに、掲示板でArAyさんが紹介されたMSDN Japanの「アルゴリズム入門:第1章 Visual C# による文字列処理入門」も参考になります。

このように自分でコードを書く以外の方法も様々あります。

JScript.NETのEvalを使用する方法

この方法は、GotDotNet Message Boardsの「String to Numeric」のスレッドでenderminhさんが紹介しています。

この方法によると、まず参照に「Microsoft.Jscript」と「Microsoft.Vsa」を追加し、次のようなコードにより、計算を実行します。

[VB.NET]
'計算式
Dim exp As String = "(1+6)*5/(7-4)"

Dim ve As Microsoft.JScript.Vsa.VsaEngine = _
    Microsoft.JScript.Vsa.VsaEngine.CreateEngine()
Dim result As Double = _
    CDbl(Microsoft.JScript.Eval.JScriptEvaluate(exp, ve))

'結果を表示
Console.WriteLine(result)
[C#]
//計算式
string exp = "(1+6)*5/(7-4)";

Microsoft.JScript.Vsa.VsaEngine ve =
    Microsoft.JScript.Vsa.VsaEngine.CreateEngine();
double result =
    (double) Microsoft.JScript.Eval.JScriptEvaluate(
        exp, ve);

//結果を表示
Console.WriteLine(result);

また、JScriptCodeProviderにより、Evalメソッドを実行する方法が「An Eval Function for C# using JScript.NET (JavaScript)」に紹介されています。

この方法によると、次のようなコードがかけます。

[VB.NET]
'Imports System.Reflection
'Imports System.CodeDom.Compiler
'がソースファイルの一番上に書かれているものとする

'計算式
Dim exp As String = "(1+6)*5/(7-4)"

'計算するためのコード
Dim [source] As String = _
    "package Evaluator {" + vbCrLf + _
    "class Evaluator {" + vbCrLf + _
    "public function Eval(expr : String) : String {" + vbCrLf + _
    "return eval(expr);}}}"

'コンパイルするための準備
Dim cp = New Microsoft.JScript.JScriptCodeProvider
Dim icc As ICodeCompiler = cp.CreateCompiler()
Dim cps As New CompilerParameters
Dim cres As CompilerResults
'メモリ内で出力を生成する
cps.GenerateInMemory = True
'コンパイルする
cres = icc.CompileAssemblyFromSource(cps, [source])

'コンパイルしたアセンブリを取得
Dim asm As [Assembly] = cres.CompiledAssembly
'クラスのTypeを取得
Dim t As Type = asm.GetType("Evaluator.Evaluator")
'インスタンスの作成
Dim eval As Object = Activator.CreateInstance(t)
'Evalメソッドを実行し、結果を取得
Dim result As String = CStr(t.InvokeMember("Eval", _
    BindingFlags.InvokeMethod, Nothing, eval, New Object() {exp}))

'結果を表示
Console.WriteLine(result)
[C#]
//using System.Reflection;
//using System.CodeDom.Compiler;
//がソースファイルの一番上に書かれているものとする

//計算式
string exp = "(1+6)*5/(7-4)";

//計算するためのコード
string source =
@"package Evaluator
{
    class Evaluator
    {
        public function Eval(expr : String) : String 
        { 
            return eval(expr); 
        }
    }
}";

//コンパイルするための準備
CodeDomProvider cp = new Microsoft.JScript.JScriptCodeProvider();
ICodeCompiler icc = cp.CreateCompiler();
CompilerParameters cps = new CompilerParameters();
CompilerResults cres;
//メモリ内で出力を生成する
cps.GenerateInMemory = true;
//コンパイルする
cres = icc.CompileAssemblyFromSource(cps, source);

//コンパイルしたアセンブリを取得
Assembly asm = cres.CompiledAssembly;
//クラスのTypeを取得
Type t = asm.GetType("Evaluator.Evaluator");
//インスタンスの作成
object eval = Activator.CreateInstance(t);
//Evalメソッドを実行し、結果を取得
string result = (string) t.InvokeMember("Eval",
    BindingFlags.InvokeMethod,
    null,
    eval,
    new object[] {exp});

//結果を表示
Console.WriteLine(result);

CSharpCodeProviderを使用した方法

さらに、掲示板でピラルクさんが提案されたように、CSharpCodeProviderを使って、計算式が含まれるコードの文字列をコンパイルし、実行する方法もあります。この方法は、「Runtime Compilation (A .NET eval statement)」や「Evaluating Mathematical Expressions by Compiling C# Code at Runtime」などでも紹介されています。

具体的なコードは、例えば、次のようなものです。

[VB.NET]
'Imports System.Reflection
'Imports System.CodeDom.Compiler
'がソースファイルの一番上に書かれているものとする

'計算するためのコード
Dim [source] As String = _
    "public class MainClass {" + vbCrLf + _
    "public static double EVal() {" + vbCrLf + _
    "return (1d+6d)*5d/(7d-4d);" + vbCrLf + _
    "}}"

'コンパイルするための準備
Dim cp = New Microsoft.CSharp.CSharpCodeProvider
Dim icc As ICodeCompiler = cp.CreateCompiler()
Dim cps As New CompilerParameters
Dim cres As CompilerResults
'メモリ内で出力を生成する
cps.GenerateInMemory = True
'コンパイルする
cres = icc.CompileAssemblyFromSource(cps, [source])

'コンパイルしたアセンブリを取得
Dim asm As [Assembly] = cres.CompiledAssembly
'MainClassクラスのTypeを取得
Dim t As Type = asm.GetType("MainClass")
'EValメソッドを実行し、結果を取得
Dim d As Double = CDbl(t.InvokeMember("EVal", _
    BindingFlags.InvokeMethod, Nothing, Nothing, Nothing))

'結果を表示
Console.WriteLine(d)
[C#]
//using System.CodeDom.Compiler;
//using System.Reflection;
//がソースファイルの一番上に書かれているものとする

//計算するためのコード
string source = @"
public class MainClass
{
    public static double EVal()
    {
        return (1d+6d)*5d/(7d-4d);
    }
}";

//コンパイルするための準備
CodeDomProvider cp = new Microsoft.CSharp.CSharpCodeProvider();
ICodeCompiler icc = cp.CreateCompiler();
CompilerParameters cps = new CompilerParameters();
CompilerResults cres;
//メモリ内で出力を生成する
cps.GenerateInMemory = true;
//コンパイルする
cres = icc.CompileAssemblyFromSource(cps, source);

//コンパイルしたアセンブリを取得
Assembly asm = cres.CompiledAssembly;
//MainClassクラスのTypeを取得
Type t = asm.GetType("MainClass");
//EValメソッドを実行し、結果を取得
double d = (double) t.InvokeMember("EVal",
    BindingFlags.InvokeMethod,
    null,
    null,
    null);

//結果を表示
Console.WriteLine(d);

前のJScript.NETのEvalを使った方法と比べると、この方法は計算式をコードに含めなければならず、計算式が変わるたびにコンパイルが必要になりますし、メモリのアセンブリをどのように解放するかという問題もあります(この問題は「Dynamically executing code in .Net」に詳しい)。そのため、実用としては困難かもしれません。

Microsoft Script Controlを使用した方法

Microsoft Script Controlが使用できるならば、VBScriptやJSCriptのEval関数を使う方法もあります。

[VB.NET]
'計算式
Dim exp As String = "(1+6)*5/(7-4)"

Dim t As Type = _
    Type.GetTypeFromProgID("MSScriptControl.ScriptControl")
Dim obj As Object = Activator.CreateInstance(t)
t.InvokeMember("Language", _
    System.Reflection.BindingFlags.SetProperty, _
    Nothing, _
    obj, _
    New Object() {"vbscript"})
'Eval関数で計算を実行して結果を取得
Dim result As Double = CDbl( _
    t.InvokeMember("Eval", _
        System.Reflection.BindingFlags.InvokeMethod, _
        Nothing, _
        obj, _
        New Object() {exp}))

'結果を表示
Console.WriteLine(result)
[C#]
//計算式
string exp = "(1+6)*5/(7-4)";

Type t = Type.GetTypeFromProgID("MSScriptControl.ScriptControl");
object obj = Activator.CreateInstance(t);
t.InvokeMember("Language",
    System.Reflection.BindingFlags.SetProperty,
    null,
    obj,
    new object[] {"vbscript"});
//Eval関数で計算を実行して結果を取得
double result = (double) t.InvokeMember("Eval",
    System.Reflection.BindingFlags.InvokeMethod,
    null,
    obj,
    new object[] {exp});

//結果を表示
Console.WriteLine(result);

その他の方法

これ以外にも、DataTable.Computeメソッドを使う方法など、まだまだありそうですが、きりが無いので、この辺で終わりにします。(面白い方法がありましたら、ご連絡ください。)

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • コードの先頭に記述されている「Imports ??? がソースファイルの一番上に書かれているものとする」(C#では、「using ???; がソースファイルの一番上に書かれているものとする」)の意味が分からないという方は、こちらをご覧ください。
  • 「???を参照に追加します」の意味が分からないという方は、こちらをご覧ください。