/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.Model/Formulas/FormulaParser.cs
216 строк
7 KB
docNemo
Фундамент продукта: параметрическое построение выкроек, печать, установщик
08 авг 2026, 22:17
08 авг 2026, 22:17
cd47cce
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using ClothesGraph.Model.Drafting; namespace ClothesGraph.Model.Formulas; /// <summary> /// Разбор текста формулы в дерево выражения. /// </summary> /// <remarks> /// Рекурсивный спуск: приоритеты операций заданы порядком вызовов, а не /// таблицей. Для набора операций такого размера это короче и читается прямее. /// </remarks> internal sealed class FormulaParser { private readonly List<Token> _tokens; private readonly IFormulaScope _scope; private int _index; private FormulaParser(List<Token> tokens, IFormulaScope scope) { _tokens = tokens; _scope = scope; } public static Expression Parse(string text, IFormulaScope scope) { if (string.IsNullOrWhiteSpace(text)) throw new FormulaException("Формула пуста", 0); var parser = new FormulaParser(Tokenizer.Tokenize(text), scope); var expression = parser.ParseAdditive(); if (parser.Current.Kind != TokenKind.End) throw new FormulaException( $"Лишний текст «{parser.Current.Text}»", parser.Current.Position, parser.Current.Length); return expression; } private Token Current => _tokens[_index]; private Token Advance() => _tokens[_index++]; private bool Match(TokenKind kind) { if (Current.Kind != kind) return false; _index++; return true; } private Expression ParseAdditive() { var left = ParseMultiplicative(); while (true) { if (Match(TokenKind.Plus)) left = new BinaryExpression(BinaryOperator.Add, left, ParseMultiplicative()); else if (Match(TokenKind.Minus)) left = new BinaryExpression(BinaryOperator.Subtract, left, ParseMultiplicative()); else return left; } } private Expression ParseMultiplicative() { var left = ParseUnary(); while (true) { if (Match(TokenKind.Star)) left = new BinaryExpression(BinaryOperator.Multiply, left, ParseUnary()); else if (Match(TokenKind.Slash)) left = new BinaryExpression(BinaryOperator.Divide, left, ParseUnary()); else return left; } } private Expression ParseUnary() { if (Match(TokenKind.Minus)) return new NegateExpression(ParseUnary()); if (Match(TokenKind.Plus)) return ParseUnary(); return ParsePower(); } private Expression ParsePower() { var left = ParsePrimary(); // Возведение в степень правоассоциативно: 2^3^2 — это 2^(3^2). if (Match(TokenKind.Caret)) return new BinaryExpression(BinaryOperator.Power, left, ParseUnary()); return left; } private Expression ParsePrimary() { var token = Current; if (Match(TokenKind.Number)) return new NumberExpression(token.Number); if (Match(TokenKind.LeftParenthesis)) { var inner = ParseAdditive(); if (!Match(TokenKind.RightParenthesis)) throw new FormulaException("Не закрыта скобка", Current.Position, Current.Length); return inner; } if (token.Kind == TokenKind.Identifier) { Advance(); return ParseIdentifier(token); } throw new FormulaException( token.Kind == TokenKind.End ? "Формула оборвана" : $"Не ожидалось «{token.Text}»", token.Position, Math.Max(1, token.Length)); } private Expression ParseIdentifier(Token token) { var name = token.Text; if (Current.Kind == TokenKind.LeftParenthesis && FormulaFunctions.IsFunction(name)) return ParseCall(token); if (Match(TokenKind.Dot)) return ParseProperty(token); if (_scope.FindMeasurement(name) is { } measurement) return new MeasurementExpression(measurement); if (FormulaFunctions.TryGetConstant(name, out var constant)) return new NumberExpression(constant); if (FormulaFunctions.IsFunction(name)) throw new FormulaException( $"После функции «{name}» ожидается скобка", token.Position, token.Length); throw new FormulaException($"Неизвестное имя «{name}»", token.Position, token.Length); } private Expression ParseCall(Token token) { var name = token.Text; Advance(); // открывающая скобка var arguments = new List<Expression>(); if (Current.Kind != TokenKind.RightParenthesis) { do { arguments.Add(ParseAdditive()); } while (Match(TokenKind.Comma)); } if (!Match(TokenKind.RightParenthesis)) throw new FormulaException( $"Не закрыта скобка вызова «{name}»", Current.Position, Current.Length); FormulaFunctions.TryGetArity(name, out var minimum, out var maximum); if (arguments.Count < minimum || arguments.Count > maximum) throw new FormulaException( maximum == int.MaxValue ? $"Функция «{name}» принимает не меньше {minimum} аргументов" : $"Функция «{name}» принимает от {minimum} до {maximum} аргументов", token.Position, token.Length); return new CallExpression(name, arguments); } private Expression ParseProperty(Token token) { var owner = token.Text; var propertyToken = Current; if (propertyToken.Kind != TokenKind.Identifier) throw new FormulaException( "После точки ожидается имя свойства", propertyToken.Position, propertyToken.Length); Advance(); if (_scope.FindNode(owner) is not { } node) throw new FormulaException($"Неизвестное построение «{owner}»", token.Position, token.Length); var property = propertyToken.Text.ToLowerInvariant() switch { "длина" or "length" => NodeProperty.Length, "угол" or "angle" => NodeProperty.Angle, "x" or "х" => NodeProperty.X, "y" or "у" => NodeProperty.Y, _ => throw new FormulaException( $"Неизвестное свойство «{propertyToken.Text}»", propertyToken.Position, propertyToken.Length) }; return new PropertyExpression(node, property); } }