EDIT: Added refactored version 2.0 to the end!
I have a parser that is supposed to take a string similar to a math expression and return a Tuple of:
- An array of strings where each string corresponds to one 'element' (number, operator, func name).
- An array of custom enum indicating whether corresponding string in array is Name, Number, etc.
I have honestly no idea how Buffer like readers are usually implemented so I'm open to suggestions on how to make my class a little less messy (I particularly don't like the fact I'm using bufferPointer
in five different functions. The way I'm testing it I'm not out of my array in two places also doesn't seem to me as a good practice (though I couldn't find any good solution)).
using System;
using System.Collections.Generic;
using System.Text;
namespace MathExpressionSolver.Parser
{
class ExpressionParser
{
const int TKNLGHT = 4;
public bool SkipInvalidChars { get; set; } = true;
private StringBuilder charBuffer;
private int bufferPointer = 0;
private List<string> listOfParsedExpressions;
private List<ParsedSubstringType> listOfParsedTypes;
private string stringExpression;
public string StringExpression
{
set
{
listOfParsedExpressions.Clear();
listOfParsedExpressions.Capacity = stringExpression.Length / TKNLGHT;
listOfParsedTypes.Clear();
listOfParsedTypes.Capacity = stringExpression.Length / TKNLGHT;
stringExpression = value;
}
}
public ExpressionParser()
{
charBuffer = new StringBuilder(TKNLGHT);
listOfParsedExpressions = new List<string>();
listOfParsedTypes = new List<ParsedSubstringType>();
stringExpression = string.Empty;
}
public ExpressionParser(string expression) : this()
{
StringExpression = expression;
}
public Tuple<string[], ParsedSubstringType[]> ParseExpression()
{
parseExpression();
return new Tuple<string[], ParsedSubstringType[]>(listOfParsedExpressions.ToArray(), listOfParsedTypes.ToArray());
}
private void parseExpression()
{
listOfParsedExpressions.Clear();
listOfParsedTypes.Clear();
while (bufferPointer < stringExpression.Length)
{
parseNextToken();
}
}
private void parseNextToken()
{
charBuffer.Clear();
Func<char, bool> isTypeFunction = null;
ParsedSubstringType currentType;
bool isLong = false;
bool trash = false;
if (ParserHelper.IsNameChar(stringExpression[bufferPointer]))
{
addCurrCharToBuffer();
isTypeFunction = ParserHelper.IsNameChar;
currentType = ParsedSubstringType.Name;
isLong = true;
}
else if (ParserHelper.IsNum(stringExpression[bufferPointer]))
{
addCurrCharToBuffer();
isTypeFunction = ParserHelper.IsNum;
currentType = ParsedSubstringType.Num;
isLong = true;
}
else if (ParserHelper.IsWhiteSpace(stringExpression[bufferPointer]))
{
trashCurrChar();
isTypeFunction = ParserHelper.IsWhiteSpace;
currentType = ParsedSubstringType.WhiteSpace;
isLong = true;
trash = true;
}
else if (ParserHelper.IsLeftBracket(stringExpression[bufferPointer]))
{
addCurrCharToBuffer();
currentType = ParsedSubstringType.Bracket;
}
else if (ParserHelper.IsRightBracket(stringExpression[bufferPointer]))
{
addCurrCharToBuffer();
currentType = ParsedSubstringType.Bracket;
}
else if (ParserHelper.IsOperator(stringExpression[bufferPointer]))
{
addCurrCharToBuffer();
currentType = ParsedSubstringType.Operator;
}
else if (ParserHelper.IsSeparator(stringExpression[bufferPointer]))
{
addCurrCharToBuffer();
currentType = ParsedSubstringType.Separator;
}
else
{
trashCurrChar();
if (SkipInvalidChars) return;
else currentType = ParsedSubstringType.WhiteSpace;
}
if(isLong)
{
while(bufferPointer < stringExpression.Length && isTypeFunction(stringExpression[bufferPointer]))
{
if (trash) trashCurrChar();
else addCurrCharToBuffer();
}
}
listOfParsedExpressions.Add(charBuffer.ToString());
listOfParsedTypes.Add(currentType);
}
private void addCurrCharToBuffer()
{
charBuffer.Append(stringExpression[bufferPointer]);
bufferPointer++;
}
private void trashCurrChar()
{
bufferPointer++;
}
}
public enum ParsedSubstringType { Name, Num, Bracket, Operator, Separator, WhiteSpace };
public static class ParserHelper
{
public static bool IsNameChar(char a)
{
if(a == '_') return true;
else return char.IsLetter(a);
}
public static bool IsNum(char a)
{
if (a == '.') return true;
else return char.IsDigit(a);
}
public static bool IsLeftBracket(char a)
{
return (a == '(') ? true : false;
}
public static bool IsRightBracket(char a)
{
return (a == ')') ? true : false;
}
public static bool IsOperator (char a)
{
switch (a)
{
case '+':
return true;
case '-':
return true;
case '*':
return true;
case '/':
return true;
case '%':
return true;
case '=':
return true;
default:
return false;
}
}
public static bool IsSeparator(char a)
{
return (a == ';') ? true : false;
}
public static bool IsWhiteSpace(char a)
{
return (char.IsWhiteSpace(a)) ? true : false;
}
}
}
INPUT/OUTPUT testing:
INPUT: 2 & 3*(7- 3+2+-2 / 2(()*(2)
OUTPUT string[]: {2,,,3,*,(,7,-,,3,+,3,+,-,2,,/,,2,(,(,),*,(,2,)}
OUTPUT types[]: {Num, WhiteSpace, WhiteSpace, Num, Operator, Bracket, Num, Operator, ... }
Refactored version:
NOTES:
- I've kept camelCase naming for private methods names. The official naming convention talks only about public methods and camelCase really helps me with readability.
- I've separated parseNewExpression to it's own method as it seemed to me that there was too much code repetition.
- ParseExpression() is still without parameters because one should be able to Reparse previously added expression without the need to set it again (e.g. after SkipInvalidTokens settings change).
Significantly reduced ParseExpression methods (there was quite a lot of overlapping cuntionality). Now it's slower but waaay nicer (and who cares about speed anyway...)
public void ParseExpression() { ParsedSubstringType lastType = ParsedSubstringType.NotSet; parsedExpression.Clear(); parsedTypes.Clear(); StringBuilder charBuffer = new StringBuilder(avgTokenLength); foreach (char token in rawExpression) { ParsedSubstringType currentType = getTokenType(token); if(IsCoumpnoundable(lastType) && currentType != lastType) { parseNewExpression(charBuffer, lastType); } lastType = currentType; if (IsCoumpnoundable(currentType)) { charBuffer.Append(token); continue; } if (currentType == ParsedSubstringType.Invalid && SkipInvalidChars) { continue; } charBuffer.Append(token); parseNewExpression(charBuffer, currentType); } if (charBuffer.Length > 0) { parseNewExpression(charBuffer, lastType); } } private void parseNewExpression(StringBuilder charBuffer, ParsedSubstringType currentType) { string expression = (isTrashable(currentType)) ? string.Empty : charBuffer.ToString(); parsedTypes.Add(currentType); parsedExpression.Add(expression); charBuffer.Clear(); } private bool IsCoumpnoundable(ParsedSubstringType type) { return ( type == ParsedSubstringType.Num || type == ParsedSubstringType.Name || type == ParsedSubstringType.WhiteSpace ); } private bool isTrashable(ParsedSubstringType type) { return (type == ParsedSubstringType.WhiteSpace); }
TKNLGHT
? I keep interpreting it asToken Light
rather than what you probably intended asToken Length
, which in that case would beTKNLGTH
... which is irrelevant because it should beTokenLength
, or if you insist on all caps,TOKEN_LENGTH
. \$\endgroup\$