using System.Globalization; namespace MrGameEng.Formulas; internal enum TokenType { Number, Identifier, String, Plus, Minus, Star, Slash, Percent, LParen, RParen, Comma, Less, Greater, LessEqual, GreaterEqual, EqualEqual, NotEqual, And, Or, Not, Question, Colon, End, } internal readonly struct Token(TokenType type, int position, float number = 0f, string text = "") { public TokenType Type { get; } = type; public int Position { get; } = position; public float Number { get; } = number; public string Text { get; } = text; } /// /// Turns a formula string into a flat token list. Pure and allocation-light; recognizes numbers, /// identifiers, the arithmetic/comparison/logical operators and the punctuation the parser needs. /// internal static class FormulaLexer { public static List Tokenize(string source) { var tokens = new List(); var i = 0; while (i < source.Length) { var c = source[i]; if (char.IsWhiteSpace(c)) { i++; continue; } if ( char.IsDigit(c) || (c == '.' && i + 1 < source.Length && char.IsDigit(source[i + 1])) ) { var start = i; while (i < source.Length && (char.IsDigit(source[i]) || source[i] == '.')) { i++; } var span = source.AsSpan(start, i - start); if ( !float.TryParse( span, NumberStyles.Float, CultureInfo.InvariantCulture, out var value ) ) { throw new FormulaException( $"Invalid number '{span.ToString()}' at position {start}." ); } tokens.Add(new Token(TokenType.Number, start, value)); continue; } if (char.IsLetter(c) || c == '_') { var start = i; while (i < source.Length && (char.IsLetterOrDigit(source[i]) || source[i] == '_')) { i++; } tokens.Add( new Token(TokenType.Identifier, start, text: source.Substring(start, i - start)) ); continue; } if (c == '\'') { var open = i; var start = ++i; while (i < source.Length && source[i] != '\'') { i++; } if (i >= source.Length) { throw new FormulaException($"Unterminated string at position {open}."); } tokens.Add( new Token(TokenType.String, open, text: source.Substring(start, i - start)) ); i++; // пропускаем закрывающую кавычку continue; } var pos = i; switch (c) { case '+': tokens.Add(new Token(TokenType.Plus, pos)); i++; break; case '-': tokens.Add(new Token(TokenType.Minus, pos)); i++; break; case '*': tokens.Add(new Token(TokenType.Star, pos)); i++; break; case '/': tokens.Add(new Token(TokenType.Slash, pos)); i++; break; case '%': tokens.Add(new Token(TokenType.Percent, pos)); i++; break; case '(': tokens.Add(new Token(TokenType.LParen, pos)); i++; break; case ')': tokens.Add(new Token(TokenType.RParen, pos)); i++; break; case ',': tokens.Add(new Token(TokenType.Comma, pos)); i++; break; case '?': tokens.Add(new Token(TokenType.Question, pos)); i++; break; case ':': tokens.Add(new Token(TokenType.Colon, pos)); i++; break; case '<': i = AddMaybeEqual(tokens, source, i, TokenType.LessEqual, TokenType.Less); break; case '>': i = AddMaybeEqual(tokens, source, i, TokenType.GreaterEqual, TokenType.Greater); break; case '=': if (Next(source, i) == '=') { tokens.Add(new Token(TokenType.EqualEqual, pos)); i += 2; break; } throw new FormulaException($"Expected '==' at position {pos}."); case '!': if (Next(source, i) == '=') { tokens.Add(new Token(TokenType.NotEqual, pos)); i += 2; break; } tokens.Add(new Token(TokenType.Not, pos)); i++; break; case '&': if (Next(source, i) == '&') { tokens.Add(new Token(TokenType.And, pos)); i += 2; break; } throw new FormulaException($"Expected '&&' at position {pos}."); case '|': if (Next(source, i) == '|') { tokens.Add(new Token(TokenType.Or, pos)); i += 2; break; } throw new FormulaException($"Expected '||' at position {pos}."); default: throw new FormulaException($"Unexpected character '{c}' at position {pos}."); } } tokens.Add(new Token(TokenType.End, source.Length)); return tokens; } private static int AddMaybeEqual( List tokens, string source, int i, TokenType withEqual, TokenType plain ) { if (Next(source, i) == '=') { tokens.Add(new Token(withEqual, i)); return i + 2; } tokens.Add(new Token(plain, i)); return i + 1; } private static char Next(string source, int i) => i + 1 < source.Length ? source[i + 1] : '\0'; }