ex-ex-01-math-interpreter/MathInterpreter/Core/ExpressionScanner.cs
2025-06-05 14:36:50 +02:00

196 lines
6.9 KiB
C#

using System;
namespace MathInterpreter.Core;
/// <summary>
/// Provides utility methods for scanning character streams for certain parts of a maths expression
/// </summary>
public static class ExpressionScanner
{
/// <summary>
/// Scans the given expression for an operator (+, -, *, /)
/// </summary>
/// <param name="expression">Expression to process</param>
/// <returns>A scan result containing the operator found and the remaining part of the expression</returns>
/// <exception cref="OperatorException">Thrown if none or an unknown operator is found</exception>
public static ScanResult<Operator> ScanOperator(ReadOnlySpan<char> expression)
{
int index = 0;
while (index < expression.Length && char.IsWhiteSpace(expression[index]))
{
index++;
}
if (index >= expression.Length)
{
throw new OperatorException("Empty expression");
}
char op = expression[index];
return op switch
{
'+' => new ScanResult<Operator>(Operator.Plus, 1, expression[(index + 1)..]),
'-' => new ScanResult<Operator>(Operator.Minus, 1, expression[(index + 1)..]),
'*' => new ScanResult<Operator>(Operator.Multiply, 1, expression[(index + 1)..]),
'/' => new ScanResult<Operator>(Operator.Divide, 1, expression[(index + 1)..]),
_ => throw new OperatorException($"Unknown operator: {op}")
};
}
/// <summary>
/// Scans the given expression for an operand which is a number which have to have an integral
/// part and may have a fractional part. It may also have a sign (+, -) in front of it.
/// </summary>
/// <param name="expression">Expression to process</param>
/// <returns>A scan result containing the parsed number and the remaining part expression</returns>
/// <exception cref="ExpressionFormatException">Thrown if a problem with the format of the number is found</exception>
public static ScanResult<double> ScanOperand(ReadOnlySpan<char> expression)
{
int index = 0;
while (index < expression.Length && char.IsWhiteSpace(expression[index]))
{
index++;
}
if (index >= expression.Length)
{
throw new ExpressionFormatException("Operand is missing");
}
if (expression[index] == '.')
{
throw new ExpressionFormatException("Unable to parse integral part of number");
}
if (!(char.IsDigit(expression[index]) ||
(index < expression.Length - 1 && (expression[index] == '+' || expression[index] == '-') && char.IsDigit(expression[index + 1]))))
{
throw new ExpressionFormatException("Operand is missing");
}
try
{
var integralResult = ScanNumber(expression, true);
int integralValue = integralResult.Value;
bool isNegative = integralValue < 0;
ReadOnlySpan<char> remaining = integralResult.RemainingExpression;
index = 0;
while (index < remaining.Length && char.IsWhiteSpace(remaining[index]))
{
index++;
}
if (index >= remaining.Length || remaining[index] != '.')
{
return new ScanResult<double>(integralValue, integralResult.Length, remaining);
}
ReadOnlySpan<char> fractionalPart = remaining[(index + 1)..];
try
{
var fractionalResult = ScanNumber(fractionalPart, false);
double fractionalValue = fractionalResult.Value / Math.Pow(10, fractionalResult.Length);
double result = Math.Abs(integralValue) + fractionalValue;
if (isNegative)
{
result = -result;
}
return new ScanResult<double>(result,
integralResult.Length + 1 + fractionalResult.Length,
fractionalResult.RemainingExpression);
}
catch (NumberFormatException)
{
throw new ExpressionFormatException("Unable to parse fractional part of number");
}
}
catch (NumberFormatException)
{
throw new ExpressionFormatException("Unable to parse integral part of number");
}
}
/// <summary>
/// Scans the given expression for a whole number which may have a sign (+, -) in front of it
/// </summary>
/// <param name="expression">Expression to process</param>
/// <param name="allowSign">Flag indicating if a pre-fix sign (+, -) is allowed or not</param>
/// <returns>A scan result containing the parsed number and the remaining part expression</returns>
/// <exception cref="NumberFormatException">Thrown if no or an invalid number is found</exception>
public static ScanResult<int> ScanNumber(ReadOnlySpan<char> expression, bool allowSign)
{
int index = 0;
int sign = 1;
while (index < expression.Length && char.IsWhiteSpace(expression[index]))
{
index++;
}
if (index < expression.Length)
{
if (expression[index] == '+' || expression[index] == '-')
{
if (!allowSign)
{
throw new NumberFormatException(expression, "Sign is not allowed here");
}
if (expression[index] == '-')
{
sign = -1;
}
index++;
while (index < expression.Length && char.IsWhiteSpace(expression[index]))
{
index++;
}
if (index < expression.Length && (expression[index] == '+' || expression[index] == '-'))
{
throw new NumberFormatException(expression, "Sign may occur only once");
}
}
}
int value = 0;
int digitCount = 0;
while (index < expression.Length)
{
char c = expression[index];
if (char.IsDigit(c))
{
int digit = (int)char.GetNumericValue(c);
value = value * 10 + digit;
index++;
digitCount++;
}
else if (char.IsWhiteSpace(c))
{
index++;
}
else
{
break;
}
}
if (digitCount == 0)
{
throw new NumberFormatException(expression);
}
return new ScanResult<int>(value * sign, digitCount, expression[index..]);
}
}