#957 Реализовать построение дерева из строки #24

Merged
on.nemtina merged 5 commits from TreeBuilder into master 2025-02-06 15:20:57 +05:00
22 changed files with 761 additions and 4 deletions

View File

@ -29,7 +29,6 @@ public class SetpointRepositoryShould : IClassFixture<RepositoryTestFixture>
var value = GetJsonFromObject(22);
await sut.Add(id, value, Guid.NewGuid(), CancellationToken.None);
var t = fixture.dbContainer.GetConnectionString();
//act
var result = await sut.GetCurrent([id], CancellationToken.None);

View File

@ -3,12 +3,12 @@ using DD.Persistence.Repositories;
using DD.Persistence.Services;
using NSubstitute;
namespace DD.Persistence.Repository.Test;
namespace DD.Persistence.Test;
public class TimestampedValuesServiceShould
{
private readonly ITimestampedValuesRepository timestampedValuesRepository = Substitute.For<ITimestampedValuesRepository>();
private readonly IDataSchemeRepository dataSchemeRepository = Substitute.For<IDataSchemeRepository>();
private TimestampedValuesService timestampedValuesService;
private readonly TimestampedValuesService timestampedValuesService;
public TimestampedValuesServiceShould()
{
@ -40,7 +40,6 @@ public class TimestampedValuesServiceShould
private static IEnumerable<TimestampedValuesDto> Generate(int countToCreate, DateTimeOffset from)
{
var result = new List<TimestampedValuesDto>();
for (int i = 0; i < countToCreate; i++)
{
var values = new Dictionary<string, object>()

View File

@ -0,0 +1,107 @@
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder;
using Newtonsoft.Json;
namespace DD.Persistence.Test;
public class TreeBuilderTest
{
[Fact]
public void TreeBuildingShouldBuilt()
{
//arrange
var treeString = "(\"A\"==1)||(\"B\"==2)&&(\"C\"==3)||((\"D\"==4)||(\"E\"==5))&&(\"F\"==6)";
//act
var root = treeString.BuildTree();
//assert
Assert.NotNull(root);
var expectedRoot = JsonConvert.SerializeObject(new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.And,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1),
new TLeaf(OperationEnum.Equal, "B", 2)
),
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "C", 3),
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "D", 4),
new TLeaf(OperationEnum.Equal, "E", 5)
)
)
),
new TLeaf(OperationEnum.Equal, "F", 6)
));
var actualRoot = JsonConvert.SerializeObject(root);
Assert.Equal(expectedRoot, actualRoot);
}
[Fact]
public void TreeOperationsShouldBuilt()
{
//arrange
var treeString = "(\"A\"==1)||(\"B\"!=1)||(\"C\">1)||(\"D\">=1)||(\"E\"<1)||(\"F\"<=1)";
//act
var root = treeString.BuildTree();
//assert
Assert.NotNull(root);
var expectedRoot = JsonConvert.SerializeObject(new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1),
new TLeaf(OperationEnum.NotEqual, "B", 1)
),
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Greate, "C", 1),
new TLeaf(OperationEnum.GreateOrEqual, "D", 1)
)
),
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Less, "E", 1),
new TLeaf(OperationEnum.LessOrEqual, "F", 1)
)
));
var actualRoot = JsonConvert.SerializeObject(root);
Assert.Equal(expectedRoot, actualRoot);
}
[Fact]
public void LeafValuesShouldBuilt()
{
//arrange
var treeString = "(\"A\"==1.2345)||(\"B\"==12345)||(\"C\"==\"12345\")";
//act
var root = treeString.BuildTree();
//assert
Assert.NotNull(root);
var expectedRoot = JsonConvert.SerializeObject(new TVertex(
OperationEnum.Or,
new TVertex(
OperationEnum.Or,
new TLeaf(OperationEnum.Equal, "A", 1.2345),
new TLeaf(OperationEnum.Equal, "B", 12345)
),
new TLeaf(OperationEnum.Equal, "C", "12345")
));
var actualRoot = JsonConvert.SerializeObject(root);
Assert.Equal(expectedRoot, actualRoot);
}
}

View File

@ -0,0 +1,22 @@
namespace DD.Persistence.Filter.Models.Abstractions;
/// <summary>
/// Посетитель бинарного дерева
/// </summary>
/// <typeparam name="TVisitResult"></typeparam>
public interface INodeVisitor<TVisitResult>
{
/// <summary>
/// Посетить узел
/// </summary>
/// <param name="vertex"></param>
/// <returns></returns>
TVisitResult Visit(TVertex vertex);
/// <summary>
/// Посетить лист
/// </summary>
/// <param name="leaf"></param>
/// <returns></returns>
TVisitResult Visit(TLeaf leaf);
}

View File

@ -0,0 +1,28 @@
using DD.Persistence.Filter.Models.Enumerations;
namespace DD.Persistence.Filter.Models.Abstractions;
/// <summary>
/// Абстрактная модель вершины
/// </summary>
public abstract class TNode
{
/// <inheritdoc/>
public TNode(OperationEnum operation)
{
Operation = operation;
}
/// <summary>
/// Логическая операция
/// </summary>
public OperationEnum Operation { get; }
/// <summary>
/// Принять посетителя
/// </summary>
/// <typeparam name="TVisitResult"></typeparam>
/// <param name="visitor"></param>
/// <returns></returns>
public abstract TVisitResult AcceptVisitor<TVisitResult>(INodeVisitor<TVisitResult> visitor);
}

View File

@ -0,0 +1,47 @@
namespace DD.Persistence.Filter.Models.Enumerations;
/// <summary>
/// Логические операции
/// </summary>
public enum OperationEnum
{
/// <summary>
/// И
/// </summary>
And = 1,
/// <summary>
/// ИЛИ
/// </summary>
Or = 2,
/// <summary>
/// РАВНО
/// </summary>
Equal = 3,
/// <summary>
/// НЕ РАВНО
/// </summary>
NotEqual = 4,
/// <summary>
/// БОЛЬШЕ
/// </summary>
Greate = 5,
/// <summary>
/// БОЛЬШЕ ЛИБО РАВНО
/// </summary>
GreateOrEqual = 6,
/// <summary>
/// МЕНЬШЕ
/// </summary>
Less = 7,
/// <summary>
/// МЕНЬШЕ ЛИБО РАВНО
/// </summary>
LessOrEqual = 8
}

View File

@ -0,0 +1,33 @@
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.Models.Enumerations;
namespace DD.Persistence.Filter.Models;
/// <summary>
/// Модель листа
/// </summary>
public class TLeaf : TNode
{
/// <summary>
/// Наименование поля
/// </summary>
public string PropName { get; }
/// <summary>
/// Значение для фильтрации
/// </summary>
public object? Value { get; }
/// <inheritdoc/>
public TLeaf(OperationEnum operation, string fieldName, object? value) : base(operation)
{
PropName = fieldName;
Value = value;
}
/// <inheritdoc/>
public override TVisitResult AcceptVisitor<TVisitResult>(INodeVisitor<TVisitResult> visitor)
{
return visitor.Visit(this);
}
}

View File

@ -0,0 +1,33 @@
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.Models.Enumerations;
namespace DD.Persistence.Filter.Models;
/// <summary>
/// Модель узла
/// </summary>
public class TVertex : TNode
{
/// <summary>
/// Левый потомок
/// </summary>
public TNode Left { get; }
/// <summary>
/// Правый потомок
/// </summary>
public TNode Rigth { get; }
/// <inheritdoc/>
public TVertex(OperationEnum operation, TNode left, TNode rigth) : base(operation)
{
Left = left;
Rigth = rigth;
}
/// <inheritdoc/>
public override TVisitResult AcceptVisitor<TVisitResult>(INodeVisitor<TVisitResult> visitor)
{
return visitor.Visit(this);
}
}

View File

@ -0,0 +1,27 @@
using DD.Persistence.Filter.Models.Enumerations;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Abstractions;
/// <summary>
/// Интерфейс для выражений
/// </summary>
interface IExpression
{
/// <summary>
/// Получить логическую операцию
/// </summary>
/// <returns></returns>
OperationEnum GetOperation();
/// <summary>
/// Получить логическую операцию в виде строки (для регулярных выражений)
/// </summary>
/// <returns></returns>
string GetOperationString();
/// <summary>
/// Реализация правила
/// </summary>
/// <param name="context"></param>
void Interpret(InterpreterContext context);
}

View File

@ -0,0 +1,83 @@
using DD.Persistence.Extensions;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Abstractions;
using System.Text.RegularExpressions;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal.Abstractions;
/// <summary>
/// Абстрактный класс для нетерминальных выражений
/// </summary>
abstract class NonTerminalExpression : IExpression
{
/// <summary>
/// Реализация правила для нетерминальных выражений
/// </summary>
/// <param name="context"></param>
public void Interpret(InterpreterContext context)
{
var operation = GetOperation();
var operationString = GetOperationString();
var matches = GetMatches(context, operationString);
while (matches.Length != 0)
{
matches.ForEach(m =>
{
var matchString = m.ToString();
var separator = operationString.Replace("\\", string.Empty);
var pair = matchString
.Trim(['(', ')'])
.Split(separator)
.Select(e => int.Parse(e));
var leftNode = context.TreeNodes
.FirstOrDefault(e => e.Key == pair.First())
.Value;
var rigthNode = context.TreeNodes
.FirstOrDefault(e => e.Key == pair.Last())
.Value;
var node = new TVertex(operation, leftNode, rigthNode);
var key = context.TreeNodes.Count;
context.TreeNodes.Add(key, node);
var keyString = key.ToString();
context.TreeString = context.TreeString.Replace(matchString, keyString);
});
matches = GetMatches(context, operationString);
}
var isRoot = int.TryParse(context.TreeString, out _);
if (isRoot)
{
context.TreeString = string.Empty;
context.Root = context.TreeNodes.Last().Value;
}
}
/// <inheritdoc/>
public abstract OperationEnum GetOperation();
/// <inheritdoc/>
public abstract string GetOperationString();
/// <summary>
/// Получить из акткуального состояния строки все совпадения для текущего выражения
/// </summary>
private static Match[] GetMatches(InterpreterContext context, string operationString)
{
string pattern = context.TreeString.Contains('(') && context.TreeString.Contains(')')
? $@"\(\d+{operationString}\d+\)" : $@"\d+{operationString}\d+";
Regex regex = new(pattern);
var matches = regex
.Matches(context.TreeString)
.ToArray();
return matches;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal.Abstractions;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal;
/// <summary>
/// Выражение для "И"
/// </summary>
class AndExpression : NonTerminalExpression
{
private const string AndString = "&&";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.And;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return AndString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal.Abstractions;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal;
/// <summary>
/// Выражение для "ИЛИ"
/// </summary>
class OrExpression : NonTerminalExpression
{
private const string OrString = @"\|\|";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.Or;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return OrString;
}
}

View File

@ -0,0 +1,79 @@
using DD.Persistence.Extensions;
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Abstractions;
using System.Text.RegularExpressions;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
/// <summary>
/// Абстрактный класс для терминальных выражений
/// </summary>
abstract class TerminalExpression : IExpression
{
/// <summary>
/// Реализация правила для терминальных выражений
/// </summary>
/// <param name="context"></param>
public void Interpret(InterpreterContext context)
{
var operation = GetOperation();
var operationString = GetOperationString();
var matches = GetMatches(context, operationString);
matches.ForEach(m =>
{
var matchString = m.ToString();
var pair = matchString
.Trim(['(', ')'])
.Split(operationString);
var fieldName = pair
.First()
.Trim('\"');
var value = ParseValue(pair.Last());
var node = new TLeaf(operation, fieldName, value);
var key = context.TreeNodes.Count;
context.TreeNodes.Add(key, node);
var keyString = key.ToString();
context.TreeString = context.TreeString.Replace(matchString, keyString);
});
}
/// <inheritdoc/>
public abstract OperationEnum GetOperation();
/// <inheritdoc/>
public abstract string GetOperationString();
/// <summary>
/// Получить из акткуального состояния строки все совпадения для текущего выражения
/// </summary>
private static Match[] GetMatches(InterpreterContext context, string operationString)
{
string pattern = $@"\([^()]*{operationString}.*?\)";
Regex regex = new(pattern);
var matches = regex.Matches(context.TreeString).ToArray();
return matches;
}
private static object? ParseValue(string value)
{
value = value.Replace('.', ',');
if (value.Contains(',') && double.TryParse(value, out _))
{
return double.Parse(value);
}
if (int.TryParse(value, out _))
{
return int.Parse(value);
}
value = value.Trim('\"');
return value;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "РАВНО"
/// </summary>
class EqualExpression : TerminalExpression
{
private const string EqualString = "==";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.Equal;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "МЕНЬШЕ"
/// </summary>
class LessExpression : TerminalExpression
{
private const string EqualString = "<";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.Less;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "МЕНЬШЕ ЛИБО РАВНО"
/// </summary>
class LessOrEqualExpression : TerminalExpression
{
private const string EqualString = "<=";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.LessOrEqual;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "БОЛЬШЕ"
/// </summary>
class MoreExpression : TerminalExpression
{
private const string EqualString = ">";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.Greate;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "БОЛЬШЕ ЛИБО РАВНО"
/// </summary>
class MoreOrEqualExpression : TerminalExpression
{
private const string EqualString = ">=";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.GreateOrEqual;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return EqualString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models.Enumerations;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal.Abstract;
namespace DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
/// <summary>
/// Выражение для "НЕРАВНО"
/// </summary>
class NotEqualExpression : TerminalExpression
{
private const string NotEqulString = "!=";
/// <inheritdoc/>
public override OperationEnum GetOperation()
{
return OperationEnum.NotEqual;
}
/// <inheritdoc/>
public override string GetOperationString()
{
return NotEqulString;
}
}

View File

@ -0,0 +1,54 @@
using DD.Persistence.Filter.Models.Abstractions;
using DD.Persistence.Filter.TreeBuilder.Expressions.Abstractions;
using DD.Persistence.Filter.TreeBuilder.Expressions.NonTerminal;
using DD.Persistence.Filter.TreeBuilder.Expressions.Terminal;
namespace DD.Persistence.Filter.TreeBuilder;
/// <summary>
/// Строитель бинарных деревьев
/// </summary>
public static class FilterTreeBuilder
{
/// <summary>
/// Построить бинарное дерево логических операций сравнения из строки
/// </summary>
/// <param name="treeString"></param>
/// <returns></returns>
public static TNode? BuildTree(this string treeString)
{
InterpreterContext context = new(treeString);
// Порядок важен
List<IExpression> terminalExpressions =
[
new EqualExpression(),
new NotEqualExpression(),
new MoreOrEqualExpression(),
new LessOrEqualExpression(),
new MoreExpression(),
new LessExpression()
];
terminalExpressions.ForEach(e =>
{
e.Interpret(context);
});
// Порядок важен
List<IExpression> nonTerminalExpressions =
[
new OrExpression(),
new AndExpression()
];
while (!string.IsNullOrEmpty(context.TreeString))
{
nonTerminalExpressions.ForEach(e =>
{
e.Interpret(context);
});
}
return context.Root;
}
}

View File

@ -0,0 +1,30 @@
using DD.Persistence.Filter.Models.Abstractions;
namespace DD.Persistence.Filter.TreeBuilder;
/// <summary>
/// Контекст интерпретатора
/// </summary>
class InterpreterContext
{
/// <summary>
/// Корень дерева (результат интерпретации)
/// </summary>
public TNode? Root { get; set; }
/// <summary>
/// Дерево в виде строки (входной параметр)
/// </summary>
public string TreeString { get; set; }
/// <summary>
/// Проиндексированные вершины дерева
/// </summary>
public Dictionary<int, TNode> TreeNodes { get; set; } = [];
/// <inheritdoc/>
public InterpreterContext(string theeString)
{
TreeString = theeString;
}
}

View File

@ -0,0 +1,24 @@
using DD.Persistence.Filter.Models;
using DD.Persistence.Filter.Models.Abstractions;
namespace DD.Persistence.Filter.Visitors;
/// <inheritdoc/>
public class NodeVisitor<TVisitResult> : INodeVisitor<TVisitResult>
{
private readonly Func<TVertex, TVisitResult> _ifVertex;
private readonly Func<TLeaf, TVisitResult> _ifLeaf;
/// <inheritdoc/>
public NodeVisitor(Func<TVertex, TVisitResult> ifVertex, Func<TLeaf, TVisitResult> ifLeaf)
{
_ifVertex = ifVertex;
_ifLeaf = ifLeaf;
}
/// <inheritdoc/>
public TVisitResult Visit(TVertex vertex) => _ifVertex(vertex);
/// <inheritdoc/>
public TVisitResult Visit(TLeaf leaf) => _ifLeaf(leaf);
}