C#コードでパーサーを直接構築するためのシンプルかつ軽量なライブラリとしてSpracheというのがある。これを利用し、文字列で入力された計算式を処理するライブラリがSprache.Calcとして提供されている。
- Sprache
https://github.com/sprache/Sprache - Sprache.Calc
https://github.com/yallie/Sprache.Calc
ちょっと前に、計算式を入力させて、計算結果を得るようなプログラムを簡単に書けないかと調べていて見つけたのが、上記ライブラリ群。
Sprache.Calcを使うことにより、文字列”1+2+3″から6という結果を得ることができる。また使い方によっては、C#のSystem.Mathの関数群を記述できたり、変数(aとbとかの値)も取り扱うことができる。
これを使い、実際にアプリケーションを組んだ場合に困ったのが、定数や関数名に間違いがあった場合。どういう名称が間違いだったのかが分からない。
そこで、その情報を取り出すためのクラスを作成してみた。
基本的には、CallFunctionとGetParameterExpressionをオーバーライドしてチェックとエラー情報の抽出を行う。
using Sprache;
using Sprache.Calc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using ParameterList = System.Collections.Generic.Dictionary<string, double>;
namespace CalcGui {
public class TryableCalculator : XtensibleCalculator {
/// <summary>チェックの結果利用できない関数の一覧</summary>
private string unresolveFunction;
/// <summary>チェックの結果利用できない変数の一覧</summary>
private string unresolveVariable;
public string UnresolveFunction { get => unresolveFunction; }
public string UnresolveVariable { get => unresolveVariable; }
/// <summary>
/// テキストが構文通りかチェックする
/// 問題なければ、ParseExpressionを再度呼び出しする
/// </summary>
public IResult<LambdaExpression> TryParse(string text) {
unresolveFunction = unresolveVariable = null;
return Lambda.TryParse(text);
}
/// <summary>パースした結果、未解決な関数・変数があるかどうかを返す</summary>
/// <return>パースが成功した場合true</return>
public bool OkParse {
get => string.IsNullOrEmpty(unresolveFunction) && string.IsNullOrEmpty(unresolveVariable);
}
/// <summary>
/// 関数の呼び出し
/// 関数が呼び出せない場合は0.0の定数とする
/// また、unresolveへ情報を書き込む
/// </summary>
protected internal override Expression CallFunction(string name, params Expression[] parameters) {
var methodInfo = typeof(Math).GetMethod(name, parameters.Select(e => e.Type).ToArray());
if (methodInfo == null) {
if (!string.IsNullOrEmpty(unresolveFunction)) {
unresolveFunction += ",";
}
unresolveFunction += string.Format("{0}({1})", name, string.Join(",", parameters.Select(e => e.Type.Name)));
return Expression.Constant(0.0);
}
else {
return Expression.Call(methodInfo, parameters);
}
}
/// <summary>
/// 定数の取り出し
/// 定数が存在しないと思われる場合は、0.0の定数とする
/// また、unresolveへ情報を書き込む
/// </summary>
protected internal override Expression GetParameterExpression(string name) {
// try to find a constant in System.Math
var systemMathConstants = typeof(System.Math).GetFields(BindingFlags.Public | BindingFlags.Static);
var constant = systemMathConstants.FirstOrDefault(c => c.Name == name);
if (constant != null) {
// return System.Math constant value
return Expression.Constant(constant.GetValue(null));
}
if (name.Length != 1 || !Char.IsLower(name[0])) {
if (!string.IsNullOrEmpty(UnresolveVariable)) {
unresolveVariable += ",";
}
unresolveVariable += name;
return Expression.Constant(0.0);
}
// return parameter value: Parameters[name]
var getItemMethod = typeof(ParameterList).GetMethod("get_Item");
return Expression.Call(ParameterExpression, getItemMethod, Expression.Constant(name));
}
/// <summary>このクラスで取り扱う関数・定数の一覧を保持するクラス</summary>
public virtual List<System.Type> GetMethodList() {
var val = new List<System.Type>();
val.Add(typeof(System.Math));
return val;
}
}
}
XtensibleCalculator内部で、関数及び定数の結果をもらう場合に、上記メソッドの呼び出しがなされる。そこで、呼び出された場合に、未登録の関数や定数だった場合に、それを履歴として残しておき、後から取り出せるようにしただけ。
エラー結果は、unresolveFunctionとunresolveVariableに格納される。
これら変数の初期化コードは、TryParseメソッドを作り、その中で実施するようにしている。
コメント