#文字列の計算について
文字列を計算して結果を出力する方法はいくつかありますが、今回は正規表現を使って計算式を解析してみたいと思います。
実現したいことは次のようなものになります。
- 四則演算ができる
- 括弧付きの計算ができる
- 関数が使える
#確認環境
Unity 2018.3.5f1
IL2CPP
Android
#実装
実装の全容です。
解説は後述します。
###文字列計算クラス(本体)
using System;
using System.Text.RegularExpressions;
public static class StringCalculation
{
// 有効桁数
public const int SIGNIFICANT_DIGIT = 16;
// 小数点も取得する数値を表すPattern
private const string DECIMAL = @"[\+\-]?\d+(?:\.\d+)?";
// 文字列を計算して結果を返す
public static string Calculation(string calcStr)
{
string result = "";
// 対象文字列の半角スペースを削除
result = Regex.Replace(calcStr, @"\s", "");
// +-の符号重複を整理
result = OperatorOrganize(result);
// 余分な括弧を整理する
result = BracketsOrganize(result);
// 関数の計算
result = ProcFunction(result);
// ()内を計算
result = CalculationBrackets(result);
// カッコが全て計算された後の通常計算
result = NormalCalculation(result);
// 先頭の + は削除する
result = Regex.Replace(result, @"^\+", "");
return result;
}
// + または - 演算子の重複を解消する
private static string OperatorOrganize(string calcStr)
{
// + または - が2個連続している箇所を検出
string pattern = @"[\+\-]{2}";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result;
// 2つ続いている演算子を取得
Match match = Regex.Match(baseMatch.Value, @"([\+\-])([\+\-])");
result = (match.Result("$1") == match.Result("$2")) ? "+" : "-";
return result;
});
}
// 余計な括弧をまとめる
private static string BracketsOrganize(string calcStr)
{
string d = @"[\+\-\*\/]|" + DECIMAL;
string pattern = @"\({2}((?:" + d + @"|\((?:" + d + @")+\))+)\){2}";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result = baseMatch.Result("($1)");
return result;
});
}
// 関数の計算
private static string ProcFunction(string calcStr)
{
// Sin() や Cos() など関数の形に一致
string d = @"[\+\-\*\/]|" + DECIMAL;
string d1 = d + @"\,?";
string d2 = @"\((?:" + d + @")+\)\,?";
string f = @"[a-zA-Z_]\w+";
string pattern = "(" + f + @")\(((?:" + d1 + "|" + d2 + @")*)\)";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
// 後ろで正規表現を使用するので、$1が消える前に関数名を取得しておく
string func = baseMatch.Result("$1");
// 引数をカンマ区切りで取得し、それぞれを計算する
string[] args = baseMatch.Result("$2").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < args.Length; i++)
{
args[i] = CalculationBrackets(args[i]);
args[i] = NormalCalculation(args[i]);
}
// 関数呼び出し
string result = StringCalculationFunctions.InvokeMethod(func, args);
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
result = result.ToDouble().ToStringWithDigit(SIGNIFICANT_DIGIT);
return result;
});
}
// カッコ付きの箇所の計算
private static string CalculationBrackets(string calcStr)
{
// 一番内側の括弧に一致
string d = DECIMAL;
string pattern = @"\(((?:[\-\+\*\/]|" + d + @")+)\)";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result = baseMatch.Result("$1");
result = OperatorOrganize(result);
result = NormalCalculation(result);
return result;
});
}
// カッコなしの通常の計算を行う
private static string NormalCalculation(string calcStr)
{
// 掛け算と割り算
string result = MultiDiv(calcStr);
// 足し算と引き算
result = AddSub(result);
return result;
}
// * または / の計算を行う
private static string MultiDiv(string calcStr)
{
// 掛け算、または割り算に一致
string d = DECIMAL;
string pattern = d + @"\*" + d + "|" + d + @"\/" + d;
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
double nResult = 0;
// 演算子で分割し、計算
Match match = Regex.Match(baseMatch.Value, @"\*|\/");
string l = match.Result("$`");
string r = match.Result("$'");
if (match.Value == "*")
{
nResult = (l.ToDouble() * r.ToDouble());
}
else if (match.Value == "/")
{
nResult = (l.ToDouble() / r.ToDouble());
}
else
{
nResult = match.Value.ToDouble();
}
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
string result = nResult.ToStringWithDigit(SIGNIFICANT_DIGIT);
// 計算結果が + の場合、前方の文字と結合する際に符号なしでくっついてしまう※ので、符号を追加
// ※この関数の結果が10、前方の文字が123だとした場合、123 10 => 12310 になってしまう。
// + を記述しておけば、 123 +10 => 123+10 と正しい数式の形に戻せる。
string prefix = nResult >= 0 ? "+" : "";
return prefix + result;
});
}
// + または - の計算を行う
private static string AddSub(string calcStr)
{
// 足し算、または引き算に一致
string d = DECIMAL;
string pattern = d + @"\+" + d + "|" + d + @"\-" + d;
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
double nResult = 0;
// 演算子で分割し、計算
Match match = Regex.Match(baseMatch.Value, @"(" + d + @")([\+\-])(" + d + @")");
string l = match.Result("$1");
string r = match.Result("$3");
string op = match.Result("$2");
if (op == "+")
{
nResult = (l.ToDouble() + r.ToDouble());
}
else if (op == "-")
{
nResult = (l.ToDouble() - r.ToDouble());
}
else
{
nResult = match.Value.ToDouble();
}
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
string result = nResult.ToStringWithDigit(SIGNIFICANT_DIGIT);
return result;
});
}
// 各種計算用メソッドのベース
private static string CalculationBase(string calcStr, string pattern, Func<Match, string> callback, Match argMatch = null)
{
// 指定したパターンに一致する
Match match = argMatch ?? Regex.Match(calcStr, pattern);
if (match.Success)
{
// あとで結合するので、計算する文字列の前後を取得しておく
string pre = match.Result("$`");
string suf = match.Result("$'");
// 計算はコールバックに任せる
string result = callback(match);
// 計算結果を元の場所に結合
result = string.Format("{0}{1}{2}", pre, result, suf);
// まだ計算するべき数式があるかチェック
Match nMatch = Regex.Match(result, pattern);
if (nMatch.Success)
{
result = CalculationBase(result, pattern, callback, nMatch);
}
return result;
}
return calcStr;
}
}
###処理用関数定義&呼び出し用クラス
using System;
using System.Collections.Generic;
using System.Linq;
public static class StringCalculationFunctions
{
// ネイピア数
private const double E = 2.71828182845904523536;
// 有効桁数
private const int SIGNIFICANT_DIGIT = StringCalculation.SIGNIFICANT_DIGIT;
// メソッド呼び出し用辞書
private static readonly Dictionary<(string, int), Func<string[], string>> _methodCache = new Dictionary<(string, int), Func<string[], string>>
{
{("Sin", new Type[]{typeof(string)}.GetValueHash()), (args)=>{ return Sin(args[0]); }},
{("Cos", new Type[]{typeof(string)}.GetValueHash()), (args)=>{ return Cos(args[0]); }},
{("Tan", new Type[]{typeof(string)}.GetValueHash()), (args)=>{ return Tan(args[0]); }},
{("Log10", new Type[]{typeof(string)}.GetValueHash()), (args)=>{ return Log10(args[0]); }},
{("Log", new Type[]{typeof(string)}.GetValueHash()), (args)=>{ return Log(args[0]); }},
{("Log", new Type[]{typeof(string), typeof(string)}.GetValueHash()), (args)=>{ return Log(args[0], args[1]); }},
{("Ln", new Type[]{typeof(string)}.GetValueHash()), (args)=>{ return Ln(args[0]); }},
};
// Type[]で同じ並びの際に同じハッシュ値を返す
private static int GetValueHash(this Type[] types)
{
int result = 0;
int length = types.Length;
for (int i = 0; i < length; i++)
{
var shift_l = (i % 32);
var shift_r = 32 - shift_l;
var hash = types[i].GetHashCode();
result += hash << shift_l | hash >> shift_r;
}
return result;
}
// 登録されたメソッドを呼び出す
public static string InvokeMethod(string methodName, params object[] args)
{
var argTypesHash = ((args == null) ? Type.EmptyTypes : args.Select(x => x.GetType()).ToArray()).GetValueHash();
var cacheKey = (methodName, argTypesHash);
var func = _methodCache[cacheKey];
return func(args);
}
//====================================================================================================
// 実体メソッド
//====================================================================================================
// Sin
public static string Sin(string calcStr)
{
double result = Math.Sin(Deg2rad(calcStr.ToDouble()));
return result.ToStringWithDigit(SIGNIFICANT_DIGIT);
}
// Cos
public static string Cos(string calcStr)
{
double result = Math.Cos(Deg2rad(calcStr.ToDouble()));
return result.ToStringWithDigit(SIGNIFICANT_DIGIT);
}
// Tan
public static string Tan(string calcStr)
{
double result = Math.Tan(Deg2rad(calcStr.ToDouble()));
return result.ToStringWithDigit(SIGNIFICANT_DIGIT);
}
// log - 10を底としたlogの計算を行う
public static string Log10(string calcStr)
{
double result = Math.Log10(calcStr.ToDouble());
return result.ToStringWithDigit(SIGNIFICANT_DIGIT);
}
// log - ネイピア数 e を底としたlogの計算を行う
public static string Log(string calcStr)
{
double result = Math.Log(calcStr.ToDouble());
return result.ToStringWithDigit(SIGNIFICANT_DIGIT);
}
// log - 自前でベース値を設定する
public static string Log(string calcStr, string baseStr)
{
double result = Math.Log(calcStr.ToDouble(), baseStr.ToDouble());
return result.ToStringWithDigit(SIGNIFICANT_DIGIT);
}
// log - ネイピア数 e を底としたlogの計算を行う
public static string Ln(string calcStr)
{
double result = Math.Log(calcStr.ToDouble(), E);
return result.ToStringWithDigit(SIGNIFICANT_DIGIT);
}
// 角度をラジアンに変換
private static double Deg2rad(double num)
{
return num / 180.0 * Math.PI;
}
}
###string⇔doubleの変換用拡張メソッド
using System.Text.RegularExpressions;
public static class DoubleExtension
{
// 小数点第n位まで取得し、後ろの余分な0を切り捨てて文字列にする
public static string ToStringWithDigit(this double value, int digit)
{
// 小数点第digit位まで取得し、後ろの余分な0を切り捨てる
string result = string.Format("{0:f" + digit + "}", value);
return Regex.Replace(result, @"(\.?0+)$", "");
}
}
using System;
public static class StringExtension
{
// 文字列をdouble型に変換します
public static double ToDouble(this string str)
{
if (double.TryParse(str, out double ret))
{
return ret;
}
throw new ArgumentException("<" + str + ">をdouble型に変換できません");
}
}
#解説
それぞれのメソッドの役割について解説していきます。
#####パターンマッチング用基底メソッド
まずはパターンマッチング用の共通処理基底メソッドです。
解析したい文字列、正規表現パターンを引数で受け取り、パターンにマッチした場合はそのマッチした部分の処理をコールバックに任せます。
コールバックにて処理された文字列は元の場所に再配置され、その結果の文字列内に再度マッチするパターンが存在する場合、再帰処理にてもう一度解析を行います。
再帰処理は指定されたパータンに一致しなくなるまで続きます。
1+2+3+4
という計算式と、足し算を処理するパターン(例えば n+m にマッチするパターン)が引数に与えられた場合の動作はこのようなイメージです。
[1+2]+3+4
⇒ 3+3+4
⇒ [3+3]+4
⇒ 6+4
⇒ [6+4]
⇒ 10
⇒ n+mのパターンに一致しないので再帰処理終了。
ソースコード
private static string CalculationBase(string calcStr, string pattern, Func<Match, string> callback, Match argMatch = null)
{
// 指定したパターンに一致する
Match match = argMatch ?? Regex.Match(calcStr, pattern);
if (match.Success)
{
// あとで結合するので、計算する文字列の前後を取得しておく
string pre = match.Result("$`");
string suf = match.Result("$'");
// 計算はコールバックに任せる
string result = callback(match);
// 計算結果を元の場所に結合
result = string.Format("{0}{1}{2}", pre, result, suf);
// まだ計算するべき数式があるかチェック
Match nMatch = Regex.Match(result, pattern);
if (nMatch.Success)
{
result = CalculationBase(result, pattern, callback, nMatch);
}
return result;
}
return calcStr;
}
private const string DECIMAL = @"[\+\-]?\d+(?:\.\d+)?";
このパターンは 123
+123
+123.45
-123
-123.45
といった文字列にマッチします。
よく使うので定数として定義しています。
+符号もマッチパターンに入れているのは、計算途中で 123-+456
のような文字列になる可能性があり、それを [123] - [+456]
のように分解して計算したいためです。
#####無駄な演算子の重複処理メソッド
++
+-
-+
--
といった重複したプラスとマイナスの符号を一つの符号にまとめます。
これは計算として無駄を省くのと、あらかじめ三重以上の演算子の重複を統合しておかないとパターンマッチングで処理できないためです。
例えば 4+-5
のような計算式を、4-5
のような形にまとめます。
マッチした2つの符号が同じ場合は+
、違う場合は-
に変換されます。
ソースコード
private static string OperatorOrganize(string calcStr)
{
// + または - が2個連続している箇所を検出
string pattern = @"[\+\-]{2}";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result;
// 2つ続いている演算子を取得
Match match = Regex.Match(baseMatch.Value, @"([\+\-])([\+\-])");
result = (match.Result("$1") == match.Result("$2")) ? "+" : "-";
return result;
});
}
ソースコード
private static string AddSub(string calcStr)
{
// 足し算、または引き算に一致
string d = DECIMAL;
string pattern = d + @"\+" + d + "|" + d + @"\-" + d;
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
double nResult = 0;
// 演算子で分割し、計算
Match match = Regex.Match(baseMatch.Value, @"(" + d + @")([\+\-])(" + d + @")");
string l = match.Result("$1");
string r = match.Result("$3");
string op = match.Result("$2");
if (op == "+")
{
nResult = (l.ToDouble() + r.ToDouble());
}
else if (op == "-")
{
nResult = (l.ToDouble() - r.ToDouble());
}
else
{
nResult = match.Value.ToDouble();
}
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
string result = nResult.ToStringWithDigit(SIGNIFICANT_DIGIT);
return result;
});
}
- 計算結果に符号を付けない場合 :
1+2*3
⇒1[+2*3]
⇒16
- 計算結果に符号を付ける場合 :
1+2*3
⇒1[+2*3]
⇒1+6
ソースコード
private static string MultiDiv(string calcStr)
{
// 掛け算、または割り算に一致
string d = DECIMAL;
string pattern = d + @"\*" + d + "|" + d + @"\/" + d;
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
double nResult = 0;
// 演算子で分割し、計算
Match match = Regex.Match(baseMatch.Value, @"\*|\/");
string l = match.Result("$`");
string r = match.Result("$'");
if (match.Value == "*")
{
nResult = (l.ToDouble() * r.ToDouble());
}
else if (match.Value == "/")
{
nResult = (l.ToDouble() / r.ToDouble());
}
else
{
nResult = match.Value.ToDouble();
}
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
string result = nResult.ToStringWithDigit(SIGNIFICANT_DIGIT);
// 計算結果が + の場合、前方の文字と結合する際に符号なしでくっついてしまう※ので、符号を追加
// ※この関数の結果が10、前方の文字が123だとした場合、123 10 => 12310 になってしまう。
// + を記述しておけば、 123 +10 => 123+10 と正しい数式の形に戻せる。
string prefix = nResult >= 0 ? "+" : "";
return prefix + result;
});
}
ソースコード
private static string NormalCalculation(string calcStr)
{
// 掛け算と割り算
string result = MultiDiv(calcStr);
// 足し算と引き算
result = AddSub(result);
return result;
}
このメソッドも括弧の内部に括弧が存在する場合はパターンにマッチしないため、結果として一番内側の括弧から処理されるようになっています。
ソースコード
private static string CalculationBrackets(string calcStr)
{
// 一番内側の括弧に一致
string d = DECIMAL;
string pattern = @"\(((?:[\-\+\*\/]|" + d + @")+)\)";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result = baseMatch.Result("$1");
result = OperatorOrganize(result);
result = NormalCalculation(result);
return result;
});
}
二重括弧内に関数がある場合も処理されませんが、このメソッドの目的が Sin((90))
といった関数内の二重括弧を排除するのが目的なので問題ありません。
後述しますが、関数は必ず Function([数式])
の形でパターンマッチさせるためです。
関数が全て計算された後の多重括弧については、CalculationBracketsメソッドが内側から一つずつ処理してくれるので問題ありません。
ソースコード
private static string BracketsOrganize(string calcStr)
{
string d = @"[\+\-\*\/]|" + DECIMAL;
string pattern = @"\({2}((?:" + d + @"|\((?:" + d + @")+\))+)\){2}";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result = baseMatch.Result("($1)");
return result;
});
}
関数名は、アルファベットとアンダースコアからしか開始できないようになっています(数値から始まる関数名はNG)。
また、Function((数式))
のような二重括弧以上のパターンは対応していないため、あらかじめ無駄な括弧を省いておく必要があります(前述参照)。
定義すれば引数なしの関数も呼び出すことができます。
ソースコード
private static string ProcFunction(string calcStr)
{
// Sin() や Cos() など関数の形に一致
string d = @"[\+\-\*\/]|" + DECIMAL;
string d1 = d + @"\,?";
string d2 = @"\((?:" + d + @")+\)\,?";
string f = @"[a-zA-Z_]\w+";
string pattern = "(" + f + @")\(((?:" + d1 + "|" + d2 + @")*)\)";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
// 後ろで正規表現を使用するので、$1が消える前に関数名を取得しておく
string func = baseMatch.Result("$1");
// 引数をカンマ区切りで取得し、それぞれを計算する
string[] args = baseMatch.Result("$2").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < args.Length; i++)
{
args[i] = CalculationBrackets(args[i]);
args[i] = NormalCalculation(args[i]);
}
// 関数呼び出し
string result = StringCalculationFunctions.InvokeMethod(func, args);
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
result = result.ToDouble().ToStringWithDigit(SIGNIFICANT_DIGIT);
return result;
});
}
(例)実体を記述
public static string Log(string calcStr)
{
double result = Math.Log(calcStr.ToDouble());
return result.ToStringWithDigit(SIGNIFICANT_DIGIT);
}
(例)辞書に登録
private static readonly Dictionary<(string, int), Func<string[], string>> _methodCache = new Dictionary<(string, int), Func<string[], string>>
{
~~~
{("Log", new Type[]{typeof(string)}.GetValueHash()), (args)=>{ return Log(args[0]); }},
~~~
};
辞書のキーはTupleで、<string 呼び出し名, int 引数の型リストのハッシュ値> となっています。
引数の型リストのハッシュ値は独自計算によって、引数の個数は同じで型だけが違うメソッドがオーバーロードされていても問題ないようになっています(今回はすべてstringなのであまり意味ありませんが)。
恐らくこのクラスの実装形式からしてstring以外の引数はありえないので、Tupleキーのint値は単純に引数の数(Length)でも良いかと思います。
引数型リストのハッシュ値生成コード
private static int GetValueHash(this Type[] types)
{
int result = 0;
int length = types.Length;
for (int i = 0; i < length; i++)
{
var shift_l = (i % 32);
var shift_r = 32 - shift_l;
var hash = types[i].GetHashCode();
result += hash << shift_l | hash >> shift_r;
}
return result;
}
引数型リストのハッシュ値生成コード
private static int GetValueHash(this Type[] types)
{
return types.Length;
}
#####処理順序
実装された各メソッドを使って、次の順序で処理を行います。
- +-の符号を整理
- 関数処理のために余分な括弧を整理
- 関数の計算
- 括弧で括られた式の計算
- 括弧がすべて計算されたので、通常の四則演算
これについては通常の計算優先度と同じ感じになってます。
半角スペースについては正規表現の邪魔になるのであらかじめ削除しておきます。
ソースコード
public static string Calculation(string calcStr)
{
string result = "";
// 対象文字列の半角スペースを削除
result = Regex.Replace(calcStr, @"\s", "");
// +-の符号重複を整理
result = OperatorOrganize(result);
// 余分な括弧を整理する
result = BracketsOrganize(result);
// 関数の計算
result = ProcFunction(result);
// ()内を計算
result = CalculationBrackets(result);
// カッコが全て計算された後の通常計算
result = NormalCalculation(result);
// 先頭の + は削除する
result = Regex.Replace(result, @"^\+", "");
return result;
}
#検算
色々なパターンの数式を用いて検算を行います。
文字列の数式と、実際のコード上で計算した結果を比べ、一定の誤差以内であればOKとしています。
適当なGameObjectにアタッチして実行したらGameView上で結果が見れます。
検証ソースコード
using System;
using UnityEngine;
public class StrCalcTest : MonoBehaviour
{
string _dbgStr = "";
private Vector2 _scrollPosition;
// Use this for initialization
void Start()
{
// 通常計算
_Recalculation(
"3.5 - 0.5 + 21 * 2 / 2.5",
3.5 - 0.5 + 21 * 2 / 2.5
);
// 通常計算
_Recalculation(
"-2 + 1 * +2 / -2 / 2.0 * -2 + 5 * 2",
-2 + 1 * +2 / -2 / 2.0 * -2 + 5 * 2
);
// 括弧付き計算
_Recalculation(
"(1 + 2.5 + (2 - 3.5) * (1 + 1)) * 5 + 3 * (20 - 8)",
(1 + 2.5 + (2 - 3.5) * (1 + 1)) * 5 + 3 * (20 - 8)
);
// 演算子重複
_Recalculation(
"1 * (+2 + (-3)) - -+4 - -+ +- -5",
1 * (+2 + (-3)) - -+4 - -+ +- -5
);
// 括弧の計算
_Recalculation(
"2 - (1 * -11)",
2 - (1 * -11)
);
// 括弧の計算
_Recalculation(
"1 * (+2 + (-3)) - (-(+4)) - (-(+(+(-(-5)))))",
1 * (+2 + (-3)) - (-(+4)) - (-(+(+(-(-5)))))
);
// 括弧の計算
_Recalculation(
"((-20.25) + 4) / ((4 / 2) + 3 * (1 * 2))",
((-20.25) + 4) / ((4 / 2) + 3 * (1 * 2))
);
// 関数の計算
_Recalculation(
"Sin(45)",
Math.Sin(Deg2rad(45))
);
// 関数の計算
_Recalculation(
"Cos(45)",
Math.Cos(Deg2rad(45))
);
// 関数の計算
_Recalculation(
"Tan(45)",
Math.Tan(Deg2rad(45))
);
// 関数の計算
_Recalculation(
"Cos((12)) + Tan((179) + Sin(75))",
Math.Cos(Deg2rad((12))) + Math.Tan(Deg2rad((179) + Math.Sin(Deg2rad(75))))
);
// 余分な括弧の計算
_Recalculation(
"((((-(2 + 3) + 9))))",
((((-(2 + 3) + 9))))
);
// 関数と過剰な括弧の計算
_Recalculation(
"((Cos(Sin(( Cos(((((45+45)))) + 90) * - 90 )) * 180)) + 20 + Tan((180)))",
((Math.Cos(Deg2rad(Math.Sin((Deg2rad(Math.Cos(Deg2rad(((((45 + 45)))) + 90)) * -90))) * 180))) + 20 + Math.Tan(Deg2rad((180))))
);
// 関数と過剰な括弧の計算
_Recalculation(
"Cos((((((((-(2+3)+9))))))))",
Math.Cos(Deg2rad((((((((-(2 + 3) + 9)))))))))
);
// Log計算
_Recalculation(
"Sin(Log10(10))",
Math.Sin(Deg2rad(Math.Log10(10)))
);
// Log計算
_Recalculation(
"Sin(Log(10))",
Math.Sin(Deg2rad(Math.Log(10)))
);
// Log計算
_Recalculation(
"Sin(Log(10+2*3, 4*2.4-5))",
Math.Sin(Deg2rad(Math.Log(10 + 2 * 3, 4 * 2.4 - 5)))
);
// Log計算
_Recalculation(
"Log10(0.1)",
Math.Log10(0.1)
);
// Log計算
_Recalculation(
"Ln(42.22*2)",
Math.Log(42.22 * 2, Math.E)
);
}
private void OnGUI()
{
_scrollPosition = GUILayout.BeginScrollView(_scrollPosition);
GUILayout.Label(_dbgStr);
GUILayout.EndScrollView();
}
private static double Deg2rad(double num)
{
return num / 180.0 * Math.PI;
}
// 文字列の式と実際に計算した際の数値結果を入力し、検算を行う
private void _Recalculation(string str, double nResult)
{
string sResult = StringCalculation.Calculation(str);
// 計算結果が文字列として一致、もしくは誤差の範囲内かどうか
string isSuccess = (sResult == nResult.ToString() || Math.Abs(sResult.ToDouble() - nResult) <= 0.000000000001) ? "○" : "×";
string resultStr = "検算結果 : " + "" + isSuccess + "\n" + str + " =>\n STR : [" + sResult + "]\n NUM : [" + nResult + "]\n\n";
_dbgStr += resultStr;
}
}
#最後に
若干の誤差は出るものの、大体一致する程度の計算ができました。
もう少し精度を上げるならdoubleではなくdecimalを使ったら良いんでしょうか。
正規表現に精通している訳ではないのでもっと簡単な書き方もあるかもしれません。
#おまけ
処理途中のログを出力する機能を実装したコードを置いておきます。
内容としてはログなしのコードと変わりません。
Unity上で実行すると、マッチング結果とそれをどのように処理しているかの過程を見ることができます。
※出力先はUnityのConsoleビューです。
ソースコード
#define CALC_LOG_OUTPUT
using System;
using System.Text.RegularExpressions;
#if CALC_LOG_OUTPUT
using UnityEngine;
#endif
namespace MyEngine
{
/// <summary>
/// 文字列内の数式を計算する静的メソッドを提供します
/// </summary>
public static class StringCalculation
{
#if CALC_LOG_OUTPUT
// Debug用変数
private static string _logText = "";
private static string _indent = "";
private static string _procFuncName = "";
#endif
/// <summary>
/// 有効桁数
/// </summary>
public const int SIGNIFICANT_DIGIT = 16;
/// <summary>
/// 小数点も取得する数値を表すPattern
/// </summary>
private const string DECIMAL = "[\\+\\-]?\\d+(?:\\.\\d+)?";
/// <summary>
/// 文字列を計算して結果を返す
/// </summary>
/// <param name="calcStr"></param>
/// <returns></returns>
public static string Calculation(string calcStr)
{
string result = "";
#if CALC_LOG_OUTPUT
// Log
CalculationLog("入力 : " + calcStr + "\n");
#endif
// 対象文字列の半角スペースを削除
result = Regex.Replace(calcStr, @"\s", "");
#if CALC_LOG_OUTPUT
// Log
CalculationLog("数式 : " + result + "\n");
#endif
// +-の符号重複を整理
result = OperatorOrganize(result);
// 余分な括弧を整理する
result = BracketsOrganize(result);
// 関数の計算
result = ProcFunction(result);
// ()内を計算
result = CalculationBrackets(result);
// カッコが全て計算された後の通常計算
result = NormalCalculation(result);
// 先頭の + は削除する
result = Regex.Replace(result, @"^\+", "");
#if CALC_LOG_OUTPUT
// Log
CalculationLog("結果 : " + result + "\n");
Debug.Log(_logText);
_logText = string.Empty;
#endif
return result;
}
/// <summary>
/// + または - 演算子の重複を解消する
/// </summary>
/// <param name="calcStr"></param>
/// <returns></returns>
private static string OperatorOrganize(string calcStr)
{
#if CALC_LOG_OUTPUT
return IndividualProcBase(GetHostFuncName(), () =>
#endif
{
// + または - が2個連続している箇所を検出
string pattern = @"[\+\-]{2}";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result;
// 2つ続いている演算子を取得
Match match = Regex.Match(baseMatch.Value, @"([\+\-])([\+\-])");
result = (match.Result("$1") == match.Result("$2")) ? "+" : "-";
return result;
});
}
#if CALC_LOG_OUTPUT
);
#endif
}
/// <summary>
/// 余計な括弧をまとめる
/// </summary>
/// <param name="calcStr"></param>
/// <returns></returns>
private static string BracketsOrganize(string calcStr)
{
#if CALC_LOG_OUTPUT
return IndividualProcBase(GetHostFuncName(), () =>
#endif
{
string d = @"[\+\-\*\/]|" + DECIMAL;
string pattern = @"\({2}((?:" + d + @"|\((?:" + d + @")+\))+)\){2}";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result = baseMatch.Result("($1)");
return result;
});
}
#if CALC_LOG_OUTPUT
);
#endif
}
/// <summary>
/// 関数の計算
/// </summary>
/// <param name="calcStr"></param>
/// <returns></returns>
private static string ProcFunction(string calcStr)
{
#if CALC_LOG_OUTPUT
return IndividualProcBase(GetHostFuncName(), () =>
#endif
{
// Sin() や Cos() など関数の形に一致
string d = @"[\+\-\*\/]|" + DECIMAL;
string d1 = d + @"\,?";
string d2 = @"\((?:" + d + @")+\)\,?";
string f = @"[a-zA-Z_]\w+";
string pattern = "(" + f + @")\(((?:" + d1 + "|" + d2 + @")*)\)";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
// 後ろで正規表現を使用するので、$1が消える前に関数名を取得しておく
string func = baseMatch.Result("$1");
// 引数をカンマ区切りで取得し、それぞれを計算する
string[] args = baseMatch.Result("$2").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
#if CALC_LOG_OUTPUT
string dArg = "";
#endif
for (int i = 0; i < args.Length; i++)
{
#if CALC_LOG_OUTPUT
CalculationLog("\n");
CalculationLog(" # " + args[i] + "\n");
#endif
args[i] = CalculationBrackets(args[i]);
args[i] = NormalCalculation(args[i]);
#if CALC_LOG_OUTPUT
dArg += args[i] + ",";
#endif
}
string result = StringCalculationFunctions.InvokeMethod(func, args);
#if CALC_LOG_OUTPUT
dArg = dArg.Substring(0, dArg.Length - 1);
CalculationLog("\n");
CalculationLog("--:" + func + "(" + dArg + ") => " + result + "\n");
CalculationLog("\n");
#endif
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
result = result.ToDouble().ToStringWithDigit(SIGNIFICANT_DIGIT);
return result;
});
}
#if CALC_LOG_OUTPUT
);
#endif
}
/// <summary>
/// カッコ付きの箇所の計算
/// </summary>
/// <param name="calcStr"></param>
/// <returns></returns>
private static string CalculationBrackets(string calcStr)
{
#if CALC_LOG_OUTPUT
return IndividualProcBase(GetHostFuncName(), () =>
#endif
{
// 一番内側の括弧に一致
string d = DECIMAL;
string pattern = @"\(((?:[\-\+\*\/]|" + d + @")+)\)";
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
string result = baseMatch.Result("$1");
result = OperatorOrganize(result);
result = NormalCalculation(result);
return result;
});
}
#if CALC_LOG_OUTPUT
);
#endif
}
/// <summary>
/// カッコなしの通常の計算を行う
/// </summary>
/// <param name="calcStr"></param>
/// <returns></returns>
private static string NormalCalculation(string calcStr)
{
// 掛け算と割り算
string result = MultiDiv(calcStr);
// 足し算と引き算
result = AddSub(result);
return result;
}
/// <summary>
/// * または / の計算を行う
/// </summary>
/// <param name="calcStr"></param>
/// <returns></returns>
private static string MultiDiv(string calcStr)
{
#if CALC_LOG_OUTPUT
return IndividualProcBase(GetHostFuncName(), () =>
#endif
{
// 掛け算、または割り算に一致
string d = DECIMAL;
string pattern = d + @"\*" + d + "|" + d + @"\/" + d;
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
double nResult = 0;
// 演算子で分割し、計算
Match match = Regex.Match(baseMatch.Value, @"\*|\/");
string l = match.Result("$`");
string r = match.Result("$'");
if (match.Value == "*")
{
nResult = (l.ToDouble() * r.ToDouble());
}
else if (match.Value == "/")
{
nResult = (l.ToDouble() / r.ToDouble());
}
else
{
nResult = match.Value.ToDouble();
}
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
string result = nResult.ToStringWithDigit(SIGNIFICANT_DIGIT);
// 計算結果が + の場合、前方の文字と結合する際に符号なしでくっついてしまう※ので、符号を追加
// ※この関数の結果が10、前方の文字が123だとした場合、123 10 => 12310 になってしまう。
// + を記述しておけば、 123 +10 => 123+10 と正しい数式の形に戻せる。
string prefix = nResult >= 0 ? "+" : "";
return prefix + result;
});
}
#if CALC_LOG_OUTPUT
);
#endif
}
/// <summary>
/// + または - の計算を行う
/// </summary>
/// <param name="calcStr">文字列の計算式</param>
/// <returns></returns>
private static string AddSub(string calcStr)
{
#if CALC_LOG_OUTPUT
return IndividualProcBase(GetHostFuncName(), () =>
#endif
{
// 足し算、または引き算に一致
string d = DECIMAL;
string pattern = d + @"\+" + d + "|" + d + @"\-" + d;
return CalculationBase(calcStr, pattern, (baseMatch) =>
{
double nResult = 0;
// 演算子で分割し、計算
Match match = Regex.Match(baseMatch.Value, @"(" + d + @")([\+\-])(" + d + @")");
string l = match.Result("$1");
string r = match.Result("$3");
string op = match.Result("$2");
if (op == "+")
{
nResult = (l.ToDouble() + r.ToDouble());
}
else if (op == "-")
{
nResult = (l.ToDouble() - r.ToDouble());
}
else
{
nResult = match.Value.ToDouble();
}
// 小数点第n位まで取得し、後ろの余分な0を切り捨てる
string result = nResult.ToStringWithDigit(SIGNIFICANT_DIGIT);
return result;
});
}
#if CALC_LOG_OUTPUT
);
#endif
}
/// <summary>
/// 各種計算用メソッドのベース
/// </summary>
/// <param name="calcStr">文字列の数式</param>
/// <param name="pattern">正規表現パターン</param>
/// <param name="callback">正規表現にマッチした際に実際に処理を行うコールバック</param>
/// <returns></returns>
private static string CalculationBase(string calcStr, string pattern, Func<Match, string> callback, Match argMatch = null)
{
#if CALC_LOG_OUTPUT
DbgInitProc();
#endif
// 指定したパターンに一致する
Match match = argMatch ?? Regex.Match(calcStr, pattern);
int matchNum = match.Groups.Count - 1;
if (match.Success)
{
// あとで結合するので、計算する文字列の前後を取得しておく
string pre = match.Result("$`");
string suf = match.Result("$'");
#if CALC_LOG_OUTPUT
string mat = match.Result("$&");
// マッチ結果を表示
CalculationLog(" match : " + pre + " [" + mat + "] " + suf + "\n");
for (int i = 1; i <= matchNum; i++)
{
CalculationLog(" ~ $" + (i) + " = " + match.Groups[i] + "\n");
}
#endif
// 計算はコールバックに任せる
string result = callback(match);
// 計算結果を元の場所に結合
result = string.Format("{0}{1}{2}", pre, result, suf);
// まだ計算するべき数式があるかチェック
Match nMatch = Regex.Match(result, pattern);
if (nMatch.Success)
{
#if CALC_LOG_OUTPUT
CalculationLog(" ● >>> more " + _procFuncName + " : " + result + "\n");
#endif
result = CalculationBase(result, pattern, callback, nMatch);
#if CALC_LOG_OUTPUT
DbgEndProc();
#endif
}
else
{
#if CALC_LOG_OUTPUT
DbgEndProc();
CalculationLog(" > end : " + result + "\n");
#endif
}
return result;
}
#if CALC_LOG_OUTPUT
DbgEndProc();
#endif
return calcStr;
}
#if CALC_LOG_OUTPUT
//====================================================================================================
// debug デバッグ出力用
//====================================================================================================
/// <summary>
/// 計算のログを出力する
/// </summary>
/// <param name="log"></param>
private static void CalculationLog(string log)
{
_logText += _indent + log;
}
/// <summary>
/// 呼び出し元のメソッド名を取得
/// </summary>
/// <param name="callerFrameIndex"></param>
/// <returns></returns>
private static string GetHostFuncName(int callerFrameIndex = 1)
{
System.Diagnostics.StackFrame callerFrame = new System.Diagnostics.StackFrame(callerFrameIndex);
System.Reflection.MethodBase callerMethod = callerFrame.GetMethod();
return callerMethod == null ? "" : callerMethod.Name;
}
/// <summary>
/// 計算開始
/// </summary>
static void DbgInitProc()
{
string host_func_name = GetHostFuncName(3);
if (host_func_name != "CalculationBase")
{
CalculationLog(" ■ ---=== " + _procFuncName + " ===---\n");
}
_indent += " |";
}
/// <summary>
/// 計算終了
/// </summary>
static void DbgEndProc()
{
_indent = _indent.Substring(0, _indent.Length - 3);
}
/// <summary>
/// debug デバッグ出力用
/// </summary>
/// <param name="funcName"></param>
/// <param name="callback"></param>
/// <returns></returns>
private static string IndividualProcBase(string funcName, Func<string> callback)
{
string prevProcFuncName = _procFuncName;
_procFuncName = funcName;
string result = callback();
_procFuncName = prevProcFuncName;
return result;
}
#endif
}
}