1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-05 10:13:00 +08:00

Fix floating point handling in filter intervals

Due to floating-point rounding and representation errors, filters could
wrongly display results incongruous with the wedge display text (ie.
a beatmap with the BPM of 139.99999 would be displayed as having 140
BPM and also pass the bpm<140 filter).

Apply tolerance when parsing floating-point constraints. The tolerance
chosen is half of what the UI displays for the particular values (so
for example half of 0.1 for AR/DR/CS, 0.01 for stars, etc.)

Tests updated accordingly.
This commit is contained in:
Bartłomiej Dach 2019-09-22 00:11:13 +02:00
parent b262ba13cd
commit 70842f71f4
2 changed files with 70 additions and 21 deletions

View File

@ -21,6 +21,16 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(4, filterCriteria.SearchTerms.Length); Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
} }
/*
* The following tests have been written a bit strangely (they don't check exact
* bound equality with what the filter says).
* This is to account for floating-point arithmetic issues.
* For example, specifying a bpm<140 filter would previously match beatmaps with BPM
* of 139.99999, which would be displayed in the UI as 140.
* Due to this the tests check the last tick inside the range and the first tick
* outside of the range.
*/
[Test] [Test]
public void TestApplyStarQueries() public void TestApplyStarQueries()
{ {
@ -29,8 +39,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query); FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("easy", filterCriteria.SearchText.Trim()); Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length); Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual(4.0f, filterCriteria.StarDifficulty.Max); Assert.IsNotNull(filterCriteria.StarDifficulty.Max);
Assert.IsFalse(filterCriteria.StarDifficulty.IsUpperInclusive); Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d);
Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d);
Assert.IsNull(filterCriteria.StarDifficulty.Min); Assert.IsNull(filterCriteria.StarDifficulty.Min);
} }
@ -42,8 +53,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query); FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("difficult", filterCriteria.SearchText.Trim()); Assert.AreEqual("difficult", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length); Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual(9.0f, filterCriteria.ApproachRate.Min); Assert.IsNotNull(filterCriteria.ApproachRate.Min);
Assert.IsTrue(filterCriteria.ApproachRate.IsLowerInclusive); Assert.Greater(filterCriteria.ApproachRate.Min, 8.9f);
Assert.Less(filterCriteria.ApproachRate.Min, 9.0f);
Assert.IsNull(filterCriteria.ApproachRate.Max); Assert.IsNull(filterCriteria.ApproachRate.Max);
} }
@ -55,10 +67,10 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query); FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim()); Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim());
Assert.AreEqual(2, filterCriteria.SearchTerms.Length); Assert.AreEqual(2, filterCriteria.SearchTerms.Length);
Assert.AreEqual(2.0f, filterCriteria.DrainRate.Min); Assert.Greater(filterCriteria.DrainRate.Min, 2.0f);
Assert.IsFalse(filterCriteria.DrainRate.IsLowerInclusive); Assert.Less(filterCriteria.DrainRate.Min, 2.1f);
Assert.AreEqual(6.0f, filterCriteria.DrainRate.Max); Assert.Greater(filterCriteria.DrainRate.Max, 6.0f);
Assert.IsTrue(filterCriteria.DrainRate.IsUpperInclusive); Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
} }
[Test] [Test]
@ -69,8 +81,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query); FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim()); Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim());
Assert.AreEqual(3, filterCriteria.SearchTerms.Length); Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
Assert.AreEqual(200d, filterCriteria.BPM.Min); Assert.IsNotNull(filterCriteria.BPM.Min);
Assert.IsTrue(filterCriteria.BPM.IsLowerInclusive); Assert.Greater(filterCriteria.BPM.Min, 199.99d);
Assert.Less(filterCriteria.BPM.Min, 200.00d);
Assert.IsNull(filterCriteria.BPM.Max); Assert.IsNull(filterCriteria.BPM.Max);
} }
@ -93,9 +106,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual("time", filterCriteria.SearchText.Trim()); Assert.AreEqual("time", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length); Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual(expectedLength.TotalMilliseconds - scale.TotalMilliseconds / 2.0, filterCriteria.Length.Min); Assert.AreEqual(expectedLength.TotalMilliseconds - scale.TotalMilliseconds / 2.0, filterCriteria.Length.Min);
Assert.IsTrue(filterCriteria.Length.IsLowerInclusive);
Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max); Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max);
Assert.IsTrue(filterCriteria.Length.IsUpperInclusive);
} }
[Test] [Test]

View File

@ -25,23 +25,23 @@ namespace osu.Game.Screens.Select
switch (key) switch (key)
{ {
case "stars" when parseFloatWithPoint(value, out var stars): case "stars" when parseFloatWithPoint(value, out var stars):
updateCriteriaRange(ref criteria.StarDifficulty, op, stars); updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
break; break;
case "ar" when parseFloatWithPoint(value, out var ar): case "ar" when parseFloatWithPoint(value, out var ar):
updateCriteriaRange(ref criteria.ApproachRate, op, ar); updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
break; break;
case "dr" when parseFloatWithPoint(value, out var dr): case "dr" when parseFloatWithPoint(value, out var dr):
updateCriteriaRange(ref criteria.DrainRate, op, dr); updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
break; break;
case "cs" when parseFloatWithPoint(value, out var cs): case "cs" when parseFloatWithPoint(value, out var cs):
updateCriteriaRange(ref criteria.CircleSize, op, cs); updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
break; break;
case "bpm" when parseDoubleWithPoint(value, out var bpm): case "bpm" when parseDoubleWithPoint(value, out var bpm):
updateCriteriaRange(ref criteria.BPM, op, bpm); updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
break; break;
case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length): case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
@ -99,29 +99,67 @@ namespace osu.Game.Screens.Select
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f) private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
{ {
updateCriteriaRange(ref range, op, value);
switch (op) switch (op)
{ {
default:
return;
case "=": case "=":
case ":": case ":":
range.Min = value - tolerance; range.Min = value - tolerance;
range.Max = value + tolerance; range.Max = value + tolerance;
break; break;
case ">":
range.Min = value + tolerance;
break;
case ">=":
case ">:":
range.Min = value - tolerance;
break;
case "<":
range.Max = value - tolerance;
break;
case "<=":
case "<:":
range.Max = value + tolerance;
break;
} }
} }
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05) private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05)
{ {
updateCriteriaRange(ref range, op, value);
switch (op) switch (op)
{ {
default:
return;
case "=": case "=":
case ":": case ":":
range.Min = value - tolerance; range.Min = value - tolerance;
range.Max = value + tolerance; range.Max = value + tolerance;
break; break;
case ">":
range.Min = value + tolerance;
break;
case ">=":
case ">:":
range.Min = value - tolerance;
break;
case "<":
range.Max = value - tolerance;
break;
case "<=":
case "<:":
range.Max = value + tolerance;
break;
} }
} }