diff --git a/MathInterpreter/Core/Exceptions.cs b/MathInterpreter/Core/Exceptions.cs index f6770ba..6ce7a78 100644 --- a/MathInterpreter/Core/Exceptions.cs +++ b/MathInterpreter/Core/Exceptions.cs @@ -2,4 +2,59 @@ namespace MathInterpreter.Core; -// TODO implement all exception classes +public abstract class ExpressionException : Exception +{ + /// + /// Initializes with the specified error and the optional + /// + /// The error message + /// The optional exception + protected ExpressionException(string message, Exception? innerException = null) + : base(message, innerException) { } +} + +public sealed class ExpressionFormatException : ExpressionException +{ + /// + /// Initializes with the specified error and the optional + /// + /// The error message + /// The innerException + public ExpressionFormatException(string message, Exception? innerException = null) + : base(message, innerException) { } +} + +public sealed class OperatorException : ExpressionException +{ + /// + /// Initializes with the specified error + /// + /// The error message + public OperatorException(string message) : base(message) { } +} + +public sealed class NumberFormatException : ExpressionException +{ + /// + /// Initializes with the and the optional + /// + /// The expression + /// The optional reason + public NumberFormatException(ReadOnlySpan expression, string? reason = null) + : base(FormatMessage(expression, reason)) { } + + private static string FormatMessage(ReadOnlySpan 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 +{ + /// + /// Initializes with the specified error + /// + /// The error message + public NumberValueException(string message) : base(message) { } +} \ No newline at end of file diff --git a/MathInterpreter/Core/ExpressionScanner.cs b/MathInterpreter/Core/ExpressionScanner.cs index 430f2dd..026e14f 100644 --- a/MathInterpreter/Core/ExpressionScanner.cs +++ b/MathInterpreter/Core/ExpressionScanner.cs @@ -15,8 +15,28 @@ public static class ExpressionScanner /// Thrown if none or an unknown operator is found public static ScanResult ScanOperator(ReadOnlySpan 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.Plus, 1, expression[(index + 1)..]), + '-' => new ScanResult(Operator.Minus, 1, expression[(index + 1)..]), + '*' => new ScanResult(Operator.Multiply, 1, expression[(index + 1)..]), + '/' => new ScanResult(Operator.Divide, 1, expression[(index + 1)..]), + _ => throw new OperatorException($"Unknown operator: {op}") + }; } /// @@ -28,8 +48,74 @@ public static class ExpressionScanner /// Thrown if a problem with the format of the number is found public static ScanResult ScanOperand(ReadOnlySpan 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 remaining = integralResult.RemainingExpression; + index = 0; + + while (index < remaining.Length && char.IsWhiteSpace(remaining[index])) + { + index++; + } + + if (index >= remaining.Length || remaining[index] != '.') + { + return new ScanResult(integralValue, integralResult.Length, remaining); + } + + ReadOnlySpan 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(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"); + } } /// @@ -41,7 +127,70 @@ public static class ExpressionScanner /// Thrown if no or an invalid number is found public static ScanResult ScanNumber(ReadOnlySpan 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(value * sign, digitCount, expression[index..]); } } diff --git a/MathInterpreter/Core/MathExpression.cs b/MathInterpreter/Core/MathExpression.cs index 388c4e5..3392223 100644 --- a/MathInterpreter/Core/MathExpression.cs +++ b/MathInterpreter/Core/MathExpression.cs @@ -8,13 +8,10 @@ namespace MathInterpreter.Core; /// public sealed class MathExpression { - // TODO - /* private double? _leftOperand; private Operator? _operator; private double? _result; private double? _rightOperand; - */ /// /// Creates a new instance of 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 /// 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 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); + } } } diff --git a/MathInterpreter/Core/ScanResult.cs b/MathInterpreter/Core/ScanResult.cs index 6d9f645..dca8db9 100644 --- a/MathInterpreter/Core/ScanResult.cs +++ b/MathInterpreter/Core/ScanResult.cs @@ -18,8 +18,13 @@ public readonly ref struct ScanResult where T : struct /// Thrown if an invalid argument is provided public ScanResult(T value, int length, ReadOnlySpan remainingExpression) { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative"); + } + Value = value; - Length = -1; // TODO + Length = length; RemainingExpression = remainingExpression; }