Skip to content

Commit 0a0a7ee

Browse files
Merge pull request #41 from marius-bughiu/feature/add-sets
Add CeleritySet and IntSet collections
2 parents 4d0802c + ad8a7b8 commit 0a0a7ee

7 files changed

Lines changed: 1253 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using Celerity.Collections;
2+
using Celerity.Hashing;
3+
4+
namespace Celerity.Tests.Collections;
5+
6+
/// <summary>
7+
/// Tests that exercise <see cref="CeleritySet{T, THasher}"/>
8+
/// under maximum hash collision pressure and with reference-type elements
9+
/// whose default is <c>null</c>.
10+
/// </summary>
11+
public class CeleritySetCollisionTests
12+
{
13+
private struct ConstantIntHasher : IHashProvider<int>
14+
{
15+
public int Hash(int key) => 42;
16+
}
17+
18+
private struct ConstantStringHasher : IHashProvider<string>
19+
{
20+
public int Hash(string key) => 7;
21+
}
22+
23+
// ---------------------------------------------------------------
24+
// Int-element collision tests
25+
// ---------------------------------------------------------------
26+
27+
[Fact]
28+
public void Insert_ShouldSucceed_UnderFullCollision()
29+
{
30+
var set = new CeleritySet<int, ConstantIntHasher>(16);
31+
for (int i = 1; i <= 10; i++)
32+
set.Add(i);
33+
34+
Assert.Equal(10, set.Count);
35+
for (int i = 1; i <= 10; i++)
36+
Assert.True(set.Contains(i));
37+
}
38+
39+
[Fact]
40+
public void Duplicate_ShouldBeRejected_UnderFullCollision()
41+
{
42+
var set = new CeleritySet<int, ConstantIntHasher>(16);
43+
for (int i = 1; i <= 5; i++)
44+
set.Add(i);
45+
for (int i = 1; i <= 5; i++)
46+
Assert.False(set.TryAdd(i));
47+
48+
Assert.Equal(5, set.Count);
49+
}
50+
51+
[Fact]
52+
public void Remove_ShouldRehashCluster_UnderFullCollision()
53+
{
54+
var set = new CeleritySet<int, ConstantIntHasher>(16);
55+
for (int i = 1; i <= 6; i++)
56+
set.Add(i);
57+
58+
Assert.True(set.Remove(3));
59+
Assert.Equal(5, set.Count);
60+
Assert.False(set.Contains(3));
61+
62+
Assert.True(set.Contains(1));
63+
Assert.True(set.Contains(2));
64+
Assert.True(set.Contains(4));
65+
Assert.True(set.Contains(5));
66+
Assert.True(set.Contains(6));
67+
}
68+
69+
[Fact]
70+
public void RemoveThenReinsert_ShouldWork_UnderFullCollision()
71+
{
72+
var set = new CeleritySet<int, ConstantIntHasher>(8);
73+
74+
for (int i = 1; i <= 10; i++)
75+
set.Add(i);
76+
77+
for (int i = 1; i <= 10; i += 2)
78+
Assert.True(set.Remove(i));
79+
80+
Assert.Equal(5, set.Count);
81+
82+
for (int i = 1; i <= 10; i += 2)
83+
set.Add(i);
84+
85+
Assert.Equal(10, set.Count);
86+
for (int i = 1; i <= 10; i++)
87+
Assert.True(set.Contains(i));
88+
}
89+
90+
[Fact]
91+
public void DefaultKey_ShouldWorkAlongside_CollisionChain()
92+
{
93+
var set = new CeleritySet<int, ConstantIntHasher>(16);
94+
set.Add(0);
95+
for (int i = 1; i <= 5; i++)
96+
set.Add(i);
97+
98+
Assert.Equal(6, set.Count);
99+
Assert.True(set.Contains(0));
100+
for (int i = 1; i <= 5; i++)
101+
Assert.True(set.Contains(i));
102+
103+
Assert.True(set.Remove(0));
104+
Assert.Equal(5, set.Count);
105+
Assert.False(set.Contains(0));
106+
for (int i = 1; i <= 5; i++)
107+
Assert.True(set.Contains(i));
108+
}
109+
110+
[Fact]
111+
public void Resize_ShouldPreserveAll_UnderFullCollision()
112+
{
113+
var set = new CeleritySet<int, ConstantIntHasher>(
114+
capacity: 4, loadFactor: 0.5f);
115+
116+
for (int i = 1; i <= 20; i++)
117+
set.Add(i);
118+
119+
Assert.Equal(20, set.Count);
120+
for (int i = 1; i <= 20; i++)
121+
Assert.True(set.Contains(i));
122+
}
123+
124+
// ---------------------------------------------------------------
125+
// String elements — exercises default(string) == null path
126+
// ---------------------------------------------------------------
127+
128+
[Fact]
129+
public void StringNull_ShouldRoundTrip()
130+
{
131+
var set = new CeleritySet<string, ConstantStringHasher>();
132+
set.Add(null!);
133+
134+
Assert.True(set.Contains(null!));
135+
Assert.Equal(1, set.Count);
136+
}
137+
138+
[Fact]
139+
public void StringNull_ShouldCoexistWithNonNullElements()
140+
{
141+
var set = new CeleritySet<string, ConstantStringHasher>(16);
142+
set.Add(null!);
143+
set.Add("alpha");
144+
set.Add("beta");
145+
set.Add("gamma");
146+
147+
Assert.Equal(4, set.Count);
148+
Assert.True(set.Contains(null!));
149+
Assert.True(set.Contains("alpha"));
150+
Assert.True(set.Contains("beta"));
151+
Assert.True(set.Contains("gamma"));
152+
}
153+
154+
[Fact]
155+
public void StringNull_Remove_ShouldWork()
156+
{
157+
var set = new CeleritySet<string, ConstantStringHasher>(16);
158+
set.Add(null!);
159+
set.Add("a");
160+
161+
Assert.True(set.Remove(null!));
162+
Assert.False(set.Contains(null!));
163+
Assert.Equal(1, set.Count);
164+
Assert.True(set.Contains("a"));
165+
}
166+
167+
[Fact]
168+
public void StringNull_Clear_ShouldReset()
169+
{
170+
var set = new CeleritySet<string, ConstantStringHasher>();
171+
set.Add(null!);
172+
set.Add("x");
173+
174+
set.Clear();
175+
176+
Assert.Equal(0, set.Count);
177+
Assert.False(set.Contains(null!));
178+
Assert.False(set.Contains("x"));
179+
180+
// Reusable after clear.
181+
set.Add(null!);
182+
Assert.Equal(1, set.Count);
183+
Assert.True(set.Contains(null!));
184+
}
185+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using Celerity.Collections;
2+
using Celerity.Hashing;
3+
4+
namespace Celerity.Tests.Collections;
5+
6+
public class CeleritySetTests
7+
{
8+
[Fact]
9+
public void TryAdd_ShouldAddAndContain()
10+
{
11+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
12+
Assert.True(set.TryAdd(10));
13+
Assert.True(set.Contains(10));
14+
Assert.Equal(1, set.Count);
15+
}
16+
17+
[Fact]
18+
public void TryAdd_ShouldReturnFalse_WhenDuplicate()
19+
{
20+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
21+
Assert.True(set.TryAdd(10));
22+
Assert.False(set.TryAdd(10));
23+
Assert.Equal(1, set.Count);
24+
}
25+
26+
[Fact]
27+
public void Add_ShouldThrow_WhenDuplicate()
28+
{
29+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
30+
set.Add(10);
31+
Assert.Throws<ArgumentException>(() => set.Add(10));
32+
}
33+
34+
[Fact]
35+
public void Contains_ShouldReturnFalse_WhenNotPresent()
36+
{
37+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
38+
Assert.False(set.Contains(99));
39+
}
40+
41+
[Fact]
42+
public void Remove_ShouldDeleteElement()
43+
{
44+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
45+
set.Add(7);
46+
47+
Assert.True(set.Remove(7));
48+
Assert.False(set.Contains(7));
49+
Assert.Equal(0, set.Count);
50+
}
51+
52+
[Fact]
53+
public void Remove_ShouldReturnFalse_WhenNotPresent()
54+
{
55+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
56+
Assert.False(set.Remove(7));
57+
}
58+
59+
[Fact]
60+
public void Set_ShouldResize_WhenThresholdExceeded()
61+
{
62+
var set = new CeleritySet<int, Int32WangNaiveHasher>(4);
63+
set.Add(1);
64+
set.Add(2);
65+
set.Add(3);
66+
set.Add(4); // Triggers resize
67+
68+
Assert.Equal(4, set.Count);
69+
Assert.True(set.Contains(1));
70+
Assert.True(set.Contains(2));
71+
Assert.True(set.Contains(3));
72+
Assert.True(set.Contains(4));
73+
}
74+
75+
// Regression: default(TKey) collides with the "empty slot" sentinel.
76+
[Fact]
77+
public void TryAdd_ShouldHandleZeroIntKey()
78+
{
79+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
80+
Assert.True(set.TryAdd(0));
81+
Assert.True(set.Contains(0));
82+
Assert.Equal(1, set.Count);
83+
}
84+
85+
[Fact]
86+
public void TryAdd_ZeroIntKey_ShouldReturnFalse_WhenDuplicate()
87+
{
88+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
89+
Assert.True(set.TryAdd(0));
90+
Assert.False(set.TryAdd(0));
91+
Assert.Equal(1, set.Count);
92+
}
93+
94+
[Fact]
95+
public void Remove_ShouldHandleZeroIntKey()
96+
{
97+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
98+
set.Add(0);
99+
set.Add(1);
100+
101+
Assert.True(set.Remove(0));
102+
Assert.False(set.Contains(0));
103+
Assert.True(set.Contains(1));
104+
Assert.Equal(1, set.Count);
105+
Assert.False(set.Remove(0));
106+
}
107+
108+
[Fact]
109+
public void DefaultKey_ShouldSurviveResize()
110+
{
111+
var set = new CeleritySet<int, Int32WangNaiveHasher>(4);
112+
set.Add(0);
113+
set.Add(1);
114+
set.Add(2);
115+
set.Add(3);
116+
set.Add(4); // Triggers resize while the default-key entry is live.
117+
118+
Assert.Equal(5, set.Count);
119+
Assert.True(set.Contains(0));
120+
Assert.True(set.Contains(1));
121+
Assert.True(set.Contains(4));
122+
}
123+
124+
[Fact]
125+
public void Clear_ShouldRemoveAllElements()
126+
{
127+
var set = new CeleritySet<int, Int32WangNaiveHasher>();
128+
for (int i = 0; i < 32; i++)
129+
set.Add(i);
130+
131+
set.Clear();
132+
133+
Assert.Equal(0, set.Count);
134+
for (int i = 0; i < 32; i++)
135+
Assert.False(set.Contains(i));
136+
137+
// Reusable after clear.
138+
set.Add(0);
139+
set.Add(5);
140+
Assert.Equal(2, set.Count);
141+
Assert.True(set.Contains(0));
142+
Assert.True(set.Contains(5));
143+
}
144+
145+
// Guid.Empty == default(Guid)
146+
[Fact]
147+
public void GuidEmpty_ShouldRoundTrip()
148+
{
149+
var set = new CeleritySet<Guid, GuidIdentityHasher>();
150+
set.Add(Guid.Empty);
151+
var other = Guid.NewGuid();
152+
set.Add(other);
153+
154+
Assert.True(set.Contains(Guid.Empty));
155+
Assert.True(set.Contains(other));
156+
Assert.Equal(2, set.Count);
157+
}
158+
159+
[Fact]
160+
public void RemoveThenReinsert_ManyElements()
161+
{
162+
var set = new CeleritySet<int, Int32WangNaiveHasher>(8);
163+
for (int i = 1; i <= 100; i++)
164+
set.Add(i);
165+
166+
for (int i = 1; i <= 100; i += 2)
167+
Assert.True(set.Remove(i));
168+
169+
Assert.Equal(50, set.Count);
170+
171+
for (int i = 1; i <= 100; i += 2)
172+
set.Add(i);
173+
174+
Assert.Equal(100, set.Count);
175+
for (int i = 1; i <= 100; i++)
176+
Assert.True(set.Contains(i));
177+
}
178+
179+
private struct GuidIdentityHasher : IHashProvider<Guid>
180+
{
181+
public int Hash(Guid key) => key.GetHashCode();
182+
}
183+
}

0 commit comments

Comments
 (0)