using System.Collections.Generic; using System.Text.RegularExpressions; namespace SabreTools.Metadata.Filter { /// /// Represents a set of filters and groups /// public class FilterGroup { /// /// How to apply the group filters /// public readonly GroupType GroupType; /// /// All standalone filters in the group /// private readonly List _subfilters = []; /// /// All filter groups contained in the group /// private readonly List _subgroups = []; public FilterGroup(GroupType groupType) { GroupType = groupType; } public FilterGroup(FilterObject[] filters, GroupType groupType) { _subfilters.AddRange(filters); GroupType = groupType; } public FilterGroup(FilterGroup[] groups, GroupType groupType) { _subgroups.AddRange(groups); GroupType = groupType; } public FilterGroup(FilterObject[] filters, FilterGroup[] groups, GroupType groupType) { _subfilters.AddRange(filters); _subgroups.AddRange(groups); GroupType = groupType; } #region Accessors /// /// Add a FilterObject to the set /// public void AddFilter(FilterObject filter) => _subfilters.Add(filter); /// /// Add a FilterGroup to the set /// public void AddGroup(FilterGroup group) => _subgroups.Add(group); #endregion #region Matching /// /// Determine if a object matches the group /// public bool Matches(object obj) { return GroupType switch { GroupType.AND => MatchesAnd(obj), GroupType.OR => MatchesOr(obj), GroupType.NONE => false, _ => false, }; } /// /// Determines if a value matches all filters /// private bool MatchesAnd(object obj) { // Run standalone filters foreach (var filter in _subfilters) { // One failed match fails the group if (!filter.Matches(obj)) return false; } // Run filter subgroups foreach (var group in _subgroups) { // One failed match fails the group if (!group.Matches(obj)) return false; } return true; } /// /// Determines if a value matches any filters /// private bool MatchesOr(object obj) { // Run standalone filters foreach (var filter in _subfilters) { // One successful match passes the group if (filter.Matches(obj)) return true; } // Run filter subgroups foreach (var group in _subgroups) { // One successful match passes the group if (group.Matches(obj)) return true; } return false; } #endregion #region Helpers #pragma warning disable IDE0051 /// /// Derive a group type from the input string, if possible /// private static GroupType GetGroupType(string? groupType) { return groupType?.ToLowerInvariant() switch { "&" => GroupType.AND, "&&" => GroupType.AND, "|" => GroupType.OR, "||" => GroupType.OR, _ => GroupType.NONE, }; } #pragma warning restore IDE0051 #pragma warning disable IDE0051 /// /// Parse an input string into a filter group /// private static void Parse(string? input) { // Tokenize the string string[] tokens = Tokenize(input); if (tokens.Length == 0) return; // Loop through the tokens and parse for (int i = 0; i < tokens.Length; i++) { // TODO: Implement parsing // - Opening parenthesis means a new group // - Closing parenthesis means finalize group and return it // - Current starting and ending with a parenthesis strips them off // - Unbalanced parenthesis can only be found on parse // - Failed parsing of FilterObjects(?) // - Invalid FilterObjects(?) } } #pragma warning restore IDE0051 /// /// Tokenize an input string for parsing /// private static string[] Tokenize(string? input) { // Null inputs are ignored if (input is null) return []; // Split the string into parseable pieces // - Left and right parenthesis are separate // - Operators & and | are separate // - Key-value pairs are enforced for statements // - Numbers can be a value without quotes // - All other values require quotes return Regex.Split(input, @"(\(|\)|[&|]{1,2}|[^\s()""]+[:!=]\d+|[^\s()""]+[:!=]{1,2}""[^""]*"")", RegexOptions.Compiled); } #endregion } }