Original question:
My 'teacher' requested this problem be solved by parsing the input into a tree based on the order of operations. The class Calc variables are specific to his instruction (except precedence) as well as the getValue()
method. Everything else is subject to review.
I'm hoping to receive detailed input on how to make my code meet best practices as closely as possible in the perspective of a much larger program to be more familiar with programs to come.
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace calcTree
{
class Program
{
static void Main(string[] args)
{
// Various prompt and response strings
const string WELCOME = "Welcome to the Calculator!";
const string INSTRUCTIONS = "\nPlease enter an equation (do not use '=' or parenthesis):" +
"\nEnter \"exit\" or \"bye\" to quit.\n";
const string INVALID_INPUT = "\nYour input was invalid.";
const string EXIT = "\nGoodbye!";
string[] EXIT_KEY = { "exit", "bye" };
Console.WriteLine(WELCOME);
while (true)
{
Console.WriteLine(INSTRUCTIONS);
string equation = Console.ReadLine(); // Read equation
equation = equation.Replace(" ", String.Empty); // Remove spaces
// Check for program exit keywords
if (equation.EndsWith(EXIT_KEY[0], StringComparison.OrdinalIgnoreCase) ||
equation.EndsWith(EXIT_KEY[1], StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(EXIT);
break;
}
// Validate the equation
else if (!Calc.ValidateEquation(equation))
{
Console.WriteLine(INVALID_INPUT);
}
// Solve the equation
else
{
string[] answers = Calc.Solve(equation);
for (int i = 0; i < answers.Length; i++) // Print the answer
{
Console.WriteLine(answers[i]);
}
}
}
Console.Read(); // Waits for 'enter' to end.
}
}
}
CalcTree.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace calcTree
{
class Calc
{
char oper;
decimal num;
int precedence; // Assists with print order, not the actual calculation.
Calc left;
Calc right;
/// <summary>
/// Tests for NULL, that all chars are numbers or operators and that the last
/// char is a number.
/// </summary>
/// <param name="equation">User input equation</param>
/// <returns>True if valid</returns>
public static bool ValidateEquation(string equation)
{
if (String.IsNullOrWhiteSpace(equation) || // test for null
!Char.IsNumber(equation[equation.Length-1])) // last char must be a number
{
return false;
}
// All chars must be a number or operator
for (int i = 0; i < equation.Length; i++)
{
if (!Char.IsNumber(equation[i]))
{
if (equation[i] != '*' &&
equation[i] != '/' &&
equation[i] != '+' &&
equation[i] != '-' &&
equation[i] != '.')
{
return false;
}
}
}
return true;
}
/// <summary>
/// Solves the equation
/// </summary>
/// <param name="equation">String of valid numbers and operators</param>
/// <returns>String[] of the solution in the order of operations</returns>
public static string[] Solve(string equation)
{
string[] parsedInput = ParseEquation(equation);
Calc[] logicTree = AllocateLogicTree(parsedInput);
logicTree = ApplyLeftLogic(logicTree);
logicTree = ApplyRightLogic(logicTree);
string[] answer = SolveLogicTree(logicTree);
return answer;
}
/// <summary>
/// Parse input string into an array so that assignments to the logic tree are clear.
/// </summary>
/// <param name="equation">User input equation</param>
/// <returns>string[] containing separated numbers and operators</returns>
static string[] ParseEquation(string equation)
{
int size = 0; // "word" count for array size
for (int i = 0; i < equation.Length; i++)
{
if (i + 1 < equation.Length)
{
if (equation[i + 1] == '*' || equation[i + 1] == '/' || equation[i + 1] == '+' || equation[i + 1] == '-')
size++;
}
}
size = size * 2 + 1; // operators * 2 + 1 == array positions required
string[] parsedInput = new string[size]; // Allocates array size
// Parse string into array by numbers and operators
StringBuilder sb = new StringBuilder();
size = 0;
for (int j = 0; j < equation.Length; j++)
{
if (equation[j] != '*' && equation[j] != '/' && equation[j] != '+' && equation[j] != '-')
{
sb.Append(equation[j]);
}
else
{
parsedInput[size] = sb.ToString(); // Add number to array
sb.Clear();
size++;
parsedInput[size] = equation[j].ToString(); // Add operator to array
size++;
}
}
parsedInput[size] = sb.ToString(); // Add last number to array
return parsedInput;
}
/// <summary>
/// Assigns string[] to eqaul sized Calc[], string[] value is either Calc.oper or Calc.num.
/// </summary>
/// <param name="parsedEquation">String[] containing separated numbers and operators</param>
/// <returns>Calc[] with either .num or .oper assigned to each index</returns>
static Calc[] AllocateLogicTree(string[] parsedEquation)
{
Calc[] logicTree = new Calc[parsedEquation.Length];
for (int i = 0; i < parsedEquation.Length; i++)
logicTree[i] = new Calc();
for (int j = 0; j < parsedEquation.Length; j++)
{
if (parsedEquation[j] == "*" || parsedEquation[j] == "/" || parsedEquation[j] == "+" || parsedEquation[j] == "-")
{
logicTree[j].oper = Char.Parse(parsedEquation[j]);
}
else
{
logicTree[j].num = Decimal.Parse(parsedEquation[j]);
}
}
return logicTree;
}
/// <summary>
/// Applies the order of operations logic for the .left branches.
/// </summary>
/// <param name="logicTree">Each index contains .num or .oper</param>
/// <returns>logicTree with .left assigned and precedence.</returns>
static Calc[] ApplyLeftLogic(Calc[] logicTree)
{
// .left requirements & assign initial values before .right can be determined for + and -.
for (int i = 0; i < logicTree.Length; i++)
{
if (logicTree[i].oper == '*' || logicTree[i].oper == '/')
{
// Previous oper must be + or - // else .left = previous * or / oper
if (i - 2 >= 0) // bounds checking
{
if (logicTree[i - 2].oper == '+' || logicTree[i - 2].oper == '-')
logicTree[i].left = logicTree[i - 1];
else // previous operator must be * or /
logicTree[i].left = logicTree[i - 2];
}
else logicTree[i].left = logicTree[i - 1];
logicTree[i].right = logicTree[i + 1]; // always
logicTree[i].precedence = 1; // Calculate this 1st
}
else if (logicTree[i].oper == '+' || logicTree[i].oper == '-')
{
// Previous oper must not exist or link to previous + or -
if (i - 2 >= 0) // bounds checking
{
for (int j = i - 2; j >= 0; j--)
{
if (logicTree[j].oper == '+' || logicTree[j].oper == '-')
{
logicTree[i].left = logicTree[j];
break;
}
j--;
}
if (logicTree[i].left == null) // logicTree[l - 2] must be the last * or / & the correct assignment
logicTree[i].left = logicTree[i - 2];
}
else logicTree[i].left = logicTree[i - 1];
// wait to assign .right
logicTree[i].precedence = 2; // Calculate this 2nd
}
}
return logicTree;
}
/// <summary>
/// Applies the order of operations logic for the .right branches.
/// </summary>
/// <param name="logicTree">Each index contains .num or .oper</param>
/// <returns>logicTree with .right assigned.</returns>
static Calc[] ApplyRightLogic(Calc[] logicTree)
{
// .right requirements for + and -
for (int i = 1; i < logicTree.Length; i++)
{
if (logicTree[i].oper == '+' || logicTree[i].oper == '-')
{
// if tree.oper + 2 == * or /, check next tree.oper and assign .right to the last consecutive * or /
if (i + 2 < logicTree.Length) // bounds checking
{
if (logicTree[i + 2].oper == '*' || logicTree[i + 2].oper == '/')
{
int j; // represents last * or /
for (j = i + 2; j < logicTree.Length; j++) // assign .right to last consecutive * or /
{
if (logicTree[j].oper != '*' && logicTree[j].oper != '/')
{
logicTree[i].right = logicTree[j - 2];
break;
}
j++;
}
if (logicTree[i].right == null) // if not assigned from the for loop, last * or / must be (o - 2)
logicTree[i].right = logicTree[j - 2];
}
else logicTree[i].right = logicTree[i + 1];
}
else logicTree[i].right = logicTree[i + 1];
}
i++;
}
return logicTree;
}
/// <summary>
/// Returns decimal value or if performed on an operator returns the solution of
/// left (operator) right.
/// </summary>
/// <returns>num value or solution</returns>
decimal GetValue()
{
if (oper == 0) // tests for operator
return num;
else
switch (oper)
{ // recursively calculates down the "tree", logical order determined in applyLogic()
case '+':
return left.GetValue() + right.GetValue();
case '-':
return left.GetValue() - right.GetValue();
case '*':
return left.GetValue() * right.GetValue();
case '/':
return left.GetValue() / right.GetValue();
default: return 0; // never returns 0
}
}
/// <summary>
/// Solves logicTree by the order of operations.
/// </summary>
/// <param name="logicTree">.left, .right, and precedence are assigned</param>
/// <returns>string[] of answers in the order of operations</returns>
static string[] SolveLogicTree(Calc[] logicTree)
{
int answersIndex = 1; // 1 extra index for final answer
for (int i = 0; i < logicTree.Length; i ++)
{
if (logicTree[i].oper != '\0')
answersIndex++;
}
string[] answers = new string[answersIndex];
answersIndex = 0;
int precedence = 1; // starts with * and /
while (precedence != 3)
{
for (int j = 0; j < logicTree.Length; j++)
{
if (logicTree[j].oper != 0 && logicTree[j].precedence == precedence)
answers[answersIndex] = "\n" + logicTree[j].left.GetValue() + " " + logicTree[j].oper + " " + logicTree[j].right.GetValue() + " = " + logicTree[j].GetValue();
}
precedence++;
}
answers[answers.Length - 1] = "\nThe answer is: " + logicTree[logicTree.Length - 2].GetValue();
return answers;
}
}
}