带括号的四则混合运算计算器(二)

上一节中,我们实现了四则混合运算。

这节中,我们在表达式中混入System.Math中定义的一些数学函数,如Sin(),Abs()等,要求在不改变原有节点和节点解析类的情况下加入新的函数节点和函数节点解析类,以此来检测我们之前的设计是否稳健。

System.Math中的函数很多,是否都需要呢?这里我们只需要使用返回值和参数全都是double类型的,如Sin(1)和Pow(2, 3),可以通过以下代码把这些函数取出来:

public static List<MethodInfo> GetSystemMathFunctions()
{
    var funcs = typeof (Math).GetMethods().Where(m => m.ReturnType == typeof (double)).ToList();
    funcs = funcs.Where(f => f.GetParameters().All(p => p.ParameterType == typeof (double))).ToList();
    //string all = funcs.Aggregate("", (current, f) => current + (f.ToString() + Environment.NewLine));
    return funcs;
}

以下是该函数执行输出的23个函数,我们暂时只解析函数名不包含数字的,为了简单起见:

Double Acos(Double)
Double Asin(Double)
Double Atan(Double)
Double Atan2(Double, Double)
Double Ceiling(Double)
Double Cos(Double)
Double Cosh(Double)
Double Floor(Double)
Double Sin(Double)
Double Tan(Double)
Double Sinh(Double)
Double Tanh(Double)
Double Round(Double)
Double Sqrt(Double)
Double Log(Double)
Double Log10(Double)
Double Exp(Double)
Double Pow(Double, Double)
Double IEEERemainder(Double, Double)
Double Abs(Double)
Double Max(Double, Double)
Double Min(Double, Double)
Double Log(Double, Double)

有23个函数,我们是否要定义23个 FunctionNode呢?而且之后还有23个解析类要写?这里我们自然又要用到策略模式,这样只需要一个函数节点了,定义很简单,和其他节点一样直接继承NodeBase

public class FunctionNode : NodeBase
{
    private readonly List<INode> _parameters;
    private readonly MethodInfo _function;
    public FunctionNode(List<INode> parameters, int index, string data, MethodInfo function)
    : base(index, data)
    {
        _parameters = parameters;
        _function = function;
    }
    public override double GetValue()
    {
        var argumentList = _parameters.Select(p => (object) p.GetValue()).ToList();
        return (double) _function.Invoke(this, argumentList.ToArray());
    }
}

下面开始来设计FunctionFinder类:

首先先确定优先级,如果把函数参数当做一个整体的话,它相当于一个一元操作,应排在所有二元Node的解析之前,我们把它放在常量解析的后面解析:

 

public enum FinderPriority
{
    XFinder = 5,
    ConstantFinder = 10,
    FunctionFinder = 15,
    NegationFinder = 20,
}

 

其次,我们是通过正则表达式解析节点的,通过分析Sin(1)这样的子式,发现(1)与我们前面定义的SingleFinder(去括号)有冲突,所以需要在解析之前对表达式中的函数子式进行重构,重构方式有很多种,我们只需要一个,看看代码:

///<summary>
/// 先从表达式中取得所有函数信息,并重构表达式
///</summary>
///<param name="expression"></param>
static List<MethodInfo> FindFunctions(ref string expression)
{
    var foundFuntions = new List<string>();
    var rFunctionName = new Regex(@"[a-zA-Z]{3,20}\(");
    var m = rFunctionName.Match(expression);
            
    string newExpression = "";
    while (m.Success)
    {
        foundFuntions.Add(m.Value.Trim('('));
        //将Sin(a)重构成(@Sin:a),以解决与SingleFinder的冲突,由此匹配正则表达式也清楚了
        newExpression += (expression.Substring(0, m.Index) + string.Format("(@{0}:", m.Value.Trim('(')));
        expression = expression.Substring(m.Index + m.Value.Length);
        m = rFunctionName.Match(expression);
    }
    newExpression += expression;
    expression = newExpression;
    if (foundFuntions.Count > 0)
    {
        var MathFunctions = Reflector.GetSystemMathFunctions();
        MathFunctions =
        (from a in foundFuntions from b in MathFunctions where a == b.Name select b).ToList();
        var unMatchedFunctions = foundFuntions.Except(MathFunctions.Select(f => f.Name)).ToList();
        if (unMatchedFunctions.Count > 0)
        throw new Exception("表达式不正确,发现了不能识别的函数名:" + unMatchedFunctions.ToListString(";"));
        return MathFunctions;
    }
    return null;
}

先从原始表达式中提取出函数的方法还有另外一个好处:可以根据提取到的函数个数来动态生成具体的Finder实例的个数,不需要把23个数学函数全都实例化以避免浪费CPU时间。

找到了函数名还不够,还需要知道参数的个数才能生成匹配正则表达式,我们把函数和正则式存在Tuple<MethodInfo, string>结构中以便以后我们的Finder类使用:

//创建解析方法的正则表达式,如Sum(a,b,c)可以定义为(@Sum:a,b,c)
static List<Tuple<MethodInfo, string>> CreateFunctionInfoList(List<MethodInfo> methods)
{
    var methodMatchRegex = new List<Tuple<MethodInfo, string>>();
    foreach (var m in methods)
    {
        string matchExpression = string.Format("\\(@{0}:", m.Name);
        matchExpression = m.GetParameters().Aggregate(matchExpression, (current, p) => current + ("[a-j]+,"));
        matchExpression = matchExpression.Trim(',');
        matchExpression += "\\)";
        methodMatchRegex.Add(new Tuple<MethodInfo, string>(m, matchExpression));
    }
    return methodMatchRegex;
}

至此,FunctionFinder类也就水落石出了,是不是很简单:

/// <summary>
/// 函数解析器,如Sin(a)
/// </summary>
public class FunctionFinder : FinderBase
{
    public FunctionFinder()
    {
    //默认构造函数不能删除,因为Reflector会用到
    }
    public FunctionFinder(Tuple<MethodInfo, string> functionInfo)
    {
        FunctionInfo = functionInfo;
    }
    private Tuple<MethodInfo, string> FunctionInfo { get; set; }
    public override int Priority { get { return (int)FinderPriority.FunctionFinder; } }
    protected override string Rule
    {
        get { return FunctionInfo.Item2; }
    }
    protected override INode GenerateNode(string sourceExpression, string data, int index)
    {
        //data:"(@Sum:a,b,c)"
        string[] ids = data.Split(':')[1].Trim(')').Split(',');
        var nodes = ids.Select(id => Calculator.GetNode(id)).ToList();
        return new FunctionNode(nodes, index, data, FunctionInfo.Item1);
    }
}

至此,我们并未对以前的设计进行重构,但是已经检测到冲突,重构在所难免。上面我们提到Sin(1)这样的子式,与我们前面定义的SingleFinder(去括号)有冲突,所以需要在解析之前对表达式中的函数子式进行重构。目前都两种方法:

  1. 在CalculateExpression()中实现,需要修改Calculator类。
  2. 在IFinder及其继承者中提供重构接口,需要修改IFinder及其继承的类。

 很明显,对IFinder的重构会牵一发而动全身,但是为便于以后的扩展,我们还是需要重构,以下是重构后的IFinderFunctionFinder类:

public interface IFinder
{
    int Priority { get; }//不同的子式解析的优先级不一样,例如乘法要先于加法
    Calculator Calculator { set; }
    INode Find(string expression);
    void AdjustExpression(ref string expression, ref List<IFinder> finders);
}

/// <summary>
/// 函数解析器,如Sin(a)
/// </summary>
publicclass FunctionFinder : FinderBase
{
    //...
    //重构原始表达式,并生成每个函数的Finder实例
    public override void AdjustExpression(ref string expression, ref List<IFinder> finders)
    {
    var foundFunctions = FindFunctions(ref expression);
    if (foundFunctions != null)
    {
        var functionInfos = CreateFunctionInfoList(foundFunctions);
        var instances = functionInfos.Select(fi => new FunctionFinder(fi, Calculator)).Cast<IFinder>().ToList();
        finders = finders.Except(new List<IFinder> {this}).ToList();//当前类的职责已经结束,将其移除
        finders.AddRange(instances);//剩下的职责交给新的Finder们完成
    }
}

其实只是新加了AdjustExpression的接口和实现而已(严格来说这种设计还不完美,因为除了FunctionFinder,其他的类并没有使用它,我们完全可以将其独立出来做一个新的接口),逻辑在代码里面有注释,不多解释!

好了,最后还是要对CalculateExpression内部进行一些修改,因为AdjustExpression的调用还是由CalculateExpression来完成的,看代码:

public double CalculateExpression(List<IFinder> finders, string expression)
{
    AdjustInputExpression(ref expression);
    //
    finders.ForEach(f => f.Calculator = this);
    //此处可能改变了finders的集合,不能用foreach
    for (int i = finders.Count - 1; i >= 0; i--)
    finders[i].AdjustExpression(ref expression, ref finders);
    bool findOver = false
    ;
    while (!findOver)
    {
        //
    }
        //
}

 只是增加了对AdjustExpression的调用,其他的代码几乎没有动。

好了,编译调试运行,该部分功能顺利结束,运行结果如图:

 

下一部分我们将添加变量x进入表达式中,如Sin(x)等等,这又将是一场重构,有重构才叫模式正如有未知数才是数学!

【注意:FindBase.Find()方法中有个非常隐秘的bug,之所以隐秘是因为明明知道它是个bug却找不到一个Case去验证它】

粤ICP备12014928号-2

几何画板

编程

数学

联系