Structure aids understanding
Variable names, functions, classes... most of the code you actually write gets thrown away by the compiler, and is much more about readability and maintainability than anything else. Well structured code is also easier to spot holes in, easier to debug, etc.
So let's start by isolating the maths bit:
double evaluateSimpleExpression(double a, char op, double b)
{
switch (op)
{
case '+': return a+b;
case '-': return a-b;
case 'x': return a*b;
case '*': return a*b;
case '/': return a/b;
default:
throw std::invalid_argument::invalid_argument("Invalid operator");
}
}
This collects the logic for the actual expression evaluation given some input values, and also throws an error if the operator isn't valid. We've excluded the logic for getting user inputs in, so we could easily point this at a file of inputs or some other format with minimal effort. Using a standard error like this makes it easier to understand the type of error (just need to #include <stdexcept>
).
It's still not quite free of the worries about inputs, which we can improve thus:
enum MathOp
{
Plus = 0,
Minus,
Multiply,
Divide,
//...
MathOp__LAST
};
MathOp interpretOperatorChar(char op)
{
switch(op)
{
case '+': return Plus;
case '-': return Minus;
case 'x': return Multiply;
case '*': return Multiply;
case '/': return Divide;
default: return MathOp__LAST;
}
}
double evaluateSimpleExpression(double a, MathOp mop, double b)
{
switch(mop)
{
case Plus: return a + b;
case Minus: return a - b;
case Multiply: return a * b;
case Divide: return a / b;
default:
throw std::invalid_argument::invalid_argument("Invalid operator");
}
}
Now we have separated the input-specific part (the characters used for operations, and the fact that there are two ways of entering a multiply) from the logic to perform the operation. If you wanted to add in a power operator '^' then hopefully it would be clear what to add.
Clearly the second sample here is somewhat overkill for a little math processing, but it is the kind of structure you'd expect to see for a more comprehensive application; it would be easy, for example, to change the characters used for operations, or to interpret the expression and store it to evaluation later.
Separation of concerns
We have separated the logic from the specification of the inputs - we could easily run the evaluation function with completely different inputs like '10111PLUS11000'.
You'll notice that the operator interpreting function returns a non-value if it can't find the operator in its switch, and that the evaluation function throws if it can't process the given operator. These are the two different ways of coping with invalid inputs - we will use the interpret method generally, and then only evaluate the expression if we think we have a valid operator; the throw is there in case something goes exceptionally badly.
int main()
{
cout << "Calculator..... " << endl;
cout << "Type '3a3' to exit... " << endl;
while(true)
{
double a=0 ,b=0;
char op = 'a';
bool parsedOK = cin >> a >> op >> b;
if(!parsedOK)
{
cout << "Could not parse expression, expected form a + b"
<< endl;
continue;
}
// 'a' is the exit operator, the value of the integers doesn't matter
if(op == 'a')
break;
// attempt to parse operator
MathOp gotOperator = interpretOperatorChar(op);
if(gotOperator == MathOp__LAST)
{
cout << "Operator " << op << " invalid" << endl;
continue;
}
// have a valid operator, attempt operation
double result = _nan();
try
{
result = evaluateSimpleExpression(a, gotOperator, b);
}
catch(std::invalid_argument e)
{
cout << "Operation failed: " << e.what();
continue;
}
// print interpreted expression and result
cout << a << " " << op << " " << b << " = " << result << endl;
}
return 0;
}
With this structure, we can progress through parsing and evaluating an expression in stages, and give appropriate errors that should inform the user. Now, we haven't included the divide by zero, but it's easy to see where to put it - it is the job of evaluateSimpleExpression
to actually do the maths, so we should put it there:
double evaluateSimpleExpression(double a, MathOp mop, double b)
{
switch(mop)
case Plus: return a + b;
case Minus: return a - b;
case Multiply: return a * b;
case Divide:
{
if(b == 0)
throw std::invalid_argument::invalid_argument("Attempt to divide by zero");
return a / b;
}
default:
throw std::invalid_argument::invalid_argument("Invalid operator");
}
If, for example, we added a 'root' operator to get the bth root of a, we could add conditions for valid inputs (like only positive values of b, or only positive values of a when b is even), again providing error messages that give as much contextual information to the user as would help them understand it.