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:
commit
1d4380cfd0
@ -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();
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user