⚠️ Warning: This is a draft ⚠️
This means it might contain formatting issues, incorrect code, conceptual problems, or other severe issues.
If you want to help to improve and eventually enable this page, please fork RosettaGit's repository and open a merge request on GitHub.
The C# language does not directly contain an Eval function for evaluating a string as a math expression, though there are still a number of ways to go about it.
You could, for example, use the CodeDOM to dynamically compile an object that contains the expression string.
Or, while not necessarily a good coding practice, but certainly a short and simple route, you could use System.Xml.XPath.XPathNavigator.Evaluate(string xpath) as shown here:
public class XPathEval : I24MathParser {
public float Evaluate(string expression) {
System.Xml.XPath.XPathNavigator navigator =
new System.Xml.XPath.XPathDocument(new System.IO.StringReader("<r/>")).CreateNavigator();
//expath evaluator needs
// '/' expressed as "div"
// '%' expressed as "mod"
string xpathExpression = expression.Replace("/", " div ").Replace("%", " mod ");
float answer = Convert.ToSingle(navigator.Evaluate(String.Format("number({0})", xpathExpression)));
return answer;
}
}
The XPathEval class is implementing this interface to facilitate swapping out Evaluate providers:
interface I24MathParser {
float Evaluate(string expression);
}
Here is a more verbose, native solution - a lightweight math expression parser for evaluating 24 Game user input:
/// <summary>
/// Lightweight math parser - C# does not have an Evaluate function
/// </summary>
public class MathParser : I24MathParser {
//used to translate brackets to implied multiplication - i.e. "3(4)5(6)" will be interpreted as "3*(4)*5*(6)"
private const string bracketsPattern = @"(?<=[0-9)])(?<rightSide>\()|(?<=\))(?<rightSide>[0-9])";
//finds multiplication or division sub expression - i.e. "4*8-4*2)" yields {"4*8", "4*2"}
private const string multiplyDividePattern = @"[0-9]+[/*][0-9]+";
//finds bracketed expressions - i.e. "(4+30)(10-1)" yields {"4+30", "10-1"}
private const string subExpressionPattern = @"\(([0-9/*\-+]*)\)";
//splits expression into it elements - i.e. "4+-30-4.123" yields {"4", "+", "-30" ,"-", "4.123"}
private const string tokenPattern = @"(?:(?<=[/*\-+]|^)[+-]?)?(?:[0-9]+(?:\.[0-9]*)?)|[/*\-+]";
Regex brackets;
Regex multiplyDivide;
Regex subExpression;
Regex token;
public MathParser() {
//initialize reusable regular expressions
brackets = new Regex(bracketsPattern, RegexOptions.Compiled);
subExpression = new Regex(subExpressionPattern, RegexOptions.Compiled);
token = new Regex(tokenPattern, RegexOptions.Compiled);
multiplyDivide = new Regex(multiplyDividePattern, RegexOptions.Compiled);
}
public float Evaluate(string input) {
//brackets with no operator implies multiplication
string equation = brackets.Replace(input, "*${rightSide}");
float answer = Solve(equation);
return answer;
}
float Solve(string equation) {
//carry out order of operations
// bracketed subexpressions - for any operator
equation = SolveSubExpressions(subExpression, equation);
// multiplication and division
equation = SolveSubExpressions(multiplyDivide, equation);
// addition and subtraction
float answer = ParseEquation(equation);
return answer;
}
string SolveSubExpressions(Regex subExpression, string equation) {
float subResult;
Match match = subExpression.Match(equation);
while (match.Success) {
if (match.Groups[1].Length > 0) {
//recursively solve for subexpressions -- match group 1 excludes outer brackets
subResult = Solve(match.Groups[1].Value);
}
else {
//no more nested expressions - get final result for this subExpression
subResult = ParseEquation(match.Value);
}
//replace subexpression with resolved answer
equation = equation.Replace(match.Value, subResult.ToString());
//retest updated equation string
match = subExpression.Match(equation);
}
return equation;
}
float ParseEquation(string equation) {
Match match = token.Match(equation);
float leftSide = leftSide = float.Parse(match.Value);
string symbol;
float rightSide;
match = match.NextMatch();
while (match.Success) {
symbol = match.Value;
match = match.NextMatch();
if (match.Success)
{
rightSide = float.Parse(match.Value);
leftSide = Calculate(leftSide, symbol, rightSide);
match = match.NextMatch();
}
}
return leftSide;
}
float Calculate(float leftSide, string symbol, float rightSide) {
float answer;
switch (symbol) {
case "/":
answer = leftSide / rightSide;
break;
case "*":
answer = leftSide * rightSide;
break;
case "-":
answer = leftSide - rightSide;
break;
case "+":
answer = leftSide + rightSide;
break;
default:
throw new ArgumentException();
}
return answer;
}
}
This is the main class that handles puzzle generation and user interaction
/// <summary>
/// The Game. Handles user interaction and puzzle generation.
/// </summary>
class TwentyFourGame {
//puzzle parameters
private const int listSize = 4;
private const int minValue = 1;
private const int maxValue = 9;
//signals end of game
private const string quitToken = "Q";
//the only valid puzzle solution
private const float targetValue = 24;
//Regular Expressions for evaluating math input
private const string dictionaryBlacklistPattern = @"[^1-9/*\-+()]";
private const string inputDigitsPattern = @"(?:(?<=[+-]|^)[+-]?)?(?:[0-9]+(?:\.[0-9]*)?)";
Regex dictionaryBlackList;
Regex inputDigits;
I24MathParser mathParser;
public TwentyFourGame() {
//initialize reusable regular expressions
dictionaryBlackList = new Regex(dictionaryBlacklistPattern, RegexOptions.Compiled);
inputDigits = new Regex(inputDigitsPattern, RegexOptions.Compiled);
//define instance of math evaluator provider
//custom parser
//mathParser = new MathParser();
//xpath parser
mathParser = new XPathEval();
}
static void Main(string[] args) {
TwentyFourGame game = new TwentyFourGame();
game.PrintInstructions();
game.PlayGame();
}
void PlayGame() {
string input;
bool endGame = false;
//repeat play cycle until user signals the end
do {
string puzzle = GetPuzzle();
bool isValid = false;
//continue prompting user until valid input is received
do {
float answer;
string message = String.Empty;
try {
//show user puzzle and get read their solution
input = GetInput(puzzle);
if (input.Length == 0) {
//skip current puzzle - perhaps there is no solution
isValid = true;
message = "Skipping this puzzle";
}
else if (String.Compare(input, quitToken, true) == 0) {
//user wishes to quit
isValid = true;
message = "End Game";
}
else if (ValidateInput(input, puzzle)) {
//interpret user input and calculate answer
answer = mathParser.Evaluate(input);
if (answer == targetValue) {
isValid = true;
message = String.Format("Good work. {0} = {1}.", input, answer);
}
else {
isValid = false;
message = String.Format("Incorrect. {0} = {1}. Try again.", input, answer);
}
}
else {
isValid = false;
message = "Invalid input. Try again.";
}
}
catch {
message = "An error occurred. Check your input and try again.";
isValid = false;
}
finally {
PrintMessage(message);
PrintMessage(String.Empty);
PrintMessage(String.Empty);
}
} while (!isValid);
} while (!endGame);
//pause
GetInput(String.Empty);
}
bool ValidateInput(string input, string puzzle) {
bool isValid;
if (dictionaryBlackList.IsMatch(input)) {
//illegal characters used
isValid = false;
}
else {
//get inputted digits and compare to those in puzzle
string inputNumbers = String.Join(" ", from Match m in inputDigits.Matches(input) orderby float.Parse(m.Value) select m.Value);
isValid = inputNumbers.CompareTo(puzzle) == 0;
}
return isValid;
}
string GetPuzzle() {
int[] digits = new int[listSize];
//randomly choose 4 digits (from 1 to 9)
Random rand = new Random();
for (int i = 0; i < digits.Length; i++) {
digits[i] = rand.Next(minValue, maxValue);
}
//format for display
Array.Sort(digits);
string puzzle = String.Join(" ", digits);
return puzzle;
}
string GetInput(string prompt) {
Console.Write(String.Concat(prompt, ": "));
return Console.ReadLine();
}
void PrintMessage(string message) {
Console.WriteLine(message);
}
void PrintInstructions() {
PrintMessage("--------------------------------- 24 Game ---------------------------------");
PrintMessage(String.Empty);
PrintMessage("------------------------------- Instructions ------------------------------");
PrintMessage("Four digits will be displayed.");
PrintMessage("Enter an equation using all of those four digits that evaluates to 24");
PrintMessage("Only * / + - operators and () are allowed");
PrintMessage("Digits can only be used once, but in any order you need.");
PrintMessage("Digits cannot be combined - i.e.: 12 + 12 when given 1,2,2,1 is not allowed");
PrintMessage("Submit a blank line to skip the current puzzle.");
PrintMessage("Type 'Q' to quit");
PrintMessage(String.Empty);
PrintMessage("Example: given 2 3 8 2, answer should resemble 8*3-(2-2)");
PrintMessage("---------------------------------------------------------------------------");
PrintMessage(String.Empty);
PrintMessage(String.Empty);
}
}
Example output:
--------------------------------- 24 Game ---------------------------------
------------------------------- Instructions ------------------------------
Four digits will be displayed.
Enter an equation using all of those four digits that evaluates to 24
Only * / + - operators and () are allowed
Digits can only be used once, but in any order you need.
Digits cannot be combined - i.e.: 12 + 12 when given 1,2,2,1 is not allowed
Submit a blank line to skip the current puzzle.
Type 'Q' to quit
Example: given 2 3 8 2, answer should resemble 8*3-(2-2)
---------------------------------------------------------------------------
1 3 3 4: 4*(3-1)*3
Good work. 4*(3-1)*3 = 24.
1 3 5 6: