C#で文字列の数式を計算するプログラム

C#で文字列の数式を計算する方法を探していたんですが、
ちょっといいのがないので作りました。

以下の事項に対応しています
・四則演算
・括弧
・+/- の単項演算子
・変数、関数

よく見かける方法と比べて、変数や関数が使えるのが特徴です。

こちらのプログラムはGitHubの方に登録しました。
https://github.com/izmktr/SimpleCalc

```CSharp
using System;
using System.Collections.Generic;

namespace Calc
{
    class Program
    {
        static float mathfunc(string name, List<float> arg)
        {
            if (name == "pi") return (float)Math.PI;
            if (name == "sin") return (float)Math.Sin(arg[0]);
            if (name == "cos") return (float)Math.Cos(arg[0]);
            if (name == "max") return Math.Max(arg[0], arg[1]);

            return 0;
        }

        static void Main(string[] args)
        {
            var formura1 = Calc.Analyze("(-5 + 2 )* -4");

            float answer1 = formura1.Calc(null);
            Console.WriteLine($"calc :{answer1}");

            var formura2 = Calc.Analyze("max(sin(theta * pi / 180), cos(theta * pi / 180))");

            for (int i = 0; i <= 90; i += 5)
            {
                Func<string, List<float>, float> f = (name, arg) =>
                {
                    if (name == "theta") return (float)i;
                    return mathfunc(name, arg);
                };

                float answer2 = formura2.Calc(f);
                Console.WriteLine($"i = {i}: calc = {answer2}");
            }

        }
    }
}
```

変数や関数はCalc時にfloat f(string name, List < float > arg) の関数を引き渡します。
変数や関数が出てくるたびに渡した関数が呼び出されます。

pi と書いた場合は変数とみなされ、pi()と書いた場合は引数0の関数とみなされます。
変数の場合は、arg=null、関数の場合はarg=List<float> となります。

Unityの座標計算に使うために作ったので、数値はすべてfloatですが、
Intなりdoubleなりに書き換えるといいと思います


関数本体のソース

```csharp
using System;
using System.Collections.Generic;

namespace Calc
{
    static class Calc
    {
        enum Operator
        {
            Unknown,
            Number,
            Alphabet,
            Plus,
            Minus,
            Multi,
            Divide,
            Mod,
            LParen,
            RParen,
            Camma,
            Space,
        }

        class Lexical
        {
            public Operator op;
            public string str;

            public override string ToString() { return $"op={op}, str={str}"; }
        }

        public interface NodeBase
        {
            float Calc(Func<string, List<float>, float> func);
            bool IsCalcable { get; }
        }

        class NodeNumber: NodeBase
        {
            float value;

            public bool IsCalcable => true;

            public float Calc(Func<string, List<float>, float> func)
            {
                return value;
            }

            public NodeNumber(float value)
            {
                this.value = value;
            }
        }

        class NodeNegative: NodeBase
        {
            NodeBase node;
            public bool IsCalcable => node.IsCalcable;

            public float Calc(Func<string, List<float>, float> func)
            {
                return -node.Calc(func);
            }

            public NodeNegative(NodeBase node)
            {
                this.node = node;
            }
        }

        class NodeFunction : NodeBase
        {
            string funcname;
            List<NodeBase> nodearg = new List<NodeBase>();

            public NodeFunction(string func, List<NodeBase> nodearg)
            {
                this.funcname = func;
                this.nodearg = nodearg;
            }

            public float Calc(Func<string, List<float>, float> func)
            {
                List<float> numberarg = new List<float>();

                if (nodearg != null)
                {
                    foreach (var item in nodearg)
                    {
                        numberarg.Add(item.Calc(func));
                    }
                }

                return func(funcname, numberarg);
            }
            public bool IsCalcable => false;
        }

        class NodeTree : NodeBase
        {
            private Operator op;
            private NodeBase term1;
            private NodeBase term2;

            public NodeTree(Operator op, Calc.NodeBase term1, Calc.NodeBase term2)
            {
                this.op = op;
                this.term1 = term1;
                this.term2 = term2;
            }

            public bool IsCalcable => (term1 == null ? term1.IsCalcable : true) && (term2 == null ? term2.IsCalcable : true);

            public float Calc(Func<string, List<float>, float> func)
            {
                switch (op)
                {
                    case Operator.Plus:
                        return term1.Calc(func) + term2.Calc(func);
                    case Operator.Minus:
                        return term1.Calc(func) - term2.Calc(func);
                    case Operator.Multi:
                        return term1.Calc(func) * term2.Calc(func);
                    case Operator.Divide:
                        return term1.Calc(func) / term2.Calc(func);
                    case Operator.Mod:
                        return term1.Calc(func) % term2.Calc(func);
                }
                return 0;
            }
        }


        class Analyzer
        {
            List<Lexical> unitlist = new List<Lexical>();
            int current = 0;

            public void LexicalAnalysis(string formula)
            {
                unitlist.Clear();

                for (int idx = 0; idx < formula.Length; idx++)
                {
                    char c = formula[idx];
                    Operator op = GetOperator(c);

                    if (op == Operator.Space) continue;
                    if (op == Operator.Unknown) throw new Exception($"unknown letter: {c}");

                    if (op == Operator.Number)
                    {
                        var str = GetContinuity(formula, n => n == Operator.Number, ref idx);
                        unitlist.Add(new Lexical() { op = op, str = str });
                    }
                    else if (op == Operator.Alphabet)
                    {
                        var str = GetContinuity(formula, n => n == Operator.Number || n == Operator.Alphabet, ref idx);
                        unitlist.Add(new Lexical() { op = op, str = str });

                    }
                    else
                    {
                        unitlist.Add(new Lexical() { op = op });
                    }

                }
            }

            private string GetContinuity(string formula, Func<Operator, bool> compare, ref int idx)
            {
                int start = idx;
                for (int i = idx + 1; i < formula.Length; i++)
                {
                    if (!compare(GetOperator(formula[i])))
                    {
                        idx = i - 1;
                        return formula.Substring(start, i - start);
                    }
                }
                idx = formula.Length - 1;
                return formula.Substring(start);
            }

            private Operator GetOperator(char c)
            {
                switch (c)
                {
                    case '+': return Operator.Plus;
                    case '-': return Operator.Minus;
                    case '*': return Operator.Multi;
                    case '/': return Operator.Divide;
                    case '%': return Operator.Mod;
                    case '(': return Operator.LParen;
                    case ')': return Operator.RParen;
                    case ',': return Operator.Camma;
                    case ' ': return Operator.Space;
                    case '\t': return Operator.Space;
                    case '.': return Operator.Number;
                }

                if (Char.IsNumber(c)) return Operator.Number;
                if (Char.IsLetter(c)) return Operator.Alphabet;

                return Operator.Unknown;
            }

            public NodeBase GetExpr()
            {
                var term1 = GetTerm();

                for(;;)
                {
	            var next = GetNext();
	            if (next == null) return term1;
	            if (next.op == Operator.Plus || next.op == Operator.Minus)
	            {
	                var term2 = GetTerm();
	                term1 = new NodeTree(next.op, term1, term2);
	            }
                    else
                        break;
                }
                Unget();
                return term1;
            }

            NodeBase GetTerm()
            {
                var term1 = GetFactor();

                for(;;)
                {
                    var next = GetNext();
                    if (next == null) return term1;
                    if (next.op == Operator.Multi || next.op == Operator.Divide || next.op == Operator.Mod)
                    {
                        var term2 = GetFactor();
                        term1 = new NodeTree(next.op, term1, term2);
                    }
                    else
                        break;
                }
                Unget();
                return term1;
            }

            NodeBase GetFactor()
            {
                var lparen = GetNext();
                if (lparen == null)
                {
                    throw new Exception("式の終端に達しました");
                }

                switch (lparen.op)
                {
                    case Operator.LParen:
                        {
                            var expr = GetExpr();
                            var rparen = GetNext();
                            if (rparen == null || rparen.op == Operator.RParen) return expr;
                            Unget();
                            break;
                        }
                    default:
                        Unget();
                        return GetNumber();
                }
                {
                    throw new Exception("式の終端に達しました");
                }
            }

            NodeBase GetNumber()
            {
                var unit = GetNext();

                switch (unit.op)
                {
                    case Operator.Number:
                        return new NodeNumber(float.Parse(unit.str));
                    case Operator.Alphabet:
                        Unget();
                        return GetFunction();
                    case Operator.Plus:
                        return GetNumber();
                    case Operator.Minus:
                        {
                            var minus = GetNumber();
                            return minus != null ? new NodeNegative(minus) : null;
                        }
                }
                return null;
            }

            private NodeBase GetFunction()
            {
                var funcname = GetNext();
                if (funcname == null || funcname.op != Operator.Alphabet)
                {
                    throw new Exception("関数名がありません");
                }

                var lparen = GetNext();
                if (lparen == null || lparen.op != Operator.LParen)
                {
                    if (lparen != null) Unget();
                    return new NodeFunction(funcname.str, null);
                }

                var arg = new List<NodeBase>();
                for (; ; )
                {
                    var node = GetExpr();
                    if (node == null) return new NodeFunction(funcname.str, arg);
                    arg.Add(node);

                    var end = GetNext();

                    if (end == null || end.op == Operator.RParen)
                    {
                        return new NodeFunction(funcname.str, arg);
                    }
                    if (end.op != Operator.Camma)
                    {
                        throw new Exception("関数内の区切りがカンマではありません");
                    }
                }

            }

            Lexical GetNext()
            {
                if (current < unitlist.Count)
                {
                    return unitlist[current++];
                }
                return null;
            }

            void Unget()
            {
                current--;
            }

        }

        static public NodeBase Analyze(string formula)
        {
            Analyzer analyzer = new Analyzer();

            analyzer.LexicalAnalysis(formula);

            return analyzer.GetExpr() ;
        }

    }

}

```