Skip to content

Commit 9d7f71e

Browse files
committed
Added more TimSorterTests tests and changed method names for clarity.
Added tests in HashTableTests for collision handling and resizing behavior. Improved null handling in HashTable methods and adjusted resizing logic.
1 parent f1fde96 commit 9d7f71e

File tree

5 files changed

+176
-60
lines changed

5 files changed

+176
-60
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: 9 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,9 @@ 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+
this.comparer = comparer ?? Comparer<T>.Default;
84+
8185
var start = 0;
8286
var remaining = array.Length;
8387

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: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,18 @@ public void Add_IncreasesCount_WhenValueAlreadyExists()
117117
Assert.That(hashTable.Count, Is.EqualTo(2));
118118
}
119119

120+
[Test]
121+
public void Add_ThrowsException_OnCollision()
122+
{
123+
// Arrange
124+
var hashTable = new HashTable<Collider, int>();
125+
hashTable.Add(new Collider(1), 1);
126+
127+
128+
// Act & Assert
129+
Assert.Throws<ArgumentException>(() => hashTable.Add(new Collider(1), 2));
130+
}
131+
120132
[Test]
121133
public void Remove_ThrowsException_WhenKeyIsNull()
122134
{
@@ -158,12 +170,28 @@ public void Remove_DecreasesCount_WhenKeyExists()
158170
public void Remove_DoesNotDecreaseCount_WhenKeyDoesNotExist()
159171
{
160172
var hashTable = new HashTable<string, int>();
161-
162173
hashTable.Remove("a");
163174

164175
Assert.That(hashTable.Count, Is.EqualTo(0));
165176
}
166177

178+
[Test]
179+
public void Remove_TriggersResizeDown()
180+
{
181+
var hashTable = new HashTable<int, string>(4);
182+
for (var i = 1; i <= 50; i++)
183+
{
184+
hashTable.Add(i, $"Value{i}");
185+
}
186+
187+
for (var i = 1; i <= 40; i++)
188+
{
189+
hashTable.Remove(i);
190+
}
191+
192+
Assert.That(hashTable.Capacity, Is.EqualTo(40));
193+
}
194+
167195
[Test]
168196
public void ContainsValue_ReturnsFalse_WhenValueDoesNotExist()
169197
{
@@ -238,6 +266,17 @@ public void Clear_RemovesAllElements()
238266
Assert.That(hashTable.ContainsKey("a"), Is.False);
239267
}
240268

269+
[Test]
270+
public void Clear_ResetsTable()
271+
{
272+
var hashTable = new HashTable<int, string>();
273+
hashTable.Add(1, "A");
274+
hashTable.Clear();
275+
hashTable.Add(2, "B");
276+
Assert.That(hashTable.Count, Is.EqualTo(1));
277+
Assert.That(hashTable[2], Is.EqualTo("B"));
278+
}
279+
241280
[Test]
242281
public void Resize_IncreasesCapacity()
243282
{
@@ -317,6 +356,13 @@ public void Constructor_ThrowsException_WhenLoadFactorIsGreaterThanOne()
317356
Assert.Throws<ArgumentOutOfRangeException>(() => new HashTable<string, int>(4, 2));
318357
}
319358

359+
[Test]
360+
public void Constructor_RoundsCapacityToPrime()
361+
{
362+
var hashTable = new HashTable<int, string>(17);
363+
Assert.That(hashTable.Capacity, Is.EqualTo(19));
364+
}
365+
320366
[Test]
321367
public void GetIndex_ThrowsException_WhenKeyIsNull()
322368
{
@@ -399,7 +445,7 @@ public void Add_ShouldTriggerResize_WhenThresholdExceeded()
399445
var hashTable = new HashTable<int, string>(initialCapacity);
400446

401447
// Act
402-
for (int i = 1; i <= 4; i++) // Start keys from 1 to avoid default(TKey) = 0 issue
448+
for (var i = 1; i <= 4; i++) // Start keys from 1 to avoid default(TKey) = 0 issue
403449
{
404450
hashTable.Add(i, $"Value{i}");
405451
}
@@ -476,23 +522,28 @@ public void Capacity_Increases_WhenResizeOccurs()
476522
var initialCapacity = 4;
477523
var hashTable = new HashTable<int, string>(initialCapacity);
478524

479-
for (int i = 1; i <= 5; i++)
525+
for (var i = 1; i <= 5; i++)
480526
{
481527
hashTable.Add(i, $"Value{i}");
482528
}
483529

484530
hashTable.Capacity.Should().BeGreaterThan(initialCapacity);
485531
}
486-
}
487-
488-
public class NegativeHashKey
489-
{
490-
private readonly int id;
491532

492-
public NegativeHashKey(int id)
533+
[Test]
534+
public void IndexerSet_Throws_KeyNotFound()
493535
{
494-
this.id = id;
536+
// Arrange
537+
var hashTable = new HashTable<int, string>();
538+
539+
// Act & Assert
540+
Assert.Throws<KeyNotFoundException>(() => hashTable[1] = "A");
495541
}
542+
}
543+
544+
public class NegativeHashKey(int id)
545+
{
546+
private readonly int id = id;
496547

497548
public override int GetHashCode()
498549
{
@@ -509,3 +560,14 @@ public override bool Equals(object? obj)
509560
return false;
510561
}
511562
}
563+
564+
/// <summary>
565+
/// Class to simulate hash collisions
566+
/// </summary>
567+
/// <param name="id">Id of this object</param>
568+
public class Collider(int id)
569+
{
570+
private readonly int id = id;
571+
public override int GetHashCode() => 42; // Force all instances to collide
572+
public override bool Equals(object? obj) => obj is Collider other && other.id == id;
573+
}

0 commit comments

Comments
 (0)