mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 15:43:21 +08:00
Add creator= and artist= filters
To match stable, add creator= and artist= filters to the beatmap carousel on song select screen. Contrary to stable, this implementation supports phrase queries with spaces within using double quotes. The quote handling is not entirely correct (can't nest), but quotes should rarely happen within names, and it is an edge case of an edge case - leaving best-effort as is. Test coverage also included.
This commit is contained in:
parent
51509f6be0
commit
b262ba13cd
@ -12,7 +12,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[TestFixture]
|
||||
public class FilterMatchingTest
|
||||
{
|
||||
private readonly BeatmapInfo exampleBeatmapInfo = new BeatmapInfo
|
||||
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 5 },
|
||||
StarDifficulty = 4.0d,
|
||||
@ -25,10 +25,10 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "The Artist",
|
||||
ArtistUnicode = "The Artist",
|
||||
ArtistUnicode = "check unicode too",
|
||||
Title = "Title goes here",
|
||||
TitleUnicode = "Title goes here",
|
||||
AuthorString = "Author",
|
||||
AuthorString = "The Author",
|
||||
Source = "unit tests",
|
||||
Tags = "look for tags too",
|
||||
},
|
||||
@ -42,6 +42,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[Test]
|
||||
public void TestCriteriaMatchingNoRuleset()
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria();
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
@ -51,6 +52,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[Test]
|
||||
public void TestCriteriaMatchingSpecificRuleset()
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 }
|
||||
@ -63,6 +65,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[Test]
|
||||
public void TestCriteriaMatchingConvertedBeatmaps()
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 },
|
||||
@ -78,6 +81,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[TestCase(false)]
|
||||
public void TestCriteriaMatchingRangeMin(bool inclusive)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 },
|
||||
@ -98,6 +102,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[TestCase(false)]
|
||||
public void TestCriteriaMatchingRangeMax(bool inclusive)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 },
|
||||
@ -122,6 +127,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[TestCase("an auteur", true)]
|
||||
public void TestCriteriaMatchingTerms(string terms, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ID = 6 },
|
||||
@ -132,5 +138,64 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("", false)]
|
||||
[TestCase("The", false)]
|
||||
[TestCase("THE", false)]
|
||||
[TestCase("author", false)]
|
||||
[TestCase("the author", false)]
|
||||
[TestCase("the author AND then something else", true)]
|
||||
[TestCase("unknown", true)]
|
||||
public void TestCriteriaMatchingCreator(string creatorName, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = creatorName }
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("", false)]
|
||||
[TestCase("The", false)]
|
||||
[TestCase("THE", false)]
|
||||
[TestCase("artist", false)]
|
||||
[TestCase("the artist", false)]
|
||||
[TestCase("the artist AND then something else", true)]
|
||||
[TestCase("unicode too", false)]
|
||||
[TestCase("unknown", true)]
|
||||
public void TestCriteriaMatchingArtist(string artistName, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("", false)]
|
||||
[TestCase("artist", false)]
|
||||
[TestCase("unknown", true)]
|
||||
public void TestCriteriaMatchingArtistWithNullUnicodeName(string artistName, bool filtered)
|
||||
{
|
||||
var exampleBeatmapInfo = getExampleBeatmap();
|
||||
exampleBeatmapInfo.Metadata.ArtistUnicode = null;
|
||||
|
||||
var criteria = new FilterCriteria
|
||||
{
|
||||
Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
|
||||
};
|
||||
var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
|
||||
carouselItem.Filter(criteria);
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,5 +125,49 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
|
||||
Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyCreatorQueries()
|
||||
{
|
||||
const string query = "beatmap specifically by creator=my_fav";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("beatmap specifically by", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyArtistQueries()
|
||||
{
|
||||
const string query = "find me songs by artist=singer please";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyArtistQueriesWithSpaces()
|
||||
{
|
||||
const string query = "really like artist=\"name with space\" yes";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyArtistQueriesOneDoubleQuote()
|
||||
{
|
||||
const string query = "weird artist=double\"quote";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("weird", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,10 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
match &= criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor);
|
||||
match &= criteria.OnlineStatus.IsInRange(Beatmap.Status);
|
||||
|
||||
match &= criteria.Creator.Matches(Beatmap.Metadata.AuthorString);
|
||||
match &= criteria.Artist.Matches(Beatmap.Metadata.Artist) ||
|
||||
criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode);
|
||||
|
||||
if (match)
|
||||
foreach (var criteriaTerm in criteria.SearchTerms)
|
||||
match &=
|
||||
|
@ -23,6 +23,8 @@ namespace osu.Game.Screens.Select
|
||||
public OptionalRange<double> BPM;
|
||||
public OptionalRange<int> BeatDivisor;
|
||||
public OptionalRange<BeatmapSetOnlineStatus> OnlineStatus;
|
||||
public OptionalTextFilter Creator;
|
||||
public OptionalTextFilter Artist;
|
||||
|
||||
public string[] SearchTerms = Array.Empty<string>();
|
||||
|
||||
@ -82,5 +84,24 @@ namespace osu.Game.Screens.Select
|
||||
&& IsLowerInclusive.Equals(other.IsLowerInclusive)
|
||||
&& IsUpperInclusive.Equals(other.IsUpperInclusive);
|
||||
}
|
||||
|
||||
public struct OptionalTextFilter : IEquatable<OptionalTextFilter>
|
||||
{
|
||||
public bool Matches(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(SearchTerm))
|
||||
return true;
|
||||
|
||||
// search term is guaranteed to be non-empty, so if the string we're comparing is empty, it's not matching
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
return value.IndexOf(SearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
public string SearchTerm;
|
||||
|
||||
public bool Equals(OptionalTextFilter other) => SearchTerm?.Equals(other.SearchTerm) ?? true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select
|
||||
internal static class FilterQueryParser
|
||||
{
|
||||
private static readonly Regex query_syntax_regex = new Regex(
|
||||
@"\b(?<key>stars|ar|dr|cs|divisor|length|objects|bpm|status)(?<op>[=:><]+)(?<value>\S*)",
|
||||
@"\b(?<key>stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static void ApplyQueries(FilterCriteria criteria, string query)
|
||||
@ -61,6 +61,14 @@ namespace osu.Game.Screens.Select
|
||||
case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue):
|
||||
updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
|
||||
break;
|
||||
|
||||
case "creator":
|
||||
updateCriteriaText(ref criteria.Creator, op, value);
|
||||
break;
|
||||
|
||||
case "artist":
|
||||
updateCriteriaText(ref criteria.Artist, op, value);
|
||||
break;
|
||||
}
|
||||
|
||||
query = query.Replace(match.ToString(), "");
|
||||
@ -78,6 +86,17 @@ namespace osu.Game.Screens.Select
|
||||
private static bool parseInt(string value, out int result) =>
|
||||
int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case "=":
|
||||
case ":":
|
||||
textFilter.SearchTerm = value.Trim('"');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
|
||||
{
|
||||
updateCriteriaRange(ref range, op, value);
|
||||
|
Loading…
Reference in New Issue
Block a user