diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index bf888348ee..ea14412f55 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -256,8 +256,8 @@ namespace osu.Game.Tests.NonVisual.Filtering const string query = "status=r"; var filterCriteria = new FilterCriteria(); FilterQueryParser.ApplyQueries(filterCriteria, query); - Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min); - Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max); + Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); } [Test] @@ -268,10 +268,71 @@ namespace osu.Game.Tests.NonVisual.Filtering FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim()); Assert.AreEqual(4, filterCriteria.SearchTerms.Length); - Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min); - Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive); - Assert.AreEqual(BeatmapOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max); - Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive); + Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); + } + + [Test] + public void TestApplyMultipleEqualityStatusQueries() + { + const string query = "status=ranked status=loved"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.That(filterCriteria.OnlineStatus.Values, Is.Empty); + } + + [Test] + public void TestApplyEqualStatusQueryWithMultipleValues() + { + const string query = "status=ranked,loved"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.That(filterCriteria.OnlineStatus.Values, Is.Not.Empty); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved)); + } + + [Test] + public void TestApplyRangeStatusMatches() + { + const string query = "status>=r"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.That(filterCriteria.OnlineStatus.Values, Has.Count.EqualTo(4)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Ranked)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Approved)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Qualified)); + Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Loved)); + } + + [Test] + public void TestApplyRangeStatusWithMultipleMatchesQuery() + { + const string query = "status>=r,l"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.That(filterCriteria.OnlineStatus.Values, Is.EquivalentTo(Enum.GetValues())); + } + + [Test] + public void TestApplyTwoRangeStatusQuery() + { + const string query = "status>r status Length; public OptionalRange BPM; public OptionalRange BeatDivisor; - public OptionalRange OnlineStatus; + public OptionalSet OnlineStatus = new OptionalSet(); public OptionalRange LastPlayed; public OptionalTextFilter Creator; public OptionalTextFilter Artist; @@ -114,6 +114,23 @@ namespace osu.Game.Screens.Select public IRulesetFilterCriteria? RulesetCriteria { get; set; } + public readonly struct OptionalSet : IEquatable> + where T : struct, Enum + { + public bool HasFilter => true; + + public bool IsInRange(T value) => Values.Contains(value); + + public HashSet Values { get; } + + public OptionalSet() + { + Values = Enum.GetValues().ToHashSet(); + } + + public bool Equals(OptionalSet other) => Values.SetEquals(other.Values); + } + public struct OptionalRange : IEquatable> where T : struct { diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 2c4077dacf..4e49495f47 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Select return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); case "status": - return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value, tryParseEnum); + return TryUpdateCriteriaSet(ref criteria.OnlineStatus, op, value); case "creator": case "author": @@ -300,6 +300,75 @@ namespace osu.Game.Screens.Select where T : struct => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted); + /// + /// Attempts to parse a keyword filter of type , + /// from the specified and . + /// If can be parsed successfully, the function returns true + /// and the resulting range constraint is stored into the 's expected values. + /// + /// The to store the parsed data into, if successful. + /// The operator for the keyword filter. + /// The value of the keyword filter. + public static bool TryUpdateCriteriaSet(ref FilterCriteria.OptionalSet range, Operator op, string filterValue) + where T : struct, Enum + { + var matchingValues = new HashSet(); + + if (op == Operator.Equal && filterValue.Contains(',')) + { + string[] splitValues = filterValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (string splitValue in splitValues) + { + if (!tryParseEnum(splitValue, out var parsedValue)) + return false; + + matchingValues.Add(parsedValue); + } + } + else + { + if (!tryParseEnum(filterValue, out var pivotValue)) + return false; + + var allDefinedValues = Enum.GetValues(); + + foreach (var val in allDefinedValues) + { + int compareResult = Comparer.Default.Compare(val, pivotValue); + + switch (op) + { + case Operator.Less: + if (compareResult < 0) matchingValues.Add(val); + break; + + case Operator.LessOrEqual: + if (compareResult <= 0) matchingValues.Add(val); + break; + + case Operator.Equal: + if (compareResult == 0) matchingValues.Add(val); + break; + + case Operator.GreaterOrEqual: + if (compareResult >= 0) matchingValues.Add(val); + break; + + case Operator.Greater: + if (compareResult > 0) matchingValues.Add(val); + break; + + default: + return false; + } + } + } + + range.Values.IntersectWith(matchingValues); + return true; + } + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, T value) where T : struct {