diff --git a/Containers.Test/OrderedSetTests.cs b/Containers.Test/OrderedSetTests.cs index dad6539..0610cec 100644 --- a/Containers.Test/OrderedSetTests.cs +++ b/Containers.Test/OrderedSetTests.cs @@ -624,6 +624,86 @@ public void LargeDataSet_MaintainsPerformanceAndOrder() Assert.HasCount(result.Length, result.Distinct(), "All elements should be unique"); } + /// + /// Builds a set whose notion of equality is case-insensitive, so that the set's own comparer + /// and disagree about which strings are the same. + /// + private static OrderedSet CaseInsensitiveSet(params string[] items) => + new(items, StringComparer.OrdinalIgnoreCase); + + [TestMethod] + public void IntersectWith_CustomComparer_KeepsElementsThatMatchUnderThatComparer() + { + OrderedSet set = CaseInsensitiveSet("Hello", "World"); + + set.IntersectWith(["HELLO"]); + + Assert.HasCount(1, set, "Only the element matching under the set's comparer should remain"); + Assert.IsTrue(set.Contains("Hello"), "\"Hello\" matches \"HELLO\" under the set's comparer and must survive"); + } + + [TestMethod] + public void IsSubsetOf_CustomComparer_MatchesUnderThatComparer() + { + OrderedSet set = CaseInsensitiveSet("Hello"); + + Assert.IsTrue(set.IsSubsetOf(["HELLO", "World"]), "\"Hello\" is contained in the collection under the set's comparer"); + } + + [TestMethod] + public void IsProperSubsetOf_CustomComparer_MatchesUnderThatComparer() + { + OrderedSet set = CaseInsensitiveSet("Hello"); + + Assert.IsTrue(set.IsProperSubsetOf(["HELLO", "World"]), "The set is a strictly smaller subset under its own comparer"); + } + + [TestMethod] + public void IsProperSubsetOf_CustomComparer_CountsDistinctElementsUnderThatComparer() + { + OrderedSet set = CaseInsensitiveSet("Hello"); + + Assert.IsFalse( + set.IsProperSubsetOf(["HELLO", "hello"]), + "The collection holds one distinct element under the set's comparer, so the set is not strictly smaller" + ); + } + + [TestMethod] + public void IsProperSupersetOf_CustomComparer_CountsDistinctElementsUnderThatComparer() + { + OrderedSet set = CaseInsensitiveSet("Hello", "World"); + + Assert.IsTrue( + set.IsProperSupersetOf(["HELLO", "hello"]), + "The collection holds one distinct element under the set's comparer, so the set is strictly larger" + ); + } + + [TestMethod] + public void SetEquals_CustomComparer_CountsDistinctElementsUnderThatComparer() + { + OrderedSet set = CaseInsensitiveSet("Hello", "World"); + + Assert.IsTrue( + set.SetEquals(["HELLO", "hello", "WORLD"]), + "The collection holds the same two distinct elements under the set's comparer" + ); + } + + [TestMethod] + public void SymmetricExceptWith_CustomComparer_TreatsMatchesUnderThatComparerAsCommon() + { + OrderedSet set = CaseInsensitiveSet("Hello", "World"); + + set.SymmetricExceptWith(["HELLO", "Foo"]); + + Assert.HasCount(2, set, "The common element should be dropped and the unmatched one added"); + Assert.IsFalse(set.Contains("Hello"), "\"Hello\" is common to both under the set's comparer and must be dropped"); + Assert.IsTrue(set.Contains("World"), "\"World\" is only in the set and must remain"); + Assert.IsTrue(set.Contains("Foo"), "\"Foo\" is only in the other collection and must be added"); + } + [TestMethod] public void StressTest_MultipleOperations() { diff --git a/Containers/OrderedSet.cs b/Containers/OrderedSet.cs index b02cd67..dd584c9 100644 --- a/Containers/OrderedSet.cs +++ b/Containers/OrderedSet.cs @@ -311,6 +311,19 @@ public int BinarySearch(T item) return ~left; // Return bitwise complement of insertion point } + /// + /// Materializes into a set that uses this set's comparer. + /// + /// The collection to materialize. + /// A set containing the distinct elements of , as this set defines distinctness. + /// + /// The set operations need both membership tests and a cardinality for . + /// Building a here would answer both with , + /// which disagrees with whenever a custom comparer was supplied, so the + /// temporary set is built with instead. + /// + private OrderedSet ToComparerSet(IEnumerable other) => new(other, Comparer); + /// /// Returns an enumerator that iterates through the set in sorted order. /// @@ -348,7 +361,7 @@ public void IntersectWith(IEnumerable other) { Ensure.NotNull(other); - HashSet otherSet = [.. other]; + OrderedSet otherSet = ToComparerSet(other); for (int i = items.Count - 1; i >= 0; i--) { @@ -383,7 +396,7 @@ public void SymmetricExceptWith(IEnumerable other) { Ensure.NotNull(other); - HashSet otherSet = [.. other]; + OrderedSet otherSet = ToComparerSet(other); // Create a list of items to remove from otherSet as we find them List toRemoveFromOther = []; @@ -421,7 +434,7 @@ public bool IsSubsetOf(IEnumerable other) { Ensure.NotNull(other); - HashSet otherSet = [.. other]; + OrderedSet otherSet = ToComparerSet(other); return items.All(otherSet.Contains); } @@ -448,8 +461,8 @@ public bool IsProperSubsetOf(IEnumerable other) { Ensure.NotNull(other); - HashSet otherSet = [.. other]; - return Count < otherSet.Count && IsSubsetOf(otherSet); + OrderedSet otherSet = ToComparerSet(other); + return Count < otherSet.Count && items.All(otherSet.Contains); } /// @@ -462,7 +475,7 @@ public bool IsProperSupersetOf(IEnumerable other) { Ensure.NotNull(other); - HashSet otherSet = [.. other]; + OrderedSet otherSet = ToComparerSet(other); return Count > otherSet.Count && IsSupersetOf(otherSet); } @@ -489,8 +502,8 @@ public bool SetEquals(IEnumerable other) { Ensure.NotNull(other); - HashSet otherSet = [.. other]; - return Count == otherSet.Count && IsSubsetOf(otherSet); + OrderedSet otherSet = ToComparerSet(other); + return Count == otherSet.Count && items.All(otherSet.Contains); } ///