using Deal.Modules.Kanban.Application.Models; namespace Deal.Modules.Kanban.Application.ColumnRules; /// /// Матчинг текста сообщения по правилам колонки /// public static class ColumnMatcher { /// /// Соответствует ли текст правилам колонки. /// /// Правила колонки (null/пустые → False: пустая колонка не матчит). /// Текст сообщения/карточки. /// Курсы для конвертации бюджета (см. ); null — если бюджет в другой валюте, суммы не конвертируются и группа бюджета не совпадает. /// True — текст прошёл правила в режиме all/any. public static bool MatchText( ContainerRulesDto? rules, string? text, IReadOnlyDictionary? rates = null) { if (rules is null) { return false; } string lower = ContentNormalizer.ContentText(text ?? string.Empty).ToLowerInvariant(); IReadOnlyList keywords = NormalizeTerms(rules.Keywords); IReadOnlyList stack = NormalizeTerms(rules.Stack); IReadOnlyList direction = NormalizeTerms(rules.Direction); IReadOnlyList grade = NormalizeTerms(rules.Grade); IReadOnlyList gradeTerms = GradeAliases.ExpandTerms(grade); IReadOnlyList levels = NormalizeTerms(rules.Levels); IReadOnlyList levelTerms = GradeAliases.ExpandTerms(levels); IReadOnlyList locations = NormalizeTerms(rules.Locations); IReadOnlyList types = NormalizeTerms(rules.Types); IReadOnlyList typeTerms = TypeAliases.ExpandTerms(types); bool budgetEnabled = rules.Budget.HasBudget(); bool pricesEnabled = rules.Prices.HasBudget(); bool anyEnabled = keywords.Count > 0 || stack.Count > 0 || direction.Count > 0 || grade.Count > 0 || levels.Count > 0 || locations.Count > 0 || types.Count > 0 || budgetEnabled || pricesEnabled; if (!anyEnabled) { return false; } // Значения групп: пустая (выключенная) группа считается True, но в свёртку не входит. bool keywordsOk = keywords.Count == 0 || ContainsAny(lower, keywords); bool stackOk = stack.Count == 0 || ContainsAny(lower, stack); bool directionOk = direction.Count == 0 || ContainsAny(lower, direction); bool gradeOk = gradeTerms.Count == 0 || ContainsAny(lower, gradeTerms); bool levelsOk = levelTerms.Count == 0 || ContainsAny(lower, levelTerms); bool locationsOk = locations.Count == 0 || ContainsAny(lower, locations); bool typesOk = typeTerms.Count == 0 || ContainsAny(lower, typeTerms); bool budgetOk = !budgetEnabled || BudgetInRange.IsInRange(AmountParser.Parse(text), rules.Budget!, rates); bool pricesOk = !pricesEnabled || BudgetInRange.IsInRange(AmountParser.Parse(text), rules.Prices!, rates); if (string.Equals(rules.Mode, ColumnRuleModes.Any, StringComparison.OrdinalIgnoreCase)) { // «любое»: совпасть должна хотя бы одна ВКЛЮЧЁННАЯ (непустая) группа. return (keywords.Count > 0 && keywordsOk) || (stack.Count > 0 && stackOk) || (direction.Count > 0 && directionOk) || (grade.Count > 0 && gradeOk) || (levels.Count > 0 && levelsOk) || (locations.Count > 0 && locationsOk) || (types.Count > 0 && typesOk) || (budgetEnabled && budgetOk) || (pricesEnabled && pricesOk); } // all: каждая включённая группа обязана совпасть. return (keywords.Count == 0 || keywordsOk) && (stack.Count == 0 || stackOk) && (direction.Count == 0 || directionOk) && (grade.Count == 0 || gradeOk) && (levels.Count == 0 || levelsOk) && (locations.Count == 0 || locationsOk) && (types.Count == 0 || typesOk) && (!budgetEnabled || budgetOk) && (!pricesEnabled || pricesOk); } /// /// Число совпавших ТЕРМОВ правил. /// /// Правила колонки. /// Текст сообщения. /// Суммарный балл (0 — текст пуст или совпадений нет). public static int ScoreText(ContainerRulesDto? rules, string? text) { if (rules is null || string.IsNullOrEmpty(text)) { return 0; } string lower = ContentNormalizer.ContentText(text).ToLowerInvariant(); int score = 0; score += CountMatchedTerms(lower, rules.Direction); score += CountMatchedTerms(lower, rules.Keywords); score += CountMatchedTerms(lower, rules.Stack); score += CountMatchedTerms(lower, rules.Locations); foreach (string alias in GradeAliases.ExpandTerms(NormalizeTerms(rules.Grade))) { if (lower.Contains(alias, StringComparison.Ordinal)) { score += 1; } } foreach (string alias in GradeAliases.ExpandTerms(NormalizeTerms(rules.Levels))) { if (lower.Contains(alias, StringComparison.Ordinal)) { score += 1; } } foreach (string alias in TypeAliases.ExpandTerms(NormalizeTerms(rules.Types))) { if (lower.Contains(alias, StringComparison.Ordinal)) { score += 1; } } return score; } /// /// Есть ли в правилах хотя бы одна реально работающая группа фильтров. /// /// Правила колонки (null → False). /// True — есть непустой терм группы или бюджет с границей. public static bool HasActiveRules(ContainerRulesDto? rules) { if (rules is null) { return false; } return rules.Direction.HasAnyTerm() || rules.Keywords.HasAnyTerm() || rules.Stack.HasAnyTerm() || rules.Grade.HasAnyTerm() || rules.Levels.HasAnyTerm() || rules.Locations.HasAnyTerm() || rules.Types.HasAnyTerm() || (rules.Budget is not null && (rules.Budget.From is not null || rules.Budget.To is not null)) || (rules.Prices is not null && (rules.Prices.From is not null || rules.Prices.To is not null)); } // Совпал ли хотя бы один терм (подстрока в тексте). // lower: Текст в нижнем регистре (очищен от ссылок). // terms: Термы в нижнем регистре. // Возвращает: True — есть совпадение подстрокой. private static bool ContainsAny(string lower, IReadOnlyList terms) { foreach (string term in terms) { if (lower.Contains(term, StringComparison.Ordinal)) { return true; } } return false; } // Сколько термов списка совпало в тексте (для score). // lower: Текст в нижнем регистре. // rawTerms: Термы как сохранены (регистр не важен). // Возвращает: Число совпавших термов. private static int CountMatchedTerms(string lower, IReadOnlyList? rawTerms) { if (rawTerms is null) { return 0; } int count = 0; foreach (string? raw in rawTerms) { string term = (raw ?? string.Empty).Trim().ToLowerInvariant(); if (term.Length > 0 && lower.Contains(term, StringComparison.Ordinal)) { count += 1; } } return count; } private static IReadOnlyList NormalizeTerms(IReadOnlyList? terms) { var result = new List(); if (terms is null) { return result; } foreach (string? raw in terms) { string term = (raw ?? string.Empty).Trim().ToLowerInvariant(); if (term.Length > 0) { result.Add(term); } } return result; } }