From 2945cb2c5862c53666e2fbd8ac4b93a3fbff31b8 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Fri, 11 Aug 2023 12:47:59 -0400 Subject: [PATCH] Create Filtering object, add helpers --- SabreTools.Filter/FilterObject.cs | 256 +++++++++++++++++++ SabreTools.Filter/FilterParser.cs | 48 ++-- SabreTools.Filter/Operation.cs | 20 ++ SabreTools.Models/Internal/DictionaryBase.cs | 40 ++- 4 files changed, 342 insertions(+), 22 deletions(-) create mode 100644 SabreTools.Filter/FilterObject.cs create mode 100644 SabreTools.Filter/Operation.cs diff --git a/SabreTools.Filter/FilterObject.cs b/SabreTools.Filter/FilterObject.cs new file mode 100644 index 00000000..1478a593 --- /dev/null +++ b/SabreTools.Filter/FilterObject.cs @@ -0,0 +1,256 @@ +using System; +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 keyValue, string? operation) + { + (string? itemName, string? fieldName) = FilterParser.ParseFilterId(keyValue); + if (itemName == null) + throw new ArgumentOutOfRangeException(nameof(keyValue)); + + this.Key = itemName; + this.Value = fieldName; + this.Operation = GetOperation(operation); + } + + public FilterObject(string keyValue, Operation operation) + { + (string? itemName, string? fieldName) = FilterParser.ParseFilterId(keyValue); + if (itemName == null) + throw new ArgumentOutOfRangeException(nameof(keyValue)); + + this.Key = itemName; + this.Value = fieldName; + this.Operation = operation; + } + + public FilterObject(string key, string? value, string? operation) + { + (string? itemName, string? fieldName) = FilterParser.ParseFilterId(key, value); + if (itemName == null) + throw new ArgumentOutOfRangeException(nameof(fieldName)); + + this.Key = itemName; + this.Value = fieldName; + this.Operation = GetOperation(operation); + } + + public FilterObject(string key, string? value, Operation operation) + { + (string? itemName, string? fieldName) = FilterParser.ParseFilterId(key, value); + if (itemName == null) + throw new ArgumentOutOfRangeException(nameof(fieldName)); + + this.Key = itemName; + this.Value = fieldName; + this.Operation = operation; + } + + /// + /// Derive an operation from the input string, if possible + /// + private static Operation GetOperation(string? operation) + { + return operation?.ToLowerInvariant() switch + { + "=" => Operation.Equals, + "==" => Operation.Equals, + + "!" => Operation.NotEquals, + "!=" => Operation.NotEquals, + + ">" => Operation.GreaterThan, + ">=" => Operation.GreaterThanOrEqual, + + "<" => Operation.LessThan, + "<=" => Operation.LessThanOrEqual, + + _ => Operation.NONE, + }; + } + + #region Matching + + /// + /// Determine if a DictionaryBase object matches the key and value + /// + public bool Matches(DictionaryBase dictionaryBase) + { + 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 + /// + /// TODO: Add regex matching to this method + private bool MatchesEqual(DictionaryBase dictionaryBase) + { + if (!dictionaryBase.ContainsKey(this.Key)) + return this.Value == null; + + string? checkValue = dictionaryBase.ReadString(this.Key); + return checkValue == this.Value; + } + + /// + /// Determines if a value does not match exactly + /// + /// TODO: Add regex matching to this method + private bool MatchesNotEqual(DictionaryBase dictionaryBase) + { + if (!dictionaryBase.ContainsKey(this.Key)) + return this.Value != null; + + string? checkValue = dictionaryBase.ReadString(this.Key); + return checkValue != this.Value; + } + + /// + /// Determines if a value is strictly greater than + /// + private bool MatchesGreaterThan(DictionaryBase dictionaryBase) + { + if (!dictionaryBase.ContainsKey(this.Key)) + return false; + + long? checkLongValue = dictionaryBase.ReadLong(this.Key); + if (checkLongValue != null) + { + if (!long.TryParse(this.Value, out long matchValue)) + return false; + + return checkLongValue > matchValue; + } + + double? checkDoubleValue = dictionaryBase.ReadDouble(this.Key); + if (checkDoubleValue != null) + { + if (!double.TryParse(this.Value, out double matchValue)) + return false; + + return checkDoubleValue > matchValue; + } + + return false; + } + + /// + /// Determines if a value is greater than or equal + /// + private bool MatchesGreaterThanOrEqual(DictionaryBase dictionaryBase) + { + if (!dictionaryBase.ContainsKey(this.Key)) + return false; + + long? checkLongValue = dictionaryBase.ReadLong(this.Key); + if (checkLongValue != null) + { + if (!long.TryParse(this.Value, out long matchValue)) + return false; + + return checkLongValue >= matchValue; + } + + double? checkDoubleValue = dictionaryBase.ReadDouble(this.Key); + if (checkDoubleValue != null) + { + if (!double.TryParse(this.Value, out double matchValue)) + return false; + + return checkDoubleValue >= matchValue; + } + + return false; + } + + /// + /// Determines if a value is strictly less than + /// + private bool MatchesLessThan(DictionaryBase dictionaryBase) + { + if (!dictionaryBase.ContainsKey(this.Key)) + return false; + + long? checkLongValue = dictionaryBase.ReadLong(this.Key); + if (checkLongValue != null) + { + if (!long.TryParse(this.Value, out long matchValue)) + return false; + + return checkLongValue < matchValue; + } + + double? checkDoubleValue = dictionaryBase.ReadDouble(this.Key); + if (checkDoubleValue != null) + { + if (!double.TryParse(this.Value, out double matchValue)) + return false; + + return checkDoubleValue < matchValue; + } + + return false; + } + + /// + /// Determines if a value is less than or equal + /// + private bool MatchesLessThanOrEqual(DictionaryBase dictionaryBase) + { + if (!dictionaryBase.ContainsKey(this.Key)) + return false; + + long? checkLongValue = dictionaryBase.ReadLong(this.Key); + if (checkLongValue != null) + { + if (!long.TryParse(this.Value, out long matchValue)) + return false; + + return checkLongValue <= matchValue; + } + + double? checkDoubleValue = dictionaryBase.ReadDouble(this.Key); + if (checkDoubleValue != null) + { + if (!double.TryParse(this.Value, out double matchValue)) + return false; + + return checkDoubleValue <= matchValue; + } + + return false; + } + + #endregion + } +} \ No newline at end of file diff --git a/SabreTools.Filter/FilterParser.cs b/SabreTools.Filter/FilterParser.cs index dfb8f493..c6a74a6f 100644 --- a/SabreTools.Filter/FilterParser.cs +++ b/SabreTools.Filter/FilterParser.cs @@ -12,38 +12,50 @@ namespace SabreTools.Filter /// /// Parse a filter ID string into the item name and field name, if possible /// - public static (string?, string?) ParseFilterId(string filterId) + public static (string?, string?) ParseFilterId(string itemFieldString) { // If we don't have a filter ID, we can't do anything - if (string.IsNullOrWhiteSpace(filterId)) + if (string.IsNullOrWhiteSpace(itemFieldString)) return (null, null); // If we only have one part, we can't do anything - string[] splitFilter = filterId.Split('.'); + string[] splitFilter = itemFieldString.Split('.'); if (splitFilter.Length != 2) return (null, null); + return ParseFilterId(splitFilter[0], splitFilter[1]); + } + + /// + /// Parse a filter ID string into the item name and field name, if possible + /// + public static (string?, string?) ParseFilterId(string itemName, string? fieldName) + { + // If we don't have a filter ID, we can't do anything + if (string.IsNullOrWhiteSpace(itemName) || string.IsNullOrWhiteSpace(fieldName)) + return (null, null); + // Return santized values based on the split ID - return splitFilter[0].ToLowerInvariant() switch + return itemName.ToLowerInvariant() switch { // Header - "header" => ParseHeaderFilterId(splitFilter), + "header" => ParseHeaderFilterId(fieldName), // Machine - "game" => ParseMachineFilterId(splitFilter), - "machine" => ParseMachineFilterId(splitFilter), - "resource" => ParseMachineFilterId(splitFilter), - "set" => ParseMachineFilterId(splitFilter), + "game" => ParseMachineFilterId(fieldName), + "machine" => ParseMachineFilterId(fieldName), + "resource" => ParseMachineFilterId(fieldName), + "set" => ParseMachineFilterId(fieldName), // DatItem - _ => ParseDatItemFilterId(splitFilter), + _ => ParseDatItemFilterId(itemName, fieldName), }; } /// /// Parse and validate header fields /// - private static (string?, string?) ParseHeaderFilterId(string[] filterId) + private static (string?, string?) ParseHeaderFilterId(string fieldName) { // Get the set of constants var constants = GetConstants(typeof(Header)); @@ -51,7 +63,7 @@ namespace SabreTools.Filter return (null, null); // Get if there's a match to the constant - string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, filterId[1], StringComparison.OrdinalIgnoreCase)); + string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, fieldName, StringComparison.OrdinalIgnoreCase)); if (constantMatch == null) return (null, null); @@ -62,7 +74,7 @@ namespace SabreTools.Filter /// /// Parse and validate machine/game fields /// - private static (string?, string?) ParseMachineFilterId(string[] filterId) + private static (string?, string?) ParseMachineFilterId(string fieldName) { // Get the set of constants var constants = GetConstants(typeof(Machine)); @@ -70,7 +82,7 @@ namespace SabreTools.Filter return (null, null); // Get if there's a match to the constant - string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, filterId[1], StringComparison.OrdinalIgnoreCase)); + string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, fieldName, StringComparison.OrdinalIgnoreCase)); if (constantMatch == null) return (null, null); @@ -81,10 +93,10 @@ namespace SabreTools.Filter /// /// Parse and validate item fields /// - private static (string?, string?) ParseDatItemFilterId(string[] filterId) + private static (string?, string?) ParseDatItemFilterId(string itemName, string fieldName) { // Get the correct item type - var itemType = GetDatItemType(filterId[0].ToLowerInvariant()); + var itemType = GetDatItemType(itemName.ToLowerInvariant()); if (itemType == null) return (null, null); @@ -94,12 +106,12 @@ namespace SabreTools.Filter return (null, null); // Get if there's a match to the constant - string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, filterId[1], StringComparison.OrdinalIgnoreCase)); + string? constantMatch = constants.FirstOrDefault(c => string.Equals(c, fieldName, StringComparison.OrdinalIgnoreCase)); if (constantMatch == null) return (null, null); // Return the sanitized ID - return (filterId[0].ToLowerInvariant(), constantMatch); + return (itemName.ToLowerInvariant(), constantMatch); } /// diff --git a/SabreTools.Filter/Operation.cs b/SabreTools.Filter/Operation.cs new file mode 100644 index 00000000..d9adaed5 --- /dev/null +++ b/SabreTools.Filter/Operation.cs @@ -0,0 +1,20 @@ +namespace SabreTools.Filter +{ + /// + /// Determines what operation is being done + /// + public enum Operation + { + /// + /// Default value, does nothing + /// + NONE, + + Equals, + NotEquals, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual, + } +} \ No newline at end of file diff --git a/SabreTools.Models/Internal/DictionaryBase.cs b/SabreTools.Models/Internal/DictionaryBase.cs index b7c445d6..44f677b6 100644 --- a/SabreTools.Models/Internal/DictionaryBase.cs +++ b/SabreTools.Models/Internal/DictionaryBase.cs @@ -12,9 +12,7 @@ namespace SabreTools.Models.Internal /// public T? Read(string key) { - if (string.IsNullOrWhiteSpace(key)) - return default; - if (!ContainsKey(key)) + if (!ValidateKey(key)) return default; return (T?)this[key]; } @@ -24,6 +22,9 @@ namespace SabreTools.Models.Internal /// public bool? ReadBool(string key) { + if (!ValidateKey(key)) + return null; + bool? asBool = Read(key); if (asBool != null) return asBool; @@ -44,6 +45,9 @@ namespace SabreTools.Models.Internal /// public double? ReadDouble(string key) { + if (!ValidateKey(key)) + return null; + double? asDouble = Read(key); if (asDouble != null) return asDouble; @@ -60,6 +64,9 @@ namespace SabreTools.Models.Internal /// public long? ReadLong(string key) { + if (!ValidateKey(key)) + return null; + long? asLong = Read(key); if (asLong != null) return asLong; @@ -76,6 +83,9 @@ namespace SabreTools.Models.Internal /// public string? ReadString(string key) { + if (!ValidateKey(key)) + return null; + string? asString = Read(key); if (asString != null) return asString; @@ -84,7 +94,7 @@ namespace SabreTools.Models.Internal if (asArray != null) return string.Join(',', asArray); - return null; + return this[key]!.ToString(); } /// @@ -92,6 +102,9 @@ namespace SabreTools.Models.Internal /// public string[]? ReadStringArray(string key) { + if (!ValidateKey(key)) + return null; + string[]? asArray = Read(key); if (asArray != null) return asArray; @@ -100,7 +113,26 @@ namespace SabreTools.Models.Internal if (asString != null) return new string[] { asString }; + asString = this[key]!.ToString(); + if (asString != null) + return new string[] { asString }; + return null; } + + /// + /// Check if a key is valid + /// + private bool ValidateKey(string key) + { + if (string.IsNullOrWhiteSpace(key)) + return false; + else if (!ContainsKey(key)) + return false; + else if (this[key] == null) + return false; + + return true; + } } } \ No newline at end of file