1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 05:22:54 +08:00

move timing point binary search back inline

This commit is contained in:
OliBomby 2024-08-21 01:50:52 +02:00
parent 7a47597234
commit 8d72ec8bd6
4 changed files with 136 additions and 166 deletions

View File

@ -1,6 +1,7 @@
// 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.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
@ -286,5 +287,62 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.TimingPoints[0].BeatLength, Is.Not.EqualTo(cpiCopy.TimingPoints[0].BeatLength));
}
[Test]
public void TestBinarySearchEmptyList()
{
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.FirstFound), Is.EqualTo(-1));
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.Leftmost), Is.EqualTo(-1));
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.Rightmost), Is.EqualTo(-1));
}
[TestCase(new[] { 1 }, 0, -1)]
[TestCase(new[] { 1 }, 1, 0)]
[TestCase(new[] { 1 }, 2, -2)]
[TestCase(new[] { 1, 3 }, 0, -1)]
[TestCase(new[] { 1, 3 }, 1, 0)]
[TestCase(new[] { 1, 3 }, 2, -2)]
[TestCase(new[] { 1, 3 }, 3, 1)]
[TestCase(new[] { 1, 3 }, 4, -3)]
public void TestBinarySearchUniqueScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 0)]
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
public void TestBinarySearchFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 0)]
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
public void TestBinarySearchLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 1)]
[TestCase(new[] { 1, 2, 2 }, 2, 2)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 3)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 2)]
public void TestBinarySearchRightMostDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
}
}
}

View File

@ -1,66 +0,0 @@
// 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 NUnit.Framework;
using osu.Game.Utils;
namespace osu.Game.Tests.Utils
{
[TestFixture]
public class BinarySearchUtilsTest
{
[Test]
public void TestEmptyList()
{
Assert.That(BinarySearchUtils.BinarySearch(Array.Empty<int>(), 0, x => x), Is.EqualTo(-1));
Assert.That(BinarySearchUtils.BinarySearch(Array.Empty<int>(), 0, x => x, EqualitySelection.Leftmost), Is.EqualTo(-1));
Assert.That(BinarySearchUtils.BinarySearch(Array.Empty<int>(), 0, x => x, EqualitySelection.Rightmost), Is.EqualTo(-1));
}
[TestCase(new[] { 1 }, 0, -1)]
[TestCase(new[] { 1 }, 1, 0)]
[TestCase(new[] { 1 }, 2, -2)]
[TestCase(new[] { 1, 3 }, 0, -1)]
[TestCase(new[] { 1, 3 }, 1, 0)]
[TestCase(new[] { 1, 3 }, 2, -2)]
[TestCase(new[] { 1, 3 }, 3, 1)]
[TestCase(new[] { 1, 3 }, 4, -3)]
public void TestUniqueScenarios(int[] values, int search, int expectedIndex)
{
Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 0)]
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
public void TestFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex)
{
Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 0)]
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
public void TestLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex)
{
Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 1)]
[TestCase(new[] { 1, 2, 2 }, 2, 2)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 3)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 2)]
public void TestRightMostDuplicateScenarios(int[] values, int search, int expectedIndex)
{
Assert.That(BinarySearchUtils.BinarySearch(values, search, x => x, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
}
}
}

View File

@ -82,7 +82,7 @@ namespace osu.Game.Beatmaps.ControlPoints
[CanBeNull]
public TimingControlPoint TimingPointAfter(double time)
{
int index = BinarySearchUtils.BinarySearch(TimingPoints, time, c => c.Time, EqualitySelection.Rightmost);
int index = BinarySearch(TimingPoints, time, EqualitySelection.Rightmost);
index = index < 0 ? ~index : index + 1;
return index < TimingPoints.Count ? TimingPoints[index] : null;
}
@ -250,7 +250,7 @@ namespace osu.Game.Beatmaps.ControlPoints
{
ArgumentNullException.ThrowIfNull(list);
int index = BinarySearchUtils.BinarySearch(list, time, c => c.Time, EqualitySelection.Rightmost);
int index = BinarySearch(list, time, EqualitySelection.Rightmost);
if (index < 0)
index = ~index - 1;
@ -258,6 +258,75 @@ namespace osu.Game.Beatmaps.ControlPoints
return index >= 0 ? list[index] : null;
}
/// <summary>
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
/// </summary>
/// <param name="list">The list to search.</param>
/// <param name="time">The time to find the control point at.</param>
/// <param name="equalitySelection">Determines which index to return if there are multiple exact matches.</param>
/// <returns>The index of the control point at <paramref name="time"/>. Will return the complement of the index of the control point after <paramref name="time"/> if no exact match is found.</returns>
public static int BinarySearch<T>(IReadOnlyList<T> list, double time, EqualitySelection equalitySelection)
where T : class, IControlPoint
{
ArgumentNullException.ThrowIfNull(list);
int n = list.Count;
if (n == 0)
return -1;
if (time < list[0].Time)
return -1;
if (time > list[^1].Time)
return ~n;
int l = 0;
int r = n - 1;
bool equalityFound = false;
while (l <= r)
{
int pivot = l + ((r - l) >> 1);
if (list[pivot].Time < time)
l = pivot + 1;
else if (list[pivot].Time > time)
r = pivot - 1;
else
{
equalityFound = true;
switch (equalitySelection)
{
case EqualitySelection.Leftmost:
r = pivot - 1;
break;
case EqualitySelection.Rightmost:
l = pivot + 1;
break;
default:
case EqualitySelection.FirstFound:
return pivot;
}
}
}
if (!equalityFound) return ~l;
switch (equalitySelection)
{
case EqualitySelection.Leftmost:
return l;
default:
case EqualitySelection.Rightmost:
return l - 1;
}
}
/// <summary>
/// Check whether <paramref name="newPoint"/> should be added.
/// </summary>
@ -328,4 +397,11 @@ namespace osu.Game.Beatmaps.ControlPoints
return controlPointInfo;
}
}
public enum EqualitySelection
{
FirstFound,
Leftmost,
Rightmost
}
}

View File

@ -1,98 +0,0 @@
// 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;
namespace osu.Game.Utils
{
public class BinarySearchUtils
{
/// <summary>
/// Finds the index of the item in the sorted list which has its property equal to the search term.
/// If no exact match is found, the complement of the index of the first item greater than the search term will be returned.
/// </summary>
/// <typeparam name="T">The type of the items in the list to search.</typeparam>
/// <typeparam name="T2">The type of the property to perform the search on.</typeparam>
/// <param name="list">The list of items to search.</param>
/// <param name="searchTerm">The query to find.</param>
/// <param name="termFunc">Function that maps an item in the list to its index property.</param>
/// <param name="equalitySelection">Determines which index to return if there are multiple exact matches.</param>
/// <returns>The index of the found item. Will return the complement of the index of the first item greater than the search query if no exact match is found.</returns>
public static int BinarySearch<T, T2>(IReadOnlyList<T> list, T2 searchTerm, Func<T, T2> termFunc, EqualitySelection equalitySelection = EqualitySelection.FirstFound)
{
int n = list.Count;
if (n == 0)
return -1;
var comparer = Comparer<T2>.Default;
if (comparer.Compare(searchTerm, termFunc(list[0])) == -1)
return -1;
if (comparer.Compare(searchTerm, termFunc(list[^1])) == 1)
return ~n;
int min = 0;
int max = n - 1;
bool equalityFound = false;
while (min <= max)
{
int mid = min + (max - min) / 2;
T2 midTerm = termFunc(list[mid]);
switch (comparer.Compare(midTerm, searchTerm))
{
case 0:
equalityFound = true;
switch (equalitySelection)
{
case EqualitySelection.Leftmost:
max = mid - 1;
break;
case EqualitySelection.Rightmost:
min = mid + 1;
break;
default:
case EqualitySelection.FirstFound:
return mid;
}
break;
case 1:
max = mid - 1;
break;
case -1:
min = mid + 1;
break;
}
}
if (!equalityFound) return ~min;
switch (equalitySelection)
{
case EqualitySelection.Leftmost:
return min;
default:
case EqualitySelection.Rightmost:
return min - 1;
}
}
}
public enum EqualitySelection
{
FirstFound,
Leftmost,
Rightmost
}
}