From 6453b2577ef4f595bafebea1ef2534daba0fcf8f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 23:25:06 +0000 Subject: [PATCH] fix: keep elements that compare equal in insertion order in OrderedCollection [patch] Add inserted at whatever index BinarySearch returned, so among elements that compare equal the order depended on where the search happened to probe. Clone, GetRange and the enumerable constructors rebuild by re-adding, so each rebuild shuffled equal elements again and a copy was not sequence-equal to its source. Add now inserts at the upper bound, after the last element that compares equal. Equal elements keep insertion order, and rebuilding a collection from its own sequence reproduces it. Fixes ktsu-dev/Containers#80 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EdrcmpMTNHSnACRgnNGs8v --- Containers.Test/OrderedCollectionTests.cs | 63 +++++++++++++++++++++++ Containers/OrderedCollection.cs | 40 ++++++++++---- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/Containers.Test/OrderedCollectionTests.cs b/Containers.Test/OrderedCollectionTests.cs index 5a0b1c5..dc084e9 100644 --- a/Containers.Test/OrderedCollectionTests.cs +++ b/Containers.Test/OrderedCollectionTests.cs @@ -678,4 +678,67 @@ public void Remove_KeyComparerWithNoExactMatch_RemovesFirstOccurrence() Assert.IsTrue(removed); Assert.AreSequenceEqual(expected, collection); } + + private static readonly IComparer<(int Key, string Name)> ByKey = + Comparer<(int Key, string Name)>.Create((x, y) => x.Key.CompareTo(y.Key)); + + [TestMethod] + public void Add_KeyComparer_KeepsEqualElementsInInsertionOrder() + { + // Act + OrderedCollection<(int Key, string Name)> collection = new(ByKey) + { + (1, "a"), (1, "b"), (1, "c"), (0, "z"), (1, "d"), (2, "y"), (1, "e"), + }; + + // Assert + Assert.AreSequenceEqual( + [(0, "z"), (1, "a"), (1, "b"), (1, "c"), (1, "d"), (1, "e"), (2, "y")], + collection); + } + + [TestMethod] + public void Clone_KeyComparer_IsSequenceEqualToSource() + { + // Arrange + OrderedCollection<(int Key, string Name)> collection = new(ByKey) + { + (1, "a"), (1, "b"), (1, "c"), (0, "z"), (1, "d"), + }; + + // Act + OrderedCollection<(int Key, string Name)> clone = collection.Clone(); + + // Assert + Assert.AreSequenceEqual(collection, clone); + } + + [TestMethod] + public void GetRange_KeyComparer_IsSequenceEqualToSourceRange() + { + // Arrange + OrderedCollection<(int Key, string Name)> collection = new(ByKey) + { + (1, "a"), (1, "b"), (1, "c"), (0, "z"), (1, "d"), + }; + + // Act + OrderedCollection<(int Key, string Name)> range = collection.GetRange(1, 3); + + // Assert + Assert.AreSequenceEqual(collection.Skip(1).Take(3), range); + } + + [TestMethod] + public void Constructor_FromEnumerableWithKeyComparer_PreservesInputOrderOfEqualElements() + { + // Arrange + (int Key, string Name)[] source = [(1, "a"), (1, "b"), (1, "c"), (1, "d"), (1, "e")]; + + // Act + OrderedCollection<(int Key, string Name)> collection = new(source, ByKey); + + // Assert + Assert.AreSequenceEqual(source, collection); + } } diff --git a/Containers/OrderedCollection.cs b/Containers/OrderedCollection.cs index f20c587..eb64144 100644 --- a/Containers/OrderedCollection.cs +++ b/Containers/OrderedCollection.cs @@ -192,17 +192,11 @@ public OrderedCollection(IEnumerable collection, IComparer comparer) /// The element to add. /// /// This operation has O(n) time complexity due to the need to maintain sorted order. - /// The element is inserted at the appropriate position to maintain the sorted sequence. + /// The element is inserted after any elements that compare equal to it, so equal elements + /// keep the order they were added in, and rebuilding a collection from its own sequence + /// (as and do) reproduces that sequence. /// - public void Add(T item) - { - int index = BinarySearch(item); - if (index < 0) - { - index = ~index; // Convert to insertion point - } - items.Insert(index, item); - } + public void Add(T item) => items.Insert(FindUpperBound(item), item); /// /// Removes all elements from the collection. @@ -372,6 +366,32 @@ private int FindFirst(T item) return found; } + /// + /// Binary searches for the position just after the last element that compares equal to the specified element. + /// + /// The element to search for. + /// The index of the first element that compares greater than , or if there is none. + private int FindUpperBound(T item) + { + int left = 0; + int right = items.Count; + + while (left < right) + { + int mid = left + ((right - left) / 2); + if (comparer.Compare(items[mid], item) <= 0) + { + left = mid + 1; + } + else + { + right = mid; + } + } + + return left; + } + /// /// Returns an enumerator that iterates through the collection in sorted order. ///