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';
}