HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Mathematical expression evaluator.

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
expressionevaluatormathematical

Problem

Code reviews and suggestions to improve coding style are welcome.

```
using ExpressionEvaluatorLibrary;
namespace ExpressionEvaluator
{
class Program
{
static void Main(string[] args)
{
ConsoleKeyInfo cki = new ConsoleKeyInfo();

Console.WriteLine(" Mathematical Expression Evaluation");
Console.WriteLine("Maximum length : 250 characters");
Console.WriteLine("Allowed Operators : +, -, *, /");

do
{
string strUserInput = "";
if (args.Length == 0)
{
Console.Write("\n\nEnter an Expression: ");
strUserInput = Console.ReadLine();
}
else
{
// TO be able to run from command prompt.
strUserInput = args[0];
}

IExpressionModel expressionModel = new ExpressionModel();

string strValidate = expressionModel.ExpressionValidate(strUserInput);

if (strValidate == "Valid")
{
try
{
string strPostFixExpression = expressionModel.ConvertToPostfix(strUserInput);
// Console.WriteLine(strPostFixExpression);

string strResult = expressionModel.EvaluateExpression(strPostFixExpression);
Console.WriteLine("\n The result is: " + strResult);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
else
{
Console.WriteLine(strValidate);
}

Console.WriteLine("\nPress any key to continue; press the 'Esc' key to quit.");
cki = Console.ReadKey(false);

} while (cki.Key != ConsoleKey.Escape);
}
}
}

namespace ExpressionEvaluatorLibrary
{
public interface IExpressionModel
{
string ExpressionValidate(string strUserEntry);
strin

Solution

-
If I'm not mistaken your acceptable input expressions are in infix notation and look like this:

number [+-*/] number [+-*/] number ....


In that case you can greatly simplify you validation check with a single regular expression:

^\d+(\s*[+-*/]\s*\d+)+$


It checks for strings starting with a number composed out of 1 or more digits followed by 1 or more groups of [+-/] number. The \s allows any number of white spaces between operators and numbers.

-
When you validate you should not return a string representing the validation result. Two alternative options:

-
Return a ValidationResult object like this:

class ValidationResult
{
    bool readonly IsValid;
    string readonly FailureReason;

    private ValidationResult() { }

    public static ValidationResult Valid()
    {
        return new ValidationResult { IsValid = true; }
    }

    public static ValidationResult Invalid(string reason)
    {
        return new ValidationResult { IsValid = false; FailureReason = reason; }
    }
}


Use: return ValidationResult.Invalid("some error")

-
Throw a ValidationException with the message as the failure reason and catch it when calling the validation method and log/display the message to the user.

-
You are doing a lot of parsing and re-parsing by passing everything around as strings. This is not really a good way of doing it and also very inefficient as you are constantly throwing away information and having to reconstruct it via parsing it out of the string.

Here is how I would split it up:

  • Write a tokenizer which splits the input string into tokens (numbers and operators) and provides the next token to the parser



  • The parser should build the expression tree. The operators should be the nodes and the leaves are the numbers. This way you can traverse it anyway you like infix, postfix, prefix and you retain the information about the parsed input.



  • Evaluating just means to traverse the tree and evaluate the nodes and the operands.



Some skeleton code:

The nodes of the tree:

interface INode
{
    double Evaluate();
}

abstract class Operator : INode
{
    protected readonly INode Left;
    protected readonly INode Right;

    protected Operator(INode left, INode right)
    {
        Left = left;
        Right = right;
    }

    public abstract double Evaluate();

    public static Operator FromString(string op, INode left, INode right)
    {
        switch (op)
        {
            case "+": return new Add(left, right);
            case "-": return new Sub(left, right);
            case "*": return new Mul(left, right);
            case "/": return new Div(left, right);
            default: throw new ArgumentException("Invalid operator", "op");
        }
    }
}

class Add : Operator
{
     public Add(INode left, INode right) : base(left, right) {}

     public override double Evaluate()
     {
         return Left.Evaluate() + Right.Evaluate();
     }
}

// ...
// similar classes for Sub, Mul And Div 
// ...

class Constant : INode
{
    private readonly double _Number;

    public Constant(double number)
    {
         _Number = number;
    }

    public double Evaluate() 
    { 
         return _Number;
    }
}


Parsing (will only deal with valid input):

public IEnumerable Tokenize(string input)
{
    // assuming only spaces or tabs are allowed in the expression
    return string.Split(new [] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
}

public INode ParseTokens(IEnumerable tokens)
{ 
    INode left = new Constant(Double.Parse(tokens.First()));

    tokens = tokens.Skip(1);

    // last token of expression is always a number
    if (!tokens.Any()) return left;

    var op = tokens.First();

    // recursive descending
    INode right = ParseTokens(tokens.Skip(1));

    return Operator.FromString(op, left, right);
}


Actually evaluating the input becomes:

double EvaluateInput(string input)
{
    var tokens = Tokenize(input);
    var root ParseToken(tokens);
    return root.Evaluate();
}


Ideally you actually create a Tokenizer class which gets instantiated with the input string and where you can call Next() on which will return the next token. The tokenizer should be passed to the parse method. I just used simple IEnumerable because it was quicker to show the basic concept.

Code Snippets

number [+-*/] number [+-*/] number ....
^\d+(\s*[+-*/]\s*\d+)+$
class ValidationResult
{
    bool readonly IsValid;
    string readonly FailureReason;

    private ValidationResult() { }

    public static ValidationResult Valid()
    {
        return new ValidationResult { IsValid = true; }
    }

    public static ValidationResult Invalid(string reason)
    {
        return new ValidationResult { IsValid = false; FailureReason = reason; }
    }
}
interface INode
{
    double Evaluate();
}

abstract class Operator : INode
{
    protected readonly INode Left;
    protected readonly INode Right;

    protected Operator(INode left, INode right)
    {
        Left = left;
        Right = right;
    }

    public abstract double Evaluate();

    public static Operator FromString(string op, INode left, INode right)
    {
        switch (op)
        {
            case "+": return new Add(left, right);
            case "-": return new Sub(left, right);
            case "*": return new Mul(left, right);
            case "/": return new Div(left, right);
            default: throw new ArgumentException("Invalid operator", "op");
        }
    }
}

class Add : Operator
{
     public Add(INode left, INode right) : base(left, right) {}

     public override double Evaluate()
     {
         return Left.Evaluate() + Right.Evaluate();
     }
}

// ...
// similar classes for Sub, Mul And Div 
// ...

class Constant : INode
{
    private readonly double _Number;

    public Constant(double number)
    {
         _Number = number;
    }

    public double Evaluate() 
    { 
         return _Number;
    }
}
public IEnumerable<string> Tokenize(string input)
{
    // assuming only spaces or tabs are allowed in the expression
    return string.Split(new [] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
}

public INode ParseTokens(IEnumerable<string> tokens)
{ 
    INode left = new Constant(Double.Parse(tokens.First()));

    tokens = tokens.Skip(1);

    // last token of expression is always a number
    if (!tokens.Any()) return left;

    var op = tokens.First();

    // recursive descending
    INode right = ParseTokens(tokens.Skip(1));

    return Operator.FromString(op, left, right);
}

Context

StackExchange Code Review Q#26631, answer score: 3

Revisions (0)

No revisions yet.