mirror of
https://github.com/ppy/osu.git
synced 2026-05-23 22:44:54 +08:00
Merge pull request #34568 from Valerus9/notequaloperator
Support not equal operator in song select search
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
@@ -373,6 +375,167 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.AreEqual(matchCustomCriteria == false, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[TestCase("title!=Title", new[] { 2, 4, 6 })]
|
||||
[TestCase("title!=\"Title1\"", new[] { 2, 3, 4, 5, 6 })]
|
||||
[TestCase("title!=\"Title1\"!", new[] { 2, 3, 4, 5, 6 })]
|
||||
public void TestNotEqualSearchForTextFilters(string query, int[] expectedBeatmapIndexes)
|
||||
{
|
||||
string[] titles =
|
||||
[
|
||||
"Title1",
|
||||
"Title1",
|
||||
"My[Favourite]Song",
|
||||
"Title",
|
||||
"Another One",
|
||||
"Diff in title",
|
||||
"a",
|
||||
];
|
||||
|
||||
var carouselBeatmaps = titles.Select(title => new CarouselBeatmap(new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Title = title,
|
||||
},
|
||||
})).ToList();
|
||||
|
||||
var criteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
carouselBeatmaps.ForEach(b => b.Filter(criteria));
|
||||
|
||||
int[] visibleBeatmaps = carouselBeatmaps
|
||||
.Where(b => !b.Filtered.Value)
|
||||
.Select(b => carouselBeatmaps.IndexOf(b)).ToArray();
|
||||
Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotEqualSearchForNumberFilters()
|
||||
{
|
||||
double[] starRatings =
|
||||
[
|
||||
2.78,
|
||||
1.78,
|
||||
1.55,
|
||||
3.78,
|
||||
1.78,
|
||||
1.55,
|
||||
2.78
|
||||
];
|
||||
|
||||
var carouselBeatmaps = starRatings.Select(starRating => new CarouselBeatmap(new BeatmapInfo
|
||||
{
|
||||
StarRating = starRating,
|
||||
})).ToList();
|
||||
|
||||
var criteria = new FilterCriteria();
|
||||
|
||||
FilterQueryParser.ApplyQueries(criteria, "star!=1.78");
|
||||
carouselBeatmaps.ForEach(b => b.Filter(criteria));
|
||||
|
||||
int[] visibleBeatmaps = carouselBeatmaps
|
||||
.Where(b => !b.Filtered.Value)
|
||||
.Select(b => carouselBeatmaps.IndexOf(b)).ToArray();
|
||||
|
||||
Assert.That(visibleBeatmaps, Is.EqualTo(new[] { 0, 2, 3, 5, 6 }));
|
||||
}
|
||||
|
||||
[TestCase("status!=ranked", new[] { 1, 2, 4, 5 })]
|
||||
[TestCase("status!=r", new[] { 1, 2, 4, 5 })]
|
||||
[TestCase("status!=loved", new[] { 0, 1, 2, 3, 4, 6 })]
|
||||
[TestCase("status!=l", new[] { 0, 1, 2, 3, 4, 6 })]
|
||||
[TestCase("status!=r,l", new[] { 1, 2, 4 })]
|
||||
public void TestNotEqualSearchForEnumFilter(string query, int[] expectedBeatmapIndexes)
|
||||
{
|
||||
var carouselBeatmaps = new[]
|
||||
{
|
||||
BeatmapOnlineStatus.Ranked,
|
||||
BeatmapOnlineStatus.Qualified,
|
||||
BeatmapOnlineStatus.Approved,
|
||||
BeatmapOnlineStatus.Ranked,
|
||||
BeatmapOnlineStatus.Approved,
|
||||
BeatmapOnlineStatus.Loved,
|
||||
BeatmapOnlineStatus.Ranked
|
||||
}.Select(info => new CarouselBeatmap(new BeatmapInfo
|
||||
{
|
||||
Status = info
|
||||
})).ToList();
|
||||
|
||||
var criteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
carouselBeatmaps.ForEach(b => b.Filter(criteria));
|
||||
|
||||
int[] visibleBeatmaps = carouselBeatmaps
|
||||
.Where(b => !b.Filtered.Value)
|
||||
.Select(b => carouselBeatmaps.IndexOf(b)).ToArray();
|
||||
|
||||
Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes));
|
||||
}
|
||||
|
||||
[TestCase("played!=1", new[] { 1, 4, 5 })]
|
||||
[TestCase("played!=0", new[] { 0, 2, 3, 6, 7 })]
|
||||
public void TestNotEqualSearchForBooleanFilter(string query, int[] expectedBeatmapIndexes)
|
||||
{
|
||||
var carouselBeatmaps = (new DateTimeOffset?[]
|
||||
{
|
||||
new DateTimeOffset(2012, 10, 21, 12, 13, 24, TimeSpan.Zero),
|
||||
null,
|
||||
new DateTimeOffset(2012, 11, 12, 23, 10, 13, TimeSpan.Zero),
|
||||
new DateTimeOffset(2013, 2, 13, 11, 43, 23, TimeSpan.Zero),
|
||||
null,
|
||||
null,
|
||||
new DateTimeOffset(2014, 1, 15, 20, 13, 24, TimeSpan.Zero),
|
||||
new DateTimeOffset(2014, 11, 16, 0, 13, 23, TimeSpan.Zero),
|
||||
}).Select(lastPlayed => new CarouselBeatmap(new BeatmapInfo
|
||||
{
|
||||
LastPlayed = lastPlayed
|
||||
})).ToList();
|
||||
|
||||
var criteria = new FilterCriteria();
|
||||
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
carouselBeatmaps.ForEach(b => b.Filter(criteria));
|
||||
|
||||
int[] visibleBeatmaps = carouselBeatmaps
|
||||
.Where(b => !b.Filtered.Value)
|
||||
.Select(b => carouselBeatmaps.IndexOf(b)).ToArray();
|
||||
|
||||
Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes));
|
||||
}
|
||||
|
||||
[TestCase("ranked!=2012", new[] { 3, 4, 5, 6, 7 })]
|
||||
[TestCase("ranked!=2012.11", new[] { 0, 1, 3, 4, 5, 6, 7 })]
|
||||
[TestCase("ranked!=2012.10.21", new[] { 1, 2, 3, 4, 5, 6, 7 })]
|
||||
public void TestNotEqualSearchForDateFilter(string query, int[] expectedBeatmapIndexes)
|
||||
{
|
||||
var carouselBeatmaps = new[]
|
||||
{
|
||||
new DateTimeOffset(2012, 10, 21, 13, 42, 13, TimeSpan.Zero),
|
||||
new DateTimeOffset(2012, 10, 11, 2, 33, 43, TimeSpan.Zero),
|
||||
new DateTimeOffset(2012, 11, 12, 10, 22, 32, TimeSpan.Zero),
|
||||
new DateTimeOffset(2013, 2, 13, 5, 19, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2013, 2, 13, 11, 23, 35, TimeSpan.Zero),
|
||||
new DateTimeOffset(2013, 3, 14, 9, 9, 1, TimeSpan.Zero),
|
||||
new DateTimeOffset(2014, 1, 15, 10, 5, 0, TimeSpan.Zero),
|
||||
new DateTimeOffset(2014, 11, 16, 23, 27, 0, TimeSpan.Zero),
|
||||
}.Select(dateRanked => new CarouselBeatmap(new BeatmapInfo
|
||||
{
|
||||
BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
DateRanked = dateRanked,
|
||||
}
|
||||
})).ToList();
|
||||
var criteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
carouselBeatmaps.ForEach(b => b.Filter(criteria));
|
||||
|
||||
int[] visibleBeatmaps = carouselBeatmaps
|
||||
.Where(b => !b.Filtered.Value)
|
||||
.Select(b => carouselBeatmaps.IndexOf(b)).ToArray();
|
||||
|
||||
Assert.That(visibleBeatmaps, Is.EqualTo(expectedBeatmapIndexes));
|
||||
}
|
||||
|
||||
private class CustomCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
private readonly bool match;
|
||||
|
||||
@@ -294,6 +294,16 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.That(filterCriteria.OnlineStatus.Values, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPartialStatusNotMatch()
|
||||
{
|
||||
const string query = "status!=r";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.IsNotEmpty(filterCriteria.OnlineStatus.Values);
|
||||
Assert.That(filterCriteria.OnlineStatus.Values, Does.Not.Contain(BeatmapOnlineStatus.Ranked));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyEqualStatusQueryWithMultipleValues()
|
||||
{
|
||||
|
||||
@@ -154,29 +154,25 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public bool IsInRange(T value)
|
||||
{
|
||||
bool lowerRangeSatisfied = true;
|
||||
bool upperRangeSatisfied = true;
|
||||
|
||||
if (Min != null)
|
||||
{
|
||||
int comparison = Comparer<T>.Default.Compare(value, Min.Value);
|
||||
|
||||
if (comparison < 0)
|
||||
return false;
|
||||
|
||||
if (comparison == 0 && !IsLowerInclusive)
|
||||
return false;
|
||||
lowerRangeSatisfied = comparison > 0 || (comparison == 0 && IsLowerInclusive);
|
||||
}
|
||||
|
||||
if (Max != null)
|
||||
{
|
||||
int comparison = Comparer<T>.Default.Compare(value, Max.Value);
|
||||
|
||||
if (comparison > 0)
|
||||
return false;
|
||||
|
||||
if (comparison == 0 && !IsUpperInclusive)
|
||||
return false;
|
||||
upperRangeSatisfied = comparison < 0 || (comparison == 0 && IsUpperInclusive);
|
||||
}
|
||||
|
||||
return true;
|
||||
bool result = lowerRangeSatisfied && upperRangeSatisfied;
|
||||
if (InvertRange)
|
||||
result = !result;
|
||||
return result;
|
||||
}
|
||||
|
||||
public T? Min;
|
||||
@@ -184,6 +180,11 @@ namespace osu.Game.Screens.Select
|
||||
public bool IsLowerInclusive;
|
||||
public bool IsUpperInclusive;
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/>, the meaning of this filter is inverted, i.e. it will <i>exclude</i> items that satisfy this range.
|
||||
/// </summary>
|
||||
public bool InvertRange;
|
||||
|
||||
public bool Equals(OptionalRange<T> other)
|
||||
=> EqualityComparer<T?>.Default.Equals(Min, other.Min)
|
||||
&& EqualityComparer<T?>.Default.Equals(Max, other.Max)
|
||||
@@ -206,22 +207,37 @@ namespace osu.Game.Screens.Select
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
bool result;
|
||||
|
||||
switch (MatchMode)
|
||||
{
|
||||
default:
|
||||
case MatchMode.Substring:
|
||||
// Note that we are using ordinal here to avoid performance issues caused by globalisation concerns.
|
||||
// See https://github.com/ppy/osu/issues/11571 / https://github.com/dotnet/docs/issues/18423.
|
||||
return value.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase);
|
||||
result = value.Contains(SearchTerm, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
|
||||
case MatchMode.IsolatedPhrase:
|
||||
return Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
result = Regex.IsMatch(value, $@"(^|\s){Regex.Escape(searchTerm)}($|\s)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
break;
|
||||
|
||||
case MatchMode.FullPhrase:
|
||||
return CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.OrdinalIgnoreCase) == 0;
|
||||
result = CultureInfo.InvariantCulture.CompareInfo.Compare(value, searchTerm, CompareOptions.OrdinalIgnoreCase) == 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ExcludeTerm)
|
||||
result = !result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/>, the meaning of this filter is inverted, i.e. it will <i>exclude</i> items which match <see cref="SearchTerm"/>.
|
||||
/// </summary>
|
||||
public bool ExcludeTerm;
|
||||
|
||||
private string searchTerm;
|
||||
|
||||
public string SearchTerm
|
||||
|
||||
@@ -76,6 +76,8 @@ namespace osu.Game.Screens.Select
|
||||
return false;
|
||||
|
||||
// Unplayed beatmaps are filtered on DateTimeOffset.MinValue.
|
||||
if (op == Operator.NotEqual)
|
||||
played = !played;
|
||||
|
||||
if (played)
|
||||
{
|
||||
@@ -232,6 +234,10 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case Operator.NotEqual:
|
||||
textFilter.ExcludeTerm = true;
|
||||
goto case Operator.Equal;
|
||||
|
||||
case Operator.Equal:
|
||||
textFilter.SearchTerm = value;
|
||||
return true;
|
||||
@@ -259,14 +265,22 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f)
|
||||
{
|
||||
range.InvertRange = false;
|
||||
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return false;
|
||||
|
||||
case Operator.NotEqual:
|
||||
range.InvertRange = true;
|
||||
goto case Operator.Equal;
|
||||
|
||||
case Operator.Equal:
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
if (tolerance == 0)
|
||||
range.IsLowerInclusive = range.IsUpperInclusive = true;
|
||||
break;
|
||||
|
||||
case Operator.Greater:
|
||||
@@ -307,11 +321,17 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05)
|
||||
{
|
||||
range.InvertRange = false;
|
||||
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return false;
|
||||
|
||||
case Operator.NotEqual:
|
||||
range.InvertRange = true;
|
||||
goto case Operator.Equal;
|
||||
|
||||
case Operator.Equal:
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
@@ -380,17 +400,30 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
var matchingValues = new HashSet<T>();
|
||||
|
||||
if (op == Operator.Equal && filterValue.Contains(','))
|
||||
if (filterValue.Contains(','))
|
||||
{
|
||||
string[] splitValues = filterValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
HashSet<T> parsedValues = new HashSet<T>();
|
||||
|
||||
foreach (string splitValue in splitValues)
|
||||
{
|
||||
if (!tryParseEnum<T>(splitValue, out var parsedValue))
|
||||
return false;
|
||||
|
||||
matchingValues.Add(parsedValue);
|
||||
parsedValues.Add(parsedValue);
|
||||
}
|
||||
|
||||
if (op == Operator.Equal)
|
||||
{
|
||||
matchingValues.UnionWith(parsedValues);
|
||||
}
|
||||
else if (op == Operator.NotEqual)
|
||||
{
|
||||
matchingValues.UnionWith(Enum.GetValues<T>());
|
||||
matchingValues.ExceptWith(parsedValues);
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -425,6 +458,10 @@ namespace osu.Game.Screens.Select
|
||||
if (compareResult > 0) matchingValues.Add(val);
|
||||
break;
|
||||
|
||||
case Operator.NotEqual:
|
||||
if (compareResult != 0) matchingValues.Add(val);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -443,6 +480,10 @@ namespace osu.Game.Screens.Select
|
||||
default:
|
||||
return false;
|
||||
|
||||
case Operator.NotEqual:
|
||||
range.InvertRange = true;
|
||||
goto case Operator.Equal;
|
||||
|
||||
case Operator.Equal:
|
||||
range.IsLowerInclusive = range.IsUpperInclusive = true;
|
||||
range.Min = value;
|
||||
@@ -523,13 +564,15 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case Operator.NotEqual:
|
||||
case Operator.Equal:
|
||||
// an equality filter is difficult to define for support here.
|
||||
// an equality or inequality filter is difficult to define for support here.
|
||||
// if "3 months 2 days ago" means a single concrete time instant, such a filter is basically useless.
|
||||
// if it means a range of 24 hours, then that is annoying to write and also comes with its own implications
|
||||
// (does it mean "time instant 3 months 2 days ago, within 12 hours of tolerance either direction"?
|
||||
// does it mean "the full calendar day, from midnight to midnight, 3 months 2 days ago"?)
|
||||
// as such, for simplicity, just refuse to support this.
|
||||
// same applies to inequality, but instead 24 hours would be need to be left out
|
||||
return false;
|
||||
|
||||
// for the remaining operators, since the value provided to this function is an "ago" type value
|
||||
@@ -682,6 +725,7 @@ namespace osu.Game.Screens.Select
|
||||
try
|
||||
{
|
||||
DateTimeOffset dateTimeOffset;
|
||||
dateRange.InvertRange = false;
|
||||
|
||||
switch (op)
|
||||
{
|
||||
@@ -737,6 +781,11 @@ namespace osu.Game.Screens.Select
|
||||
dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1);
|
||||
return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset);
|
||||
|
||||
case Operator.NotEqual:
|
||||
|
||||
dateRange.InvertRange = true;
|
||||
goto case Operator.Equal;
|
||||
|
||||
case Operator.Equal:
|
||||
|
||||
DateTimeOffset minDateTimeOffset;
|
||||
|
||||
Reference in New Issue
Block a user