using System; using System.Text.RegularExpressions; using SabreTools.Core.Tools; using SabreTools.Models.Internal; namespace SabreTools.Filter { /// /// Represents a single filtering object /// public class FilterObject { /// /// Key name for the filter /// public string[] Key { get; init; } /// /// Value to match in the filter /// public string? Value { get; init; } /// /// Operation on how to match the filter /// public Operation Operation { get; init; } public FilterObject(string filterString) { (string? keyItem, Operation operation, string? value) = SplitFilterString(filterString); if (keyItem == null) throw new ArgumentOutOfRangeException(nameof(filterString)); (string? itemName, string? fieldName) = FilterParser.ParseFilterId(keyItem); if (itemName == null || fieldName == null) throw new ArgumentOutOfRangeException(nameof(filterString)); this.Key = new string[] { itemName, fieldName }; this.Value = value; this.Operation = operation; } public FilterObject(string itemField, string? value, string? operation) { (string? itemName, string? fieldName) = FilterParser.ParseFilterId(itemField); if (itemName == null || fieldName == null) throw new ArgumentOutOfRangeException(nameof(value)); this.Key = new string[] { itemName, fieldName }; this.Value = value; this.Operation = GetOperation(operation); } public FilterObject(string itemField, string? value, Operation operation) { (string? itemName, string? fieldName) = FilterParser.ParseFilterId(itemField); if (itemName == null || fieldName == null) throw new ArgumentOutOfRangeException(nameof(value)); this.Key = new string[] { itemName, fieldName }; this.Value = value; this.Operation = operation; } #region Matching /// /// Determine if a DictionaryBase object matches the key and value /// public bool Matches(DictionaryBase dictionaryBase) { // TODO: Add validation of dictionary base type from this.Key[0] return this.Operation switch { Operation.Equals => MatchesEqual(dictionaryBase), Operation.NotEquals => MatchesNotEqual(dictionaryBase), Operation.GreaterThan => MatchesGreaterThan(dictionaryBase), Operation.GreaterThanOrEqual => MatchesGreaterThanOrEqual(dictionaryBase), Operation.LessThan => MatchesLessThan(dictionaryBase), Operation.LessThanOrEqual => MatchesLessThanOrEqual(dictionaryBase), _ => false, }; } /// /// Determines if a value matches exactly /// private bool MatchesEqual(DictionaryBase dictionaryBase) { // If the key doesn't exist, we count it as null if (!dictionaryBase.ContainsKey(this.Key[1])) return this.Value == null; // If the value in the dictionary is null string? checkValue = dictionaryBase.ReadString(this.Key[1]); if (checkValue == null) return this.Value == null; // If we have both a potentially numeric check and value if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(this.Value)) { // Check Int64 values long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); long? matchValueLong = NumberHelper.ConvertToInt64(checkValue); if (checkValueLong != null && matchValueLong != null) return checkValueLong == matchValueLong; // Check Double values double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); double? matchValueDouble = NumberHelper.ConvertToDouble(checkValue); if (checkValueDouble != null && matchValueDouble != null) return checkValueDouble == matchValueDouble; } // If the value might contain valid Regex if (this.Value != null && ContainsRegex(this.Value)) return Regex.IsMatch(checkValue, this.Value); return string.Equals(checkValue, this.Value, StringComparison.OrdinalIgnoreCase); } /// /// Determines if a value does not match exactly /// private bool MatchesNotEqual(DictionaryBase dictionaryBase) { // If the key doesn't exist, we count it as null if (!dictionaryBase.ContainsKey(this.Key[1])) return this.Value != null; // If the value in the dictionary is null string? checkValue = dictionaryBase.ReadString(this.Key[1]); if (checkValue == null) return this.Value == null; // If we have both a potentially numeric check and value if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(this.Value)) { // Check Int64 values long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); long? matchValueLong = NumberHelper.ConvertToInt64(checkValue); if (checkValueLong != null && matchValueLong != null) return checkValueLong != matchValueLong; // Check Double values double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); double? matchValueDouble = NumberHelper.ConvertToDouble(checkValue); if (checkValueDouble != null && matchValueDouble != null) return checkValueDouble != matchValueDouble; } // If the value might contain valid Regex if (this.Value != null && ContainsRegex(this.Value)) return !Regex.IsMatch(checkValue, this.Value); return !string.Equals(checkValue, this.Value, StringComparison.OrdinalIgnoreCase); } /// /// Determines if a value is strictly greater than /// private bool MatchesGreaterThan(DictionaryBase dictionaryBase) { // If the key doesn't exist, we count it as null if (!dictionaryBase.ContainsKey(this.Key[1])) return false; // If the value in the dictionary is null string? checkValue = dictionaryBase.ReadString(this.Key[1]); if (checkValue == null) return false; // If we have both a potentially numeric check and value if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(this.Value)) { // Check Int64 values long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); long? matchValueLong = NumberHelper.ConvertToInt64(checkValue); if (checkValueLong != null && matchValueLong != null) return checkValueLong > matchValueLong; // Check Double values double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); double? matchValueDouble = NumberHelper.ConvertToDouble(checkValue); if (checkValueDouble != null && matchValueDouble != null) return checkValueDouble > matchValueDouble; } return false; } /// /// Determines if a value is greater than or equal /// private bool MatchesGreaterThanOrEqual(DictionaryBase dictionaryBase) { // If the key doesn't exist, we count it as null if (!dictionaryBase.ContainsKey(this.Key[1])) return false; // If the value in the dictionary is null string? checkValue = dictionaryBase.ReadString(this.Key[1]); if (checkValue == null) return false; // If we have both a potentially numeric check and value if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(this.Value)) { // Check Int64 values long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); long? matchValueLong = NumberHelper.ConvertToInt64(checkValue); if (checkValueLong != null && matchValueLong != null) return checkValueLong >= matchValueLong; // Check Double values double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); double? matchValueDouble = NumberHelper.ConvertToDouble(checkValue); if (checkValueDouble != null && matchValueDouble != null) return checkValueDouble >= matchValueDouble; } return false; } /// /// Determines if a value is strictly less than /// private bool MatchesLessThan(DictionaryBase dictionaryBase) { // If the key doesn't exist, we count it as null if (!dictionaryBase.ContainsKey(this.Key[1])) return false; // If the value in the dictionary is null string? checkValue = dictionaryBase.ReadString(this.Key[1]); if (checkValue == null) return false; // If we have both a potentially numeric check and value if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(this.Value)) { // Check Int64 values long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); long? matchValueLong = NumberHelper.ConvertToInt64(checkValue); if (checkValueLong != null && matchValueLong != null) return checkValueLong < matchValueLong; // Check Double values double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); double? matchValueDouble = NumberHelper.ConvertToDouble(checkValue); if (checkValueDouble != null && matchValueDouble != null) return checkValueDouble < matchValueDouble; } return false; } /// /// Determines if a value is less than or equal /// private bool MatchesLessThanOrEqual(DictionaryBase dictionaryBase) { // If the key doesn't exist, we count it as null if (!dictionaryBase.ContainsKey(this.Key[1])) return false; // If the value in the dictionary is null string? checkValue = dictionaryBase.ReadString(this.Key[1]); if (checkValue == null) return false; // If we have both a potentially numeric check and value if (NumberHelper.IsNumeric(checkValue) && NumberHelper.IsNumeric(this.Value)) { // Check Int64 values long? checkValueLong = NumberHelper.ConvertToInt64(checkValue); long? matchValueLong = NumberHelper.ConvertToInt64(checkValue); if (checkValueLong != null && matchValueLong != null) return checkValueLong <= matchValueLong; // Check Double values double? checkValueDouble = NumberHelper.ConvertToDouble(checkValue); double? matchValueDouble = NumberHelper.ConvertToDouble(checkValue); if (checkValueDouble != null && matchValueDouble != null) return checkValueDouble <= matchValueDouble; } return false; } #endregion #region Helpers /// /// Determine if a value may contain regex for matching /// /// /// If a value contains one of the following characters: /// ^ $ * ? + /// Then it will attempt to check if the value is regex or not. /// If none of those characters exist, then value will assumed /// not to be regex. /// private static bool ContainsRegex(string? value) { // If the value is missing, it can't be regex if (value == null) return false; // If we find a special character, try parsing as regex if (value.Contains('^') || value.Contains('$') || value.Contains('*') || value.Contains('?') || value.Contains('+')) { try { _ = new Regex(value); return true; } catch { return false; } } return false; } /// /// Derive an operation from the input string, if possible /// private static Operation GetOperation(string? operation) { return operation?.ToLowerInvariant() switch { "=" => Operation.Equals, "==" => Operation.Equals, ":" => Operation.Equals, "::" => Operation.Equals, "!" => Operation.NotEquals, "!=" => Operation.NotEquals, "!:" => Operation.NotEquals, ">" => Operation.GreaterThan, ">=" => Operation.GreaterThanOrEqual, "<" => Operation.LessThan, "<=" => Operation.LessThanOrEqual, _ => Operation.NONE, }; } /// /// Derive a key, operation, and value from the input string, if possible /// private static (string?, Operation, string?) SplitFilterString(string? filterString) { if (filterString == null) return (null, Operation.NONE, null); // Split the string using regex var match = Regex.Match(filterString, @"^(?[a-zA-Z.]+)(?[=!:><]{1,2})(?.*)$"); if (!match.Success) return (null, Operation.NONE, null); string itemField = match.Groups["itemField"].Value; Operation operation = GetOperation(match.Groups["operation"].Value); string value = match.Groups["value"].Value; return (itemField, operation, value); } #endregion } }