Skip to content

Honor OrderedSet's own comparer in the set operations - #52

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/nice-davinci-ouwhlc
Sep 17, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/nice-davinci-ouwhlc

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #51

The problem

OrderedSet<T> decides membership two different ways. Add, Remove and Contains route through BinarySearch, which uses the IComparer<T> the set was constructed with. But six of the ISet<T> operations materialized the other collection into a HashSet<T> built with the default constructor, so both membership tests and cardinality were answered by EqualityComparer<T>.Default instead:

  • IntersectWith
  • SymmetricExceptWith
  • IsSubsetOf
  • IsProperSubsetOf
  • IsProperSupersetOf
  • SetEquals

For any set built with a comparer that defines a different equality notion than the default, the two disagree:

OrderedSet<string> set = new(StringComparer.OrdinalIgnoreCase);
set.Add("Hello");
set.Contains("HELLO");        // true — respects the custom comparer
set.IntersectWith(["HELLO"]);
set.Contains("Hello");        // false — the item was wrongly evicted

The fix

Build the temporary set with the set's own comparer, via a small private ToComparerSet helper, so membership and distinct-element counts are decided by the same rule as Contains. This mirrors the sibling ContiguousSet<T>.IntersectWith, which already threads its comparer into the temporary set.

A HashSet<T> cannot be used here: an IEqualityComparer<T> adapter over an IComparer<T> has no way to produce hash codes consistent with that comparer's equality. Building an OrderedSet<T> with the same comparer sidesteps that and keeps membership at O(log n).

IsProperSubsetOf and SetEquals now test membership directly rather than calling back into IsSubsetOf, which would have rebuilt the same temporary set a second time.

Nothing changes for a set using the default comparer, and no public API changed.

Tests

Seven tests added to OrderedSetTests.cs, all using StringComparer.OrdinalIgnoreCase so the set's comparer and default equality disagree. They cover every affected operation, including the cases where the bug showed up in the count rather than the membership test (SetEquals(["HELLO", "hello", "WORLD"]) on a two-element set).

  • Baseline on main: 357 passed.
  • With this change: 364 passed, 0 failed.
  • Reverting only OrderedSet.cs and re-running: 6 of the 7 new tests fail. The seventh (IsProperSubsetOf_CustomComparer_CountsDistinctElementsUnderThatComparer) asserts a false the old code also happened to return, and is kept as a regression guard.

dotnet build Containers.sln is clean — 0 warnings, 0 errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K19wdK61qatQNZ7MvvVgvB


Generated by Claude Code

IntersectWith, SymmetricExceptWith, IsSubsetOf, IsProperSubsetOf,
IsProperSupersetOf and SetEquals each materialized the other collection
into a HashSet<T> built with the default constructor, so membership and
cardinality were decided by EqualityComparer<T>.Default while Add,
Remove and Contains route through BinarySearch and the set's own
IComparer<T>. A set built with StringComparer.OrdinalIgnoreCase would
report Contains("HELLO") as true and then have "Hello" evicted by
IntersectWith(["HELLO"]).

Build the temporary set with the set's own comparer instead, mirroring
what the sibling ContiguousSet<T> already does. IsProperSubsetOf and
SetEquals now test membership directly rather than round-tripping
through IsSubsetOf, which would have rebuilt the same temporary set.

Fixes #51

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K19wdK61qatQNZ7MvvVgvB
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 845fa40 into main Sep 17, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/nice-davinci-ouwhlc branch September 17, 2026 00:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OrderedSet&lt;T&gt; set-operations ignore the set's own custom IComparer&lt;T&gt; and use default equality instead

2 participants