Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support filtering for multiple statuses when searching beatmaps in the map picker #27635

Merged
merged 10 commits into from
Mar 26, 2024
73 changes: 67 additions & 6 deletions osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,8 @@ public void TestPartialStatusMatch()
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]
Expand All @@ -268,10 +268,71 @@ public void TestApplyStatusQueries()
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);
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved
}

[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<BeatmapOnlineStatus>()));
}

[Test]
public void TestApplyTwoRangeStatusQuery()
{
const string query = "status>r status<l";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.OnlineStatus.Values, Has.Count.EqualTo(2));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Approved));
Assert.That(filterCriteria.OnlineStatus.Values, Contains.Item(BeatmapOnlineStatus.Qualified));
}

[Test]
public void TestApplyRangeAndEqualStatusQuery()
{
const string query = "status>r status=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.Loved));
}

[TestCase("creator")]
Expand Down
19 changes: 18 additions & 1 deletion osu.Game/Screens/Select/FilterCriteria.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class FilterCriteria
public OptionalRange<double> Length;
public OptionalRange<double> BPM;
public OptionalRange<int> BeatDivisor;
public OptionalRange<BeatmapOnlineStatus> OnlineStatus;
public OptionalSet<BeatmapOnlineStatus> OnlineStatus = new OptionalSet<BeatmapOnlineStatus>();
public OptionalRange<DateTimeOffset> LastPlayed;
public OptionalTextFilter Creator;
public OptionalTextFilter Artist;
Expand Down Expand Up @@ -114,6 +114,23 @@ public string SearchText

public IRulesetFilterCriteria? RulesetCriteria { get; set; }

public readonly struct OptionalSet<T> : IEquatable<OptionalSet<T>>
where T : struct, Enum
{
public bool HasFilter => true;

public bool IsInRange(T value) => Values.Contains(value);

public HashSet<T> Values { get; }

public OptionalSet()
{
Values = Enum.GetValues<T>().ToHashSet();
}

public bool Equals(OptionalSet<T> other) => Values.SetEquals(other.Values);
}

public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
where T : struct
{
Expand Down
71 changes: 70 additions & 1 deletion osu.Game/Screens/Select/FilterQueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key,
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":
Expand Down Expand Up @@ -300,6 +300,75 @@ public static bool TryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T>
where T : struct
=> parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted);

/// <summary>
/// Attempts to parse a keyword filter of type <typeparamref name="T"/>,
/// from the specified <paramref name="op"/> and <paramref name="filterValue"/>.
/// If <paramref name="filterValue"/> can be parsed successfully, the function returns <c>true</c>
/// and the resulting range constraint is stored into the <paramref name="range"/>'s expected values.
/// </summary>
/// <param name="range">The <see cref="FilterCriteria.OptionalSet{T}"/> to store the parsed data into, if successful.</param>
/// <param name="op">The operator for the keyword filter.</param>
/// <param name="filterValue">The value of the keyword filter.</param>
public static bool TryUpdateCriteriaSet<T>(ref FilterCriteria.OptionalSet<T> range, Operator op, string filterValue)
where T : struct, Enum
{
var matchingValues = new HashSet<T>();

if (op == Operator.Equal && filterValue.Contains(','))
{
string[] splitValues = filterValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

foreach (string splitValue in splitValues)
{
if (!tryParseEnum<T>(splitValue, out var parsedValue))
return false;

matchingValues.Add(parsedValue);
}
}
else
{
if (!tryParseEnum<T>(filterValue, out var pivotValue))
return false;

var allDefinedValues = Enum.GetValues<T>();

foreach (var val in allDefinedValues)
{
int compareResult = Comparer<T>.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<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value)
where T : struct
{
Expand Down