Skip to content

Commit

Permalink
Merge pull request #27635 from vladfrangu/feat/support-filtering-for-…
Browse files Browse the repository at this point in the history
…multiple-types

Support filtering for multiple statuses when searching beatmaps in the map picker
  • Loading branch information
bdach committed Mar 26, 2024
2 parents fcfb9d6 + 56dc6bb commit d0ee0cc
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 8 deletions.
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);
}

[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

0 comments on commit d0ee0cc

Please sign in to comment.