C# で文字列の式から計算結果を得る

# はじめに

例えば IoT ゲートウェイでセンサー値を集めてサーバに送信する際、値をそのまま送るのではなく、**事前に定義しておいた式に値を代入して計算した結果を扱いたい** 場合があります。（少なくとも僕には 😅

この IoT ゲートウェイには、任意のタイミングで任意の計算式を設定画面から登録できれば便利です。でも、この機能を実装するにあたり、登録された計算式を自分でパースするのはかなり危険な香りがします、、

そこで、**「文字列の式から計算結果を得る」** ために、いくつかライブラリを候補に挙げ、特徴を比較したり、ベンチマークをとったりして、どれがよさそうか検証してみます。

# 候補のライブラリ

今回は .NET Framework や OS に依存せず、.NET 8 環境で利用できる下記５件を候補に挙げました。

1. [ClosedXml](https://www.nuget.org/packages/ClosedXML/)
2. DataTable
3. [IronPython](https://www.nuget.org/packages/IronPython/)
4. [NCalc](https://www.nuget.org/packages/NCalcSync/)
5. [xFunc](https://www.nuget.org/packages/xFunc.Maths/)

1 の ClosedXml は僕は普段からよくお世話になっていて、スプレッドシートのセルで計算式をよきにはからってくれそうなので候補に上げました。

2 の DataTable は [こちらのページ](https://dobon.net/vb/dotnet/programing/eval.html) で紹介されていて、外部ライブラリに依存せず利用できて実績もありそうです。

3 ～ 5 は Gemini に教えてもらいました。

# 特徴の比較

[こちらのページ](https://dobon.net/vb/dotnet/programing/eval.html) で検証されている「`(1 + 6) * 5 / (7 - 4)`」の四則演算に加えて、下記２件の演算についても比較します。

- `%`：剰余演算
- `^`：べき乗演算
  - `^` は C# では「排他的論理和演算子」ですが、べき乗を扱いたいです。僕は

## 特徴の検証

下記コードを用意して実行してみました。

```C#
using ClosedXML.Excel;
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
using NCalc;
using System.Data;
using xFunc.Maths;

var c = new Calculator();

var formula = "(1 + 6) * 5 / (7 - 4)";
Console.WriteLine($"----- {formula}");
Console.WriteLine($"0. Primitive:  {(double)(1 + 6) * 5 / (7 - 4)}");
Console.WriteLine($"1. ClosedXml:  {c.Calc_ClosedXml(formula)}");
Console.WriteLine($"2. DataTable:  {c.Calc_DataTable(formula)}");
Console.WriteLine($"3. IronPython: {c.Calc_IronPython(formula)}");
Console.WriteLine($"4. NCalc:      {c.Calc_NCalc(formula)}");
Console.WriteLine($"5. xFunc:      {c.Calc_xFunc(formula)}");

formula = "11 % 3";
Console.WriteLine($"----- {formula}");
Console.WriteLine($"0. Primitive:  {11 % 3}");
Console.WriteLine($"1. ClosedXml:  {c.Calc_ClosedXml(formula)}");
Console.WriteLine($"2. DataTable:  {c.Calc_DataTable(formula)}");
Console.WriteLine($"3. IronPython: {c.Calc_IronPython(formula)}");
Console.WriteLine($"4. NCalc:      {c.Calc_NCalc(formula)}");
Console.WriteLine($"5. xFunc:      {c.Calc_xFunc(formula)}");

formula = "7 ^ 2";
Console.WriteLine($"----- {formula}");
Console.WriteLine($"0. Primitive:  {7 ^ 2}");
Console.WriteLine($"1. ClosedXml:  {c.Calc_ClosedXml(formula)}");
Console.WriteLine($"2. DataTable:  {c.Calc_DataTable(formula)}");
Console.WriteLine($"3. IronPython: {c.Calc_IronPython(formula)}");
Console.WriteLine($"4. NCalc:      {c.Calc_NCalc(formula)}");
Console.WriteLine($"5. xFunc:      {c.Calc_xFunc(formula)}");

Console.ReadLine();

public class Calculator
{
	// for ClosedXml
	private readonly IXLWorksheet _sheet;

	// for DataTable
	private readonly DataTable _table = new();

	// for IronPython
	private readonly ScriptEngine _engine = Python.CreateEngine();

	// for xFunc
	private readonly Processor _processor = new();

	/// <summary>
	/// コンストラクタ
	/// </summary>
	public Calculator()
	{
		// for ClosedXml
		var book = new XLWorkbook();
		book.AddWorksheet();
		_sheet = book.Worksheets.Worksheet(1)!;
	}

	public dynamic Calc_ClosedXml(string formula)
	{
		try
		{
			_sheet.Cell("A1").FormulaA1 = formula;
			return _sheet.Cell("A1").Value.GetNumber();
		}
		catch (Exception ex) { return ex.Message; }
	}

	public dynamic Calc_DataTable(string formula)
	{
		try { return _table.Compute(formula, null); }
		catch (Exception ex) { return ex.Message; }
	}

	public dynamic Calc_IronPython(string formula)
	{
		try { return _engine.Execute($"eval('{formula}')"); }
		catch (Exception ex) { return ex.Message; }
	}

	public dynamic Calc_NCalc(string formula)
	{
		try { return new Expression(formula).Evaluate(); }
		catch (Exception ex) { return ex.Message; }
	}

	public dynamic Calc_xFunc(string formula)
	{
		try { return _processor.Solve(formula).Number.Number; }
		catch (Exception ex) { return ex.Message; }
	}
}

```

## 四則演算の結果

```
----- (1 + 6) * 5 / (7 - 4)
0. Primitive:  11.666666666666666
1. ClosedXml:  11.666666666666666
2. DataTable:  11.666666666666666
3. IronPython: 11.666666666666666
4. NCalc:      11.666666666666666
5. xFunc:      11.666666666666666
```

すべて同じ結果でした 👍

## 剰余演算の結果

```
----- 11 % 3
0. Primitive:  2
1. ClosedXml:  Unable to parse formula '11 % 3':
Location 0:5 - Syntax error, expected: %, ^, *, /, +, -, &, >, =, <, <>, >=, <=
2. DataTable:  2
3. IronPython: 2
4. NCalc:      2
5. xFunc:      2
```

「ClosedXml」のみ対応していませんでした。
なお、下記のようにスプレッドシートの関数を使えば剰余演算も正しい値を得ることができました。

```C#
Console.WriteLine($"1. ClosedXml:  {c.Calc_ClosedXml("MOD(11, 3)")}");
```

```
1. ClosedXml:  2
```

## べき乗演算の結果

```
----- 7 ^ 2
0. Primitive:  5
1. ClosedXml:  49
2. DataTable:  The expression contains unsupported operator '^'.
3. IronPython: 5
4. NCalc:      5
5. xFunc:      49
```

僕が欲しかった「7 の 2乗 = 49」の結果を返してくれたのは「ClosedXml」と「xFunc」でした。
「DataTable」は `^` 演算子に対応していませんでした。
「IronPython」と「NCalc」（と C# での計算結果）では、`0111(2) XOR 0010(2) = 0101(2)` というように排他的論理和の結果を返してくれました[^1]。
[^1]: IronPythonとNCalcでは、べき乗には `7 ** 2` のように `**` 演算子が用意されています。もしくは、IronPythonでは `pow(7, 2)`, NCalcでは `Pow(7, 2)` のように関数を使うこともできます。

# ベンチマーク

[BenchmarkDotNet](https://www.nuget.org/packages/BenchmarkDotNet/) を使って処理速度とメモリ効率を確認するために、上記で使ったプログラムを次のように修正しました。

```C#
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using ClosedXML.Excel;
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
using NCalc;
using System.Data;
using xFunc.Maths;

// ベンチマークスタート
BenchmarkRunner.Run<Calculator>();
Console.ReadLine();

[MemoryDiagnoser]
[ShortRunJob]
public class Calculator
{
	// 評価に利用する計算式
	private const string FORMULA = "(1 + 6) * 5 / (7 - 4)";

	// for ClosedXml
	private readonly IXLWorksheet _sheet;

	// for DataTable
	private readonly DataTable _table = new();

	// for IronPython
	private readonly ScriptEngine _engine = Python.CreateEngine();

	// for xFunc
	private readonly Processor _processor = new();

	/// <summary>
	/// コンストラクタ
	/// </summary>
	public Calculator()
	{
		// for ClosedXml
		var book = new XLWorkbook();
		book.AddWorksheet();
		_sheet = book.Worksheets.Worksheet(1)!;
	}

	[Benchmark]
	public dynamic Calc_Primitive()
		=> (decimal)(1 + 6) * 5 / (7 - 4);

	[Benchmark]
	public dynamic Calc_ClosedXml()
	{
		_sheet.Cell("A1").FormulaA1 = FORMULA;
		return _sheet.Cell("A1").Value.GetNumber();
	}

	[Benchmark]
	public dynamic Calc_DataTable()
		=> _table.Compute(FORMULA, null);

	[Benchmark]
	public dynamic Calc_IronPython()
		=> _engine.Execute($"eval('{FORMULA}')");

	[Benchmark]
	public dynamic Calc_NCalc()
		=> new Expression(FORMULA).Evaluate();

	[Benchmark]
	public dynamic Calc_xFunc()
		=> _processor.Solve(FORMULA).Number.Number;
}
```

## 処理速度とメモリ効率の比較結果

ベンチマークの結果は次の通りです。
処理速度は「NCalc」が速いです！メモリ効率は「xFunc」がよさそう。
「IronPython」はオーバースペックが故に重たい感じでしょうか。

| Method          | Mean          | Error         | StdDev        | Allocated  |
|---------------- |--------------:|--------------:|--------------:|-----------:|
| Calc_Primitive  |      5.727 ns |     17.684 ns |     0.9693 ns |       32 B |
| Calc_ClosedXml  |  2,050.161 ns |    654.618 ns |    35.8818 ns |    1,400 B |
| Calc_DataTable  |  1,145.021 ns |    256.373 ns |    14.0526 ns |    3,256 B |
| Calc_IronPython | 31,397.335 ns | 49,899.015 ns | 2,735.1334 ns |   43,343 B |
| Calc_NCalc      |    323.583 ns |    178.721 ns |     9.7963 ns |    1,048 B |
| Calc_xFunc      |  1,911.553 ns |    475.276 ns |    26.0515 ns |      632 B |

- 凡例
  - `Mean`: Arithmetic mean of all measurements
  - `Error`: Half of 99.9% confidence interval
  - `StdDev`: Standard deviation of all measurements
  - `Allocated`: Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  - 1 ns: 1 Nanosecond (0.000000001 sec)

ちなみに実行環境は下記のとおりです。

- CPU: Core i7-1068NG7 2.30GHz
- RAM: 32.0 GB
- OS: Windows 10 Pro (22H2)

# まとめ

特徴の比較とベンチマークの結果をまとめると、こんな感じです。

|               | 四則演算 | 剰余演算 | べき乗演算 | 処理速度 | メモリ効率 | 
| ------------- | :------: | :------: | :--------: | :------: | :--------: | 
| 1. ClosedXml  | 〇       | △ (MOD) | 〇         | △       | △         | 
| 2. DataTable  | 〇       | 〇       | ×          | △       | △         | 
| 3. IronPython | 〇       | 〇       | △ (XOR)   | ×        | ×          | 
| 4. Ncalc      | 〇       | 〇       | △ (XOR)   | 〇       | △         | 
| 5. xFunc      | 〇       | 〇       | 〇         | △       | 〇         | 

僕には「**xFunc**」の利用が最適な解決策となりそうです。処理速度は「Ncalc」と「DataTable」には劣るものの、そこそこ速く、メモリ使用量は最も少なく済みました。さらに、`^` 演算子が排他的論理和演算子ではなく、べき乗演算子として利用できることが僕には使い勝手が良かったです。

# 参考ページ

https://dobon.net/vb/dotnet/programing/eval.html

https://www.nuget.org/packages/ClosedXML/

https://www.nuget.org/packages/IronPython/

https://www.nuget.org/packages/NCalcSync/

https://www.nuget.org/packages/xFunc.Maths/

https://takap-tech.com/entry/2023/06/16/225735

https://www.nuget.org/packages/BenchmarkDotNet/