Fastest way to solve chain-calculations

后端 未结 5 821
长发绾君心
长发绾君心 2021-02-04 07:14

I have a input like

string input = \"14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34\"; 
// up to 10MB string size

int result = Calc(input); // 11
         


        
5条回答
  •  被撕碎了的回忆
    2021-02-04 07:29

    NOTE

    Per comments, this answer does not give a performant solution. I'll leave it here as there are points to be considered / which may be of interest to others finding this thread in future; but as people have said below, this is far from the most performant solution.


    Original Answer

    The .net framework already supplies a way to handle formulas given as strings:

    var formula = "14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34";
    var result = new DataTable().Compute(formula, null);
    Console.WriteLine(result); //returns 139.125490196078
    

    Initial feedback based on comments

    Per the comments thread I need to point out some things:

    Does this work the way I've described?

    No; this follows the normal rules of maths.

    I assume that your amended rules are to simplify writing code to handle them, rather than because you want to support a new branch of mathematics? If that's the case, I'd argue against that. People will expect things to behave in a certain way; so you'd have to ensure that anyone sending equations to your code was primed with the knowledge to expect the rules of this new-maths rather than being able to use their existing expectations.

    There isn't an option to change the rules here; so if your requirement is to change the rules of maths, this won't work for you.

    Is this the Fastest Solution

    No. However it should perform well given MS spend a lot of time optimising their code, and so will likely perform faster than any hand-rolled code to do the same (though admittedly this code does a lot more than just support the four main operators; so it's not doing exactly the same).

    Per MatthewWatson's specific comment (i.e. calling the DataTable constructor incurs a significant overhead) you'd want to create and then re-use one instance of this object. Depending on what your solution looks like there are various ways to do that; here's one:

    interface ICalculator //if we use an interface we can easily switch from datatable to some other calulator; useful for testing, or if we wanted to compare different calculators without much recoding
    {
        T Calculate(string expression) where T: struct;
    }
    class DataTableCalculator: ICalculator 
    {
        readonly DataTable dataTable = new DataTable();
        public DataTableCalculator(){}
        public T Calculate(string expression) where T: struct =>
            (T)dataTable.Compute(expression, null);
    }
    
    class Calculator: ICalculator
    {
        static ICalculator internalInstance;
        public Calculator(){}
        public void InitialiseCalculator (ICalculator calculator)
        {
            if (internalInstance != null)
            {
                throw new InvalidOperationException("Calculator has already been initialised");
            }
            internalInstance = calculator;
        }
        public T Calculate(string expression) where T: struct =>
            internalInstance.Calculate(expression);
    }
    
    //then we use it on our code
    void Main()
    {
        var calculator1 = new Calculator();
        calculator1.InitialiseCalculator(new DataTableCalculator());
        var equation = "14 + 2 * 32 / 60 + 43 - 7 + 3 - 1 + 0 * 7 + 87 - 32 / 34"; 
        Console.WriteLine(calculator1.Calculate(equation)); //139.125490196078
        equation = "1 + 2 - 3 + 4";
        Console.WriteLine(calculator1.Calculate(equation)); //4
        calculator1 = null;
        System.GC.Collect(); //in reality we'd pretty much never do this, but just to illustrate that our static variable continues fro the life of the app domain rather than the life of the instance
        var calculator2 = new Calculator();
        //calculator2.InitialiseCalculator(new DataTableCalculator()); //uncomment this and you'll get an error; i.e. the calulator should only be initialised once.
        equation = "1 + 2 - 3 + 4 / 5 * 6 - 7 / 8 + 9";
        Console.WriteLine(calculator2.Calculate(equation)); //12.925
    }
    

    NB: The above solution uses a static variable; some people are against use of statics. For this scenario (i.e. where during the lifetime of the application you're only going to require one way of doing calculations) this is a legitimate use case. If you wanted to support switching the calculator at runtime a different approach would be required.


    Update after Testing & Comparing

    Having run some performance tests:

    • The DataTable.Compute method's biggest problem is that for equations the size of which you're dealing with it throws a StackOverflowException; (i.e. based on your equation generator's loop for (int i = 0; i < 1000000; i++).
    • For a single operation with a smaller equation (i < 1000), the compute method (including constructor and Convert.ToInt32 on the double result) takes almost 100 times longer.
    • for the single operation I also encountered overflow exceptions more often; i.e. because the result of the operations had pushed the value outside the bounds of supported data types...
    • Even if we move the constructor/initialise call outside of the timed area and remove the conversion to int (and run for thousands of iterations to get an average), your solution comes in 3.5 times faster than mine.

    Link to the docs: https://msdn.microsoft.com/en-us/library/system.data.datatable.compute%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396

提交回复
热议问题