This commit is contained in:
MarcUs7i 2025-06-05 14:36:50 +02:00
parent 0643e89b36
commit 3adf8b4c50
4 changed files with 300 additions and 17 deletions

View file

@ -2,4 +2,59 @@
namespace MathInterpreter.Core;
// TODO implement all exception classes
public abstract class ExpressionException : Exception
{
/// <summary>
/// Initializes <see cref="ExpressionException"/> with the specified error <paramref name="message"/> and the optional <paramref name="innerException"/>
/// </summary>
/// <param name="message">The error message</param>
/// <param name="innerException">The optional exception</param>
protected ExpressionException(string message, Exception? innerException = null)
: base(message, innerException) { }
}
public sealed class ExpressionFormatException : ExpressionException
{
/// <summary>
/// Initializes <see cref="ExpressionFormatException"/> with the specified error <paramref name="message" /> and the optional <paramref name="innerException"/>
/// </summary>
/// <param name="message">The error message</param>
/// <param name="innerException">The innerException</param>
public ExpressionFormatException(string message, Exception? innerException = null)
: base(message, innerException) { }
}
public sealed class OperatorException : ExpressionException
{
/// <summary>
/// Initializes <see cref="OperatorException"/> with the specified error <paramref name="message"/>
/// </summary>
/// <param name="message">The error message</param>
public OperatorException(string message) : base(message) { }
}
public sealed class NumberFormatException : ExpressionException
{
/// <summary>
/// Initializes <see cref="NumberFormatException"/> with the <paramref name="expression"/> and the optional <paramref name="reason"/>
/// </summary>
/// <param name="expression">The expression</param>
/// <param name="reason">The optional reason</param>
public NumberFormatException(ReadOnlySpan<char> expression, string? reason = null)
: base(FormatMessage(expression, reason)) { }
private static string FormatMessage(ReadOnlySpan<char> expression, string? reason = null)
{
reason = string.IsNullOrWhiteSpace(reason) ? string.Empty : $" ({reason})";
return $"Unable to read number from beginning of '{expression}'{reason}";
}
}
public sealed class NumberValueException : ExpressionException
{
/// <summary>
/// Initializes <see cref="NumberValueException"/> with the specified error <paramref name="message"/>
/// </summary>
/// <param name="message">The error message</param>
public NumberValueException(string message) : base(message) { }
}

View file

@ -15,8 +15,28 @@ public static class ExpressionScanner
/// <exception cref="OperatorException">Thrown if none or an unknown operator is found</exception>
public static ScanResult<Operator> ScanOperator(ReadOnlySpan<char> expression)
{
// TODO
throw new NotImplementedException();
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>
@ -28,8 +48,74 @@ public static class ExpressionScanner
/// <exception cref="ExpressionFormatException">Thrown if a problem with the format of the number is found</exception>
public static ScanResult<double> ScanOperand(ReadOnlySpan<char> expression)
{
// TODO
throw new NotImplementedException();
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>
@ -41,7 +127,70 @@ public static class ExpressionScanner
/// <exception cref="NumberFormatException">Thrown if no or an invalid number is found</exception>
public static ScanResult<int> ScanNumber(ReadOnlySpan<char> expression, bool allowSign)
{
// TODO
throw new NotImplementedException();
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..]);
}
}

View file

@ -8,13 +8,10 @@ namespace MathInterpreter.Core;
/// </summary>
public sealed class MathExpression
{
// TODO
/*
private double? _leftOperand;
private Operator? _operator;
private double? _result;
private double? _rightOperand;
*/
/// <summary>
/// Creates a new instance of <see cref="MathExpression" /> from the provided expression string.
@ -39,8 +36,29 @@ public sealed class MathExpression
{
get
{
// TODO
throw new NotImplementedException();
if (_result.HasValue)
{
return _result.Value;
}
if (!_leftOperand.HasValue || !_operator.HasValue || !_rightOperand.HasValue)
{
throw new UnreachableException("Expression is not fully parsed");
}
double left = _leftOperand.Value;
double right = _rightOperand.Value;
_result = _operator.Value switch
{
Operator.Plus => left + right,
Operator.Minus => left - right,
Operator.Multiply => left * right,
Operator.Divide => left / right,
_ => throw new UnreachableException("Unknown operator")
};
return _result.Value;
}
}
@ -55,13 +73,69 @@ public sealed class MathExpression
/// </exception>
public override string ToString()
{
// TODO
throw new NotImplementedException();
if (!_leftOperand.HasValue || !_operator.HasValue || !_rightOperand.HasValue)
{
throw new UnreachableException("Expression is not fully parsed");
}
string operatorSymbol = _operator.Value switch
{
Operator.Plus => "+",
Operator.Minus => "-",
Operator.Multiply => "*",
Operator.Divide => "/",
_ => throw new UnreachableException("Unknown operator")
};
return $"{_leftOperand.Value} {operatorSymbol} {_rightOperand.Value}";
}
private void ValidateAndParse(ReadOnlySpan<char> expression)
{
// TODO
throw new NotImplementedException();
int index = 0;
while (index < expression.Length && char.IsWhiteSpace(expression[index]))
{
index++;
}
if (index < expression.Length)
{
char firstChar = expression[index];
if (firstChar is '+' or '*' or '/')
{
throw new OperatorException("Operator is missing");
}
}
try
{
var leftOperandResult = ExpressionScanner.ScanOperand(expression);
var operatorResult = ExpressionScanner.ScanOperator(leftOperandResult.RemainingExpression);
_leftOperand = leftOperandResult.Value;
_operator = operatorResult.Value;
try
{
var rightOperandResult = ExpressionScanner.ScanOperand(operatorResult.RemainingExpression);
_rightOperand = rightOperandResult.Value;
if (_operator == Operator.Divide && _rightOperand == 0)
{
throw new NumberValueException("Division by zero is not allowed");
}
}
catch (ExpressionFormatException)
{
throw new ExpressionFormatException("Right operand is missing");
}
}
catch (ExpressionException)
{
throw;
}
catch (Exception ex)
{
throw new ExpressionFormatException("Unable to parse expression", ex);
}
}
}

View file

@ -18,8 +18,13 @@ public readonly ref struct ScanResult<T> where T : struct
/// <exception cref="ArgumentOutOfRangeException">Thrown if an invalid argument is provided</exception>
public ScanResult(T value, int length, ReadOnlySpan<char> remainingExpression)
{
if (length < 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative");
}
Value = value;
Length = -1; // TODO
Length = length;
RemainingExpression = remainingExpression;
}