1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-13 15:27:30 +08:00

Merge pull request #24058 from peppy/full-term-exact-match

Add support for matching full terms at song select using suffixed `!`
This commit is contained in:
Dean Herbert 2023-06-28 12:14:15 +09:00 committed by GitHub
commit 1d4380cfd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 111 additions and 18 deletions

View File

@ -169,6 +169,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
[TestCase("\"tags to\"", true)]
[TestCase("\"version\"", false)]
[TestCase("\"an auteur\"", true)]
[TestCase("\"Artist\"!", true)]
[TestCase("\"The Artist\"!", false)]
[TestCase("\"the artist\"!", false)]
[TestCase("\"\\\"", true)] // nasty case, covers properly escaping user input in underlying regex.
public void TestCriteriaMatchingExactTerms(string terms, bool filtered)
{
@ -234,6 +237,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
[TestCase("the artist AND then something else", true)]
[TestCase("unicode too", false)]
[TestCase("unknown", true)]
[TestCase("\"Artist\"!", true)]
[TestCase("\"The Artist\"!", false)]
[TestCase("\"the artist\"!", false)]
public void TestCriteriaMatchingArtist(string artistName, bool filtered)
{
var exampleBeatmapInfo = getExampleBeatmap();

View File

@ -26,26 +26,58 @@ namespace osu.Game.Tests.NonVisual.Filtering
[Test]
public void TestApplyQueriesBareWordsWithExactMatch()
{
const string query = "looking for \"a beatmap\" like \"this\"";
const string query = "looking for \"a beatmap\"! like \"this\"";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("looking for \"a beatmap\" like \"this\"", filterCriteria.SearchText);
Assert.AreEqual("looking for \"a beatmap\"! like \"this\"", filterCriteria.SearchText);
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("a beatmap"));
Assert.That(filterCriteria.SearchTerms[0].Exact, Is.True);
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("this"));
Assert.That(filterCriteria.SearchTerms[1].Exact, Is.True);
Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("looking"));
Assert.That(filterCriteria.SearchTerms[2].Exact, Is.False);
Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
Assert.That(filterCriteria.SearchTerms[3].SearchTerm, Is.EqualTo("for"));
Assert.That(filterCriteria.SearchTerms[3].Exact, Is.False);
Assert.That(filterCriteria.SearchTerms[3].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
Assert.That(filterCriteria.SearchTerms[4].SearchTerm, Is.EqualTo("like"));
Assert.That(filterCriteria.SearchTerms[4].Exact, Is.False);
Assert.That(filterCriteria.SearchTerms[4].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
}
[Test]
public void TestApplyFullPhraseQueryWithExclamationPointInTerm()
{
const string query = "looking for \"circles!\"!";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("looking for \"circles!\"!", filterCriteria.SearchText);
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("circles!"));
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
Assert.That(filterCriteria.SearchTerms[1].SearchTerm, Is.EqualTo("looking"));
Assert.That(filterCriteria.SearchTerms[1].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
Assert.That(filterCriteria.SearchTerms[2].SearchTerm, Is.EqualTo("for"));
Assert.That(filterCriteria.SearchTerms[2].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
}
[Test]
public void TestApplyBrokenFullPhraseQuery()
{
const string query = "\"!";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("\"!", filterCriteria.SearchText);
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.That(filterCriteria.SearchTerms[0].SearchTerm, Is.EqualTo("!"));
Assert.That(filterCriteria.SearchTerms[0].MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
}
/*
@ -260,7 +292,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual("find me songs with please", filterCriteria.SearchText.Trim());
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
Assert.AreEqual("a certain title", filterCriteria.Title.SearchTerm);
Assert.That(filterCriteria.Title.Exact, Is.True);
Assert.That(filterCriteria.Title.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
}
[Test]
@ -272,7 +304,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
Assert.That(filterCriteria.Artist.Exact, Is.False);
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.Substring));
}
[Test]
@ -284,7 +316,19 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
Assert.That(filterCriteria.Artist.Exact, Is.True);
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.IsolatedPhrase));
}
[Test]
public void TestApplyArtistQueriesWithSpacesFullPhrase()
{
const string query = "artist=\"The Only One\"!";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.SearchText.Trim(), Is.Empty);
Assert.AreEqual(0, filterCriteria.SearchTerms.Length);
Assert.AreEqual("The Only One", filterCriteria.Artist.SearchTerm);
Assert.That(filterCriteria.Artist.MatchMode, Is.EqualTo(FilterCriteria.MatchMode.FullPhrase));
}
[Test]

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
@ -63,7 +64,7 @@ namespace osu.Game.Screens.Select
string remainingText = value;
// First handle quoted segments to ensure we keep inline spaces in exact matches.
foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\")"))
foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)"))
{
terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value });
remainingText = remainingText.Replace(quotedSegment.Value, string.Empty);
@ -139,7 +140,7 @@ namespace osu.Game.Screens.Select
{
public bool HasFilter => !string.IsNullOrEmpty(SearchTerm);
public bool Exact { get; private set; }
public MatchMode MatchMode { get; private set; }
public bool Matches(string value)
{
@ -150,10 +151,18 @@ namespace osu.Game.Screens.Select
if (string.IsNullOrEmpty(value))
return false;
if (Exact)
return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
switch (MatchMode)
{
default:
case MatchMode.Substring:
return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase);
case MatchMode.IsolatedPhrase:
return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
case MatchMode.FullPhrase:
return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.IgnoreCase) == 0;
}
}
private string searchTerm;
@ -163,12 +172,46 @@ namespace osu.Game.Screens.Select
get => searchTerm;
set
{
searchTerm = value.Trim('"');
Exact = searchTerm != value;
searchTerm = value;
if (searchTerm.StartsWith('\"'))
{
// length check ensures that the quote character in the `StartsWith()` check above and the `EndsWith()` check below is not the same character.
if (searchTerm.EndsWith("\"!", StringComparison.Ordinal) && searchTerm.Length >= 3)
{
searchTerm = searchTerm.TrimEnd('!').Trim('\"');
MatchMode = MatchMode.FullPhrase;
}
else
{
searchTerm = searchTerm.Trim('\"');
MatchMode = MatchMode.IsolatedPhrase;
}
}
else
MatchMode = MatchMode.Substring;
}
}
public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm;
}
public enum MatchMode
{
/// <summary>
/// Match using a simple "contains" substring match.
/// </summary>
Substring,
/// <summary>
/// Match for the search phrase being isolated by spaces, or at the start or end of the text.
/// </summary>
IsolatedPhrase,
/// <summary>
/// Match for the search phrase matching the full text in completion.
/// </summary>
FullPhrase,
}
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select
public static class FilterQueryParser
{
private static readonly Regex query_syntax_regex = new Regex(
@"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*"")|(\S*))",
@"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*""[!]?)|(\S*))",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static void ApplyQueries(FilterCriteria criteria, string query)