Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions Containers.Test/OrderedCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -593,4 +593,63 @@ public void Constructor_WithNonComparableType_WithoutComparer_ThrowsArgumentExce
Assert.ThrowsExactly<ArgumentException>(() => new OrderedCollection<object>(10));
Assert.ThrowsExactly<ArgumentException>(() => new OrderedCollection<object>([]));
}

[TestMethod]
public void IndexOf_OddLengthDuplicateRun_ReturnsFirstOccurrence()
{
// Arrange
OrderedCollection<int> collection = [1, 1, 1];

// Act & Assert
Assert.AreEqual(0, collection.IndexOf(1));
}

[TestMethod]
public void IndexOf_EvenLengthDuplicateRun_ReturnsFirstOccurrence()
{
// Arrange
OrderedCollection<int> collection = [0, 2, 2, 2, 2, 3];

// Act & Assert
Assert.AreEqual(1, collection.IndexOf(2));
}

[TestMethod]
public void Remove_KeyComparerWithDuplicateKeys_RemovesTheElementPassedIn()
{
// Arrange
IComparer<(int Key, string Name)> byKey = Comparer<(int Key, string Name)>.Create((x, y) => x.Key.CompareTo(y.Key));
OrderedCollection<(int Key, string Name)> collection = new(byKey)
{
(1, "a"), (1, "b"), (1, "c"), (1, "d"), (0, "zero"), (2, "two"),
};

// Act
bool removed = collection.Remove((1, "c"));

// Assert
Assert.IsTrue(removed);
Assert.HasCount(5, collection);
Assert.DoesNotContain((1, "c"), collection.ToList());
}

[TestMethod]
public void Remove_KeyComparerWithNoExactMatch_RemovesFirstOccurrence()
{
// Arrange
IComparer<(int Key, string Name)> byKey = Comparer<(int Key, string Name)>.Create((x, y) => x.Key.CompareTo(y.Key));
OrderedCollection<(int Key, string Name)> collection = new(byKey)
{
(0, "zero"), (1, "a"), (1, "b"), (1, "c"), (1, "d"), (2, "two"),
};
List<(int Key, string Name)> expected = [.. collection];
expected.RemoveAt(1);

// Act
bool removed = collection.Remove((1, "missing"));

// Assert
Assert.IsTrue(removed);
Assert.AreSequenceEqual(expected, collection);
}
}
72 changes: 63 additions & 9 deletions Containers/OrderedCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,17 +256,32 @@ public void CopyTo(T[] array, int arrayIndex)
/// <returns>true if the element was found and removed; otherwise, false.</returns>
/// <remarks>
/// This operation uses binary search to locate the element and has O(n) time complexity
/// due to the need to shift elements after removal.
/// due to the need to shift elements after removal. When several elements compare equal to
/// <paramref name="item"/>, the first of them that also equals it is removed, so a key-based
/// comparer does not cause a different element with the same key to be removed. If none of
/// them equals it, the first element that compares equal is removed.
/// </remarks>
public bool Remove(T item)
{
int index = BinarySearch(item);
if (index >= 0)
int first = FindFirst(item);
if (first < 0)
{
items.RemoveAt(index);
return true;
return false;
}

int index = first;
EqualityComparer<T> equality = EqualityComparer<T>.Default;
for (int i = first; i < items.Count && comparer.Compare(items[i], item) == 0; i++)
{
if (equality.Equals(items[i], item))
{
index = i;
break;
}
}
return false;

items.RemoveAt(index);
return true;
}

/// <summary>
Expand All @@ -286,9 +301,14 @@ public void RemoveAt(int index)
/// </summary>
/// <param name="item">The element to search for.</param>
/// <returns>
/// The zero-based index of the element if found; otherwise, a negative number that is the
/// bitwise complement of the index where the element should be inserted.
/// The zero-based index of an element that compares equal to <paramref name="item"/> if found;
/// otherwise, a negative number that is the bitwise complement of the index where the element
/// should be inserted.
/// </returns>
/// <remarks>
/// When the collection holds several elements that compare equal to <paramref name="item"/>,
/// the index of any one of them may be returned. Use <see cref="IndexOf(T)"/> for the first.
/// </remarks>
public int BinarySearch(T item)
{
int left = 0;
Expand Down Expand Up @@ -323,10 +343,44 @@ public int BinarySearch(T item)
/// <returns>The zero-based index of the first occurrence if found; otherwise, -1.</returns>
public int IndexOf(T item)
{
int index = BinarySearch(item);
int index = FindFirst(item);
return index >= 0 ? index : -1;
}

/// <summary>
/// Binary searches for the leftmost element that compares equal to the specified element.
/// </summary>
/// <param name="item">The element to search for.</param>
/// <returns>The index of the first matching element, or -1 if there is none.</returns>
private int FindFirst(T item)
{
int left = 0;
int right = items.Count - 1;
int found = -1;

while (left <= right)
{
int mid = left + ((right - left) / 2);
int comparison = comparer.Compare(items[mid], item);

if (comparison == 0)
{
found = mid;
right = mid - 1;
}
else if (comparison < 0)
{
left = mid + 1;
}
else
{
right = mid - 1;
}
}

return found;
}

/// <summary>
/// Returns an enumerator that iterates through the collection in sorted order.
/// </summary>
Expand Down
Loading