196 lines
6.9 KiB
C#
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..]);
|
|
}
|
|
}
|