Skip to content

Commit 178d7f9

Browse files
authored
Fix issue #465. Added tests and adjusted logic to get more complete test coverage (#530)
1 parent a985c67 commit 178d7f9

File tree

5 files changed

+211
-62
lines changed

5 files changed

+211
-62
lines changed

Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public static class TimSorterTests
99
private static readonly TimSorterSettings Settings = new();
1010

1111
[Test]
12-
public static void ArraySorted(
13-
[Random(0, 10_000, 2000)] int n)
12+
public static void Sort_ShouldBeEquivalentToSuccessfulBasicSort(
13+
[Random(0, 10_000, 5000)] int n)
1414
{
1515
// Arrange
1616
var sorter = new TimSorter<int>(Settings, IntComparer);
@@ -25,7 +25,7 @@ public static void ArraySorted(
2525
}
2626

2727
[Test]
28-
public static void TinyArray()
28+
public static void Sort_TinyArray_ShouldSortCorrectly()
2929
{
3030
// Arrange
3131
var sorter = new TimSorter<int>(Settings, IntComparer);
@@ -40,7 +40,7 @@ public static void TinyArray()
4040
}
4141

4242
[Test]
43-
public static void SmallChunks()
43+
public static void Sort_SmallChunks_ShouldSortCorrectly()
4444
{
4545
// Arrange
4646
var sorter = new TimSorter<int>(Settings, IntComparer);
@@ -63,4 +63,74 @@ public static void SmallChunks()
6363
// Assert
6464
Assert.That(correctArray, Is.EqualTo(testArray));
6565
}
66+
67+
[Test]
68+
public static void Sort_ThrowsArgumentNullException_WhenArrayIsNull()
69+
{
70+
// Arrange
71+
var sorter = new TimSorter<int>(Settings, IntComparer);
72+
73+
// Act & Assert
74+
Assert.Throws<ArgumentNullException>(() => sorter.Sort(null!, IntComparer));
75+
}
76+
77+
[Test]
78+
public static void Sort_UsesDefaultComparer_WhenComparerIsNull()
79+
{
80+
// Arrange
81+
var sorter = new TimSorter<int>(Settings, null!);
82+
var (correctArray, testArray) = RandomHelper.GetArrays(20);
83+
84+
// Act
85+
sorter.Sort(testArray, IntComparer);
86+
Array.Sort(correctArray, IntComparer);
87+
88+
// Assert
89+
Assert.That(correctArray, Is.EqualTo(testArray));
90+
}
91+
92+
[Test]
93+
public static void Sort_AlreadySortedArray_RemainsUnchanged()
94+
{
95+
// Arrange
96+
var sorter = new TimSorter<int>(Settings, IntComparer);
97+
var array = new[] { 1, 2, 3, 4, 5 };
98+
var expected = new[] { 1, 2, 3, 4, 5 };
99+
100+
// Act
101+
sorter.Sort(array, IntComparer);
102+
103+
// Assert
104+
Assert.That(array, Is.EqualTo(expected));
105+
}
106+
107+
[Test]
108+
public static void MergeAt_ShouldReturnEarly_WhenLenAIsZero()
109+
{
110+
// Arrange: left run is all less than right run's first element
111+
var array = Enumerable.Range(1, 25).Concat(Enumerable.Range(100, 25)).ToArray();
112+
var sortedArray = Enumerable.Range(1, 25).Concat(Enumerable.Range(100, 25)).ToArray();
113+
var sorter = new TimSorter<int>(new TimSorterSettings(), Comparer<int>.Default);
114+
115+
// Act
116+
sorter.Sort(array, Comparer<int>.Default);
117+
118+
// Assert: Array order will not have changed, and the lenA <= 0 branch should be hit
119+
Assert.That(sortedArray, Is.EqualTo(array));
120+
}
121+
122+
[Test]
123+
public static void MergeAt_ShouldReturnEarly_WhenLenBIsZero()
124+
{
125+
// Arrange: right run is all less than left run's last element
126+
var array = Enumerable.Range(100, 25).Concat(Enumerable.Range(1, 25)).ToArray();
127+
var sortedArray = Enumerable.Range(1, 25).Concat(Enumerable.Range(100, 25)).ToArray();
128+
var sorter = new TimSorter<int>(new TimSorterSettings(), Comparer<int>.Default);
129+
130+
// Act
131+
sorter.Sort(array, Comparer<int>.Default);
132+
133+
// Assert: The left and right sides of the array should have swapped places, and the lenB <= 0 branch should be hit
134+
Assert.That(sortedArray, Is.EqualTo(array));
135+
}
66136
}

Algorithms/Sorters/Comparison/TimSorter.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Algorithms.Sorters.Comparison;
88
///
99
/// This class is based on a Java interpretation of Tim Peter's original work.
1010
/// Java class is viewable here:
11-
/// http://cr.openjdk.java.net/~martin/webrevs/openjdk7/timsort/raw_files/new/src/share/classes/java/util/TimSort.java
11+
/// https://web.archive.org/web/20190119032242/http://cr.openjdk.java.net:80/~martin/webrevs/openjdk7/timsort/raw_files/new/src/share/classes/java/util/TimSort.java
1212
///
1313
/// Tim Peters's list sort for Python, is described in detail here:
1414
/// http://svn.python.org/projects/python/trunk/Objects/listsort.txt
@@ -27,9 +27,6 @@ public class TimSorter<T> : IComparisonSorter<T>
2727
private readonly int minMerge;
2828
private readonly int initMinGallop;
2929

30-
// Pool of reusable TimChunk objects for memory efficiency.
31-
private readonly TimChunk<T>[] chunkPool = new TimChunk<T>[2];
32-
3330
private readonly int[] runBase;
3431
private readonly int[] runLengths;
3532

@@ -56,6 +53,11 @@ private class TimChunk<Tc>
5653
public TimSorter(TimSorterSettings settings, IComparer<T> comparer)
5754
{
5855
initMinGallop = minGallop;
56+
57+
// Using the worst case stack size from the C implementation.
58+
// Based on the findings in the original listsort.txt:
59+
// ... the stack can never grow larger than about log_base_phi(N) entries, where phi = (1 + sqrt(5)) / 2 ~= 1.618.
60+
// Thus a small # of stack slots suffice for very large arrays ...
5961
runBase = new int[85];
6062
runLengths = new int[85];
6163

@@ -77,7 +79,8 @@ public TimSorter(TimSorterSettings settings, IComparer<T> comparer)
7779
/// <param name="comparer">Compares elements.</param>
7880
public void Sort(T[] array, IComparer<T> comparer)
7981
{
80-
this.comparer = comparer;
82+
ArgumentNullException.ThrowIfNull(array);
83+
8184
var start = 0;
8285
var remaining = array.Length;
8386

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
namespace Algorithms.Sorters.Comparison;
22

3-
public class TimSorterSettings
3+
public class TimSorterSettings(int minMerge = 32, int minGallop = 7)
44
{
5-
public int MinMerge { get; }
5+
public int MinMerge { get; } = minMerge;
66

7-
public int MinGallop { get; }
8-
9-
public TimSorterSettings(int minMerge = 32, int minGallop = 7)
10-
{
11-
MinMerge = minMerge;
12-
MinGallop = minGallop;
13-
}
7+
public int MinGallop { get; } = minGallop;
148
}

DataStructures.Tests/Hashing/HashTableTests.cs

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,18 @@ public void Add_IncreasesCount_WhenValueAlreadyExists()
113113
Assert.That(hashTable.Count, Is.EqualTo(2));
114114
}
115115

116+
[Test]
117+
public void Add_ThrowsException_OnCollision()
118+
{
119+
// Arrange
120+
var hashTable = new HashTable<Collider, int>();
121+
hashTable.Add(new Collider(1), 1);
122+
123+
124+
// Act & Assert
125+
Assert.Throws<ArgumentException>(() => hashTable.Add(new Collider(1), 2));
126+
}
127+
116128
[Test]
117129
public void Remove_ThrowsException_WhenKeyIsNull()
118130
{
@@ -154,12 +166,45 @@ public void Remove_DecreasesCount_WhenKeyExists()
154166
public void Remove_DoesNotDecreaseCount_WhenKeyDoesNotExist()
155167
{
156168
var hashTable = new HashTable<string, int>();
157-
158169
hashTable.Remove("a");
159170

160171
Assert.That(hashTable.Count, Is.EqualTo(0));
161172
}
162173

174+
[Test]
175+
public void Remove_TriggersResizeDown()
176+
{
177+
var hashTable = new HashTable<int, string>(4);
178+
for (var i = 1; i <= 50; i++)
179+
{
180+
hashTable.Add(i, $"Value{i}");
181+
}
182+
183+
for (var i = 1; i <= 40; i++)
184+
{
185+
hashTable.Remove(i);
186+
}
187+
188+
Assert.That(hashTable.Capacity, Is.EqualTo(40));
189+
}
190+
191+
[Test]
192+
public void Remove_TriggersResizeDown_MinimumOfDefaultCapacity()
193+
{
194+
var hashTable = new HashTable<int, string>(4);
195+
for (var i = 1; i <= 50; i++)
196+
{
197+
hashTable.Add(i, $"Value{i}");
198+
}
199+
200+
for (var i = 1; i <= 48; i++)
201+
{
202+
hashTable.Remove(i);
203+
}
204+
205+
Assert.That(hashTable.Capacity, Is.EqualTo(16));
206+
}
207+
163208
[Test]
164209
public void ContainsValue_ReturnsFalse_WhenValueDoesNotExist()
165210
{
@@ -234,6 +279,17 @@ public void Clear_RemovesAllElements()
234279
Assert.That(hashTable.ContainsKey("a"), Is.False);
235280
}
236281

282+
[Test]
283+
public void Clear_ResetsTable()
284+
{
285+
var hashTable = new HashTable<int, string>();
286+
hashTable.Add(1, "A");
287+
hashTable.Clear();
288+
hashTable.Add(2, "B");
289+
Assert.That(hashTable.Count, Is.EqualTo(1));
290+
Assert.That(hashTable[2], Is.EqualTo("B"));
291+
}
292+
237293
[Test]
238294
public void Resize_IncreasesCapacity()
239295
{
@@ -313,6 +369,13 @@ public void Constructor_ThrowsException_WhenLoadFactorIsGreaterThanOne()
313369
Assert.Throws<ArgumentOutOfRangeException>(() => new HashTable<string, int>(4, 2));
314370
}
315371

372+
[Test]
373+
public void Constructor_RoundsCapacityToPrime()
374+
{
375+
var hashTable = new HashTable<int, string>(17);
376+
Assert.That(hashTable.Capacity, Is.EqualTo(19));
377+
}
378+
316379
[Test]
317380
public void GetIndex_ThrowsException_WhenKeyIsNull()
318381
{
@@ -388,6 +451,23 @@ public void Test_NegativeHashKey_ReturnsCorrectValue()
388451
Assert.That(hashTable[new NegativeHashKey(1)], Is.EqualTo(1));
389452
}
390453

454+
[Test]
455+
public void Resize_HandlesNegativeHashCodeCorrectly()
456+
{
457+
// Arrange
458+
var hashTable = new HashTable<NegativeHashKey, string>(2);
459+
460+
// Act
461+
hashTable.Add(new NegativeHashKey(1), "A");
462+
hashTable.Add(new NegativeHashKey(2), "B");
463+
hashTable.Add(new NegativeHashKey(3), "C");
464+
465+
// Assert
466+
Assert.That(hashTable[new NegativeHashKey(1)], Is.EqualTo("A"));
467+
Assert.That(hashTable[new NegativeHashKey(2)], Is.EqualTo("B"));
468+
Assert.That(hashTable[new NegativeHashKey(3)], Is.EqualTo("C"));
469+
}
470+
391471
[Test]
392472
public void Add_ShouldTriggerResize_WhenThresholdExceeded()
393473
{
@@ -396,14 +476,14 @@ public void Add_ShouldTriggerResize_WhenThresholdExceeded()
396476
var hashTable = new HashTable<int, string>(initialCapacity);
397477

398478
// Act
399-
for (int i = 1; i <= 4; i++) // Start keys from 1 to avoid default(TKey) = 0 issue
479+
for (var i = 1; i <= 32; i++)
400480
{
401481
hashTable.Add(i, $"Value{i}");
402482
}
403483

404484
// Assert
405-
hashTable.Capacity.Should().BeGreaterThan(initialCapacity); // Ensure resizing occurred
406-
hashTable.Count.Should().Be(4); // Verify count reflects number of added items
485+
hashTable.Capacity.Should().BeGreaterThan(initialCapacity);
486+
hashTable.Count.Should().Be(32);
407487
}
408488

409489

@@ -473,23 +553,28 @@ public void Capacity_Increases_WhenResizeOccurs()
473553
var initialCapacity = 4;
474554
var hashTable = new HashTable<int, string>(initialCapacity);
475555

476-
for (int i = 1; i <= 5; i++)
556+
for (var i = 1; i <= 5; i++)
477557
{
478558
hashTable.Add(i, $"Value{i}");
479559
}
480560

481561
hashTable.Capacity.Should().BeGreaterThan(initialCapacity);
482562
}
483-
}
484-
485-
public class NegativeHashKey
486-
{
487-
private readonly int id;
488563

489-
public NegativeHashKey(int id)
564+
[Test]
565+
public void IndexerSet_Throws_KeyNotFound()
490566
{
491-
this.id = id;
567+
// Arrange
568+
var hashTable = new HashTable<int, string>();
569+
570+
// Act & Assert
571+
Assert.Throws<KeyNotFoundException>(() => hashTable[1] = "A");
492572
}
573+
}
574+
575+
public class NegativeHashKey(int id)
576+
{
577+
private readonly int id = id;
493578

494579
public override int GetHashCode()
495580
{
@@ -506,3 +591,14 @@ public override bool Equals(object? obj)
506591
return false;
507592
}
508593
}
594+
595+
/// <summary>
596+
/// Class to simulate hash collisions
597+
/// </summary>
598+
/// <param name="id">Id of this object</param>
599+
public class Collider(int id)
600+
{
601+
private readonly int id = id;
602+
public override int GetHashCode() => 42; // Force all instances to collide
603+
public override bool Equals(object? obj) => obj is Collider other && other.id == id;
604+
}

0 commit comments

Comments
 (0)