1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-27 01:02:54 +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);
}
/*
* 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]
public void TestApplyStarQueries()
{
@ -29,8 +39,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual(4.0f, filterCriteria.StarDifficulty.Max);
Assert.IsFalse(filterCriteria.StarDifficulty.IsUpperInclusive);
Assert.IsNotNull(filterCriteria.StarDifficulty.Max);
Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d);
Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d);
Assert.IsNull(filterCriteria.StarDifficulty.Min);
}
@ -42,8 +53,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("difficult", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual(9.0f, filterCriteria.ApproachRate.Min);
Assert.IsTrue(filterCriteria.ApproachRate.IsLowerInclusive);
Assert.IsNotNull(filterCriteria.ApproachRate.Min);
Assert.Greater(filterCriteria.ApproachRate.Min, 8.9f);
Assert.Less(filterCriteria.ApproachRate.Min, 9.0f);
Assert.IsNull(filterCriteria.ApproachRate.Max);
}
@ -55,10 +67,10 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim());
Assert.AreEqual(2, filterCriteria.SearchTerms.Length);
Assert.AreEqual(2.0f, filterCriteria.DrainRate.Min);
Assert.IsFalse(filterCriteria.DrainRate.IsLowerInclusive);
Assert.AreEqual(6.0f, filterCriteria.DrainRate.Max);
Assert.IsTrue(filterCriteria.DrainRate.IsUpperInclusive);
Assert.Greater(filterCriteria.DrainRate.Min, 2.0f);
Assert.Less(filterCriteria.DrainRate.Min, 2.1f);
Assert.Greater(filterCriteria.DrainRate.Max, 6.0f);
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
}
[Test]
@ -69,8 +81,9 @@ namespace osu.Game.Tests.NonVisual.Filtering
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim());
Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
Assert.AreEqual(200d, filterCriteria.BPM.Min);
Assert.IsTrue(filterCriteria.BPM.IsLowerInclusive);
Assert.IsNotNull(filterCriteria.BPM.Min);
Assert.Greater(filterCriteria.BPM.Min, 199.99d);
Assert.Less(filterCriteria.BPM.Min, 200.00d);
Assert.IsNull(filterCriteria.BPM.Max);
}
@ -93,9 +106,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual("time", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
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.IsTrue(filterCriteria.Length.IsUpperInclusive);
}
[Test]

View File

@ -25,23 +25,23 @@ namespace osu.Game.Screens.Select
switch (key)
{
case "stars" when parseFloatWithPoint(value, out var stars):
updateCriteriaRange(ref criteria.StarDifficulty, op, stars);
updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
break;
case "ar" when parseFloatWithPoint(value, out var ar):
updateCriteriaRange(ref criteria.ApproachRate, op, ar);
updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
break;
case "dr" when parseFloatWithPoint(value, out var dr):
updateCriteriaRange(ref criteria.DrainRate, op, dr);
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
break;
case "cs" when parseFloatWithPoint(value, out var cs):
updateCriteriaRange(ref criteria.CircleSize, op, cs);
updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
break;
case "bpm" when parseDoubleWithPoint(value, out var bpm):
updateCriteriaRange(ref criteria.BPM, op, bpm);
updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
break;
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)
{
updateCriteriaRange(ref range, op, value);
switch (op)
{
default:
return;
case "=":
case ":":
range.Min = value - tolerance;
range.Max = value + tolerance;
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)
{
updateCriteriaRange(ref range, op, value);
switch (op)
{
default:
return;
case "=":
case ":":
range.Min = value - tolerance;
range.Max = value + tolerance;
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;
}
}