Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 301708f

Browse files
authored
Adding TrimExcess API to Dictionary and providing tests (#26239)
1 parent 257a149 commit 301708f

File tree

4 files changed

+227
-1
lines changed

4 files changed

+227
-1
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
7+
namespace Common.System
8+
{
9+
internal static class RandomExtensions
10+
{
11+
public static void Shuffle<T>(this Random random, T[] array)
12+
{
13+
int n = array.Length;
14+
while (n > 1)
15+
{
16+
int k = random.Next(n--);
17+
T temp = array[n];
18+
array[n] = array[k];
19+
array[k] = temp;
20+
}
21+
}
22+
}
23+
}

src/System.Collections/ref/System.Collections.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ public virtual void GetObjectData(System.Runtime.Serialization.SerializationInfo
9696
public virtual void OnDeserialization(object sender) { }
9797
public bool Remove(TKey key) { throw null; }
9898
public bool Remove(TKey key, out TValue value) { throw null; }
99+
public void TrimExcess(int capacity) { }
100+
public void TrimExcess() { }
99101
public bool TryAdd(TKey key, TValue value) { throw null; }
100102
void System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey, TValue>>.Add(System.Collections.Generic.KeyValuePair<TKey, TValue> keyValuePair) { }
101103
bool System.Collections.Generic.ICollection<System.Collections.Generic.KeyValuePair<TKey, TValue>>.Contains(System.Collections.Generic.KeyValuePair<TKey, TValue> keyValuePair) { throw null; }

src/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.netcoreapp.cs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Collections.Generic;
6+
using System.Linq;
7+
using Common.System;
68
using Xunit;
79

810
namespace System.Collections.Tests
@@ -220,5 +222,201 @@ public void EnsureCapacity_Generic_CapacityIsSetToPrimeNumberLargerOrEqualToRequ
220222
}
221223

222224
#endregion
225+
226+
#region TrimExcess
227+
228+
[Fact]
229+
public void TrimExcess_Generic_NegativeCapacity_Throw()
230+
{
231+
var dictionary = new Dictionary<TKey, TValue>();
232+
AssertExtensions.Throws<ArgumentOutOfRangeException>("capacity", () => dictionary.TrimExcess(-1));
233+
}
234+
235+
[Theory]
236+
[InlineData(20)]
237+
[InlineData(23)]
238+
public void TrimExcess_Generic_CapacitySmallerThanCount_Throws(int suggestedCapacity)
239+
{
240+
var dictionary = new Dictionary<TKey, TValue>();
241+
dictionary.Add(GetNewKey(dictionary), CreateTValue(0));
242+
AssertExtensions.Throws<ArgumentOutOfRangeException>("capacity", () => dictionary.TrimExcess(0));
243+
244+
dictionary = new Dictionary<TKey, TValue>(suggestedCapacity);
245+
dictionary.Add(GetNewKey(dictionary), CreateTValue(0));
246+
AssertExtensions.Throws<ArgumentOutOfRangeException>("capacity", () => dictionary.TrimExcess(0));
247+
}
248+
249+
[Fact]
250+
public void TrimExcess_Generic_LargeInitialCapacity_TrimReducesSize()
251+
{
252+
var dictionary = new Dictionary<TKey, TValue>(20);
253+
dictionary.TrimExcess(7);
254+
Assert.Equal(7, dictionary.EnsureCapacity(0));
255+
}
256+
257+
[Theory]
258+
[InlineData(20)]
259+
[InlineData(23)]
260+
public void TrimExcess_Generic_TrimToLargerThanExistingCapacity_DoesNothing(int suggestedCapacity)
261+
{
262+
var dictionary = new Dictionary<TKey, TValue>();
263+
int capacity = dictionary.EnsureCapacity(0);
264+
dictionary.TrimExcess(suggestedCapacity);
265+
Assert.Equal(capacity, dictionary.EnsureCapacity(0));
266+
267+
dictionary = new Dictionary<TKey, TValue>(suggestedCapacity/2);
268+
capacity = dictionary.EnsureCapacity(0);
269+
dictionary.TrimExcess(suggestedCapacity);
270+
Assert.Equal(capacity, dictionary.EnsureCapacity(0));
271+
}
272+
273+
[Fact]
274+
public void TrimExcess_Generic_DictionaryNotInitialized_CapacityRemainsAsMinPossible()
275+
{
276+
var dictionary = new Dictionary<TKey, TValue>();
277+
Assert.Equal(0, dictionary.EnsureCapacity(0));
278+
dictionary.TrimExcess();
279+
Assert.Equal(0, dictionary.EnsureCapacity(0));
280+
}
281+
282+
[Theory]
283+
[InlineData(1)]
284+
[InlineData(85)]
285+
[InlineData(89)]
286+
public void TrimExcess_Generic_ClearThenTrimNonEmptyDictionary_SetsCapacityTo3(int count)
287+
{
288+
Dictionary<TKey, TValue> dictionary = (Dictionary<TKey, TValue>)GenericIDictionaryFactory(count);
289+
Assert.Equal(count, dictionary.Count);
290+
// The smallest possible capacity size after clearing a dictionary is 3
291+
dictionary.Clear();
292+
dictionary.TrimExcess();
293+
Assert.Equal(3, dictionary.EnsureCapacity(0));
294+
}
295+
296+
[Theory]
297+
[InlineData(85)]
298+
[InlineData(89)]
299+
public void TrimExcess_NoArguments_TrimsToAtLeastCount(int count)
300+
{
301+
var dictionary = new Dictionary<int, int>(20);
302+
for (int i = 0; i < count; i++)
303+
{
304+
dictionary.Add(i, 0);
305+
}
306+
dictionary.TrimExcess();
307+
Assert.InRange(dictionary.EnsureCapacity(0), count, int.MaxValue);
308+
}
309+
310+
[Theory]
311+
[InlineData(85)]
312+
[InlineData(89)]
313+
public void TrimExcess_WithArguments_OnDictionaryWithManyElementsRemoved_TrimsToAtLeastRequested(int finalCount)
314+
{
315+
const int InitToFinalRatio = 10;
316+
int initialCount = InitToFinalRatio * finalCount;
317+
var dictionary = new Dictionary<int, int>(initialCount);
318+
Assert.InRange(dictionary.EnsureCapacity(0), initialCount, int.MaxValue);
319+
for (int i = 0; i < initialCount; i++)
320+
{
321+
dictionary.Add(i, 0);
322+
}
323+
for (int i = 0; i < initialCount - finalCount; i++)
324+
{
325+
dictionary.Remove(i);
326+
}
327+
for (int i = InitToFinalRatio; i > 0; i--)
328+
{
329+
dictionary.TrimExcess(i * finalCount);
330+
Assert.InRange(dictionary.EnsureCapacity(0), i * finalCount, int.MaxValue);
331+
}
332+
}
333+
334+
[Theory]
335+
[InlineData(1000, 900, 5000, 85, 89)]
336+
[InlineData(1000, 400, 5000, 85, 89)]
337+
[InlineData(1000, 900, 500, 85, 89)]
338+
[InlineData(1000, 400, 500, 85, 89)]
339+
[InlineData(1000, 400, 500, 1, 3)]
340+
public void TrimExcess_NoArgument_TrimAfterEachBulkAddOrRemove_TrimsToAtLeastCount(int initialCount, int numRemove, int numAdd, int newCount, int newCapacity)
341+
{
342+
Random random = new Random(32);
343+
var dictionary = new Dictionary<int, int>();
344+
dictionary.TrimExcess();
345+
Assert.InRange(dictionary.EnsureCapacity(0), dictionary.Count, int.MaxValue);
346+
347+
var initialKeys = new int[initialCount];
348+
for (int i = 0; i < initialCount; i++)
349+
{
350+
initialKeys[i] = i;
351+
}
352+
random.Shuffle(initialKeys);
353+
foreach (var key in initialKeys)
354+
{
355+
dictionary.Add(key, 0);
356+
}
357+
dictionary.TrimExcess();
358+
Assert.InRange(dictionary.EnsureCapacity(0), dictionary.Count, int.MaxValue);
359+
360+
random.Shuffle(initialKeys);
361+
for (int i = 0; i < numRemove; i++)
362+
{
363+
dictionary.Remove(initialKeys[i]);
364+
}
365+
dictionary.TrimExcess();
366+
Assert.InRange(dictionary.EnsureCapacity(0), dictionary.Count, int.MaxValue);
367+
368+
var moreKeys = new int[numAdd];
369+
for (int i = 0; i < numAdd; i++)
370+
{
371+
moreKeys[i] = i + initialCount;
372+
}
373+
random.Shuffle(moreKeys);
374+
foreach (var key in moreKeys)
375+
{
376+
dictionary.Add(key, 0);
377+
}
378+
int currentCount = dictionary.Count;
379+
dictionary.TrimExcess();
380+
Assert.InRange(dictionary.EnsureCapacity(0), currentCount, int.MaxValue);
381+
382+
int[] existingKeys = new int[currentCount];
383+
Array.Copy(initialKeys, numRemove, existingKeys, 0, initialCount - numRemove);
384+
Array.Copy(moreKeys, 0, existingKeys, initialCount - numRemove, numAdd);
385+
random.Shuffle(existingKeys);
386+
for (int i = 0; i < currentCount - newCount; i++)
387+
{
388+
dictionary.Remove(existingKeys[i]);
389+
}
390+
dictionary.TrimExcess();
391+
int finalCapacity = dictionary.EnsureCapacity(0);
392+
Assert.InRange(finalCapacity, newCount, initialCount);
393+
Assert.Equal(newCapacity, finalCapacity);
394+
}
395+
396+
[Fact]
397+
public void TrimExcess_DictionaryHasElementsChainedWithSameHashcode_Success()
398+
{
399+
var dictionary = new Dictionary<string, int>(7);
400+
for (int i = 0; i < 4; i++)
401+
{
402+
dictionary.Add(i.ToString(), 0);
403+
}
404+
var s_64bit = new string[] { "95e85f8e-67a3-4367-974f-dd24d8bb2ca2", "eb3d6fe9-de64-43a9-8f58-bddea727b1ca" };
405+
var s_32bit = new string[] { "25b1f130-7517-48e3-96b0-9da44e8bfe0e", "ba5a3625-bc38-4bf1-a707-a3cfe2158bae" };
406+
string[] chained = (Environment.Is64BitProcess ? s_64bit : s_32bit).ToArray();
407+
dictionary.Add(chained[0], 0);
408+
dictionary.Add(chained[1], 0);
409+
for (int i = 0; i < 4; i++)
410+
{
411+
dictionary.Remove(i.ToString());
412+
}
413+
dictionary.TrimExcess(3);
414+
Assert.Equal(2, dictionary.Count);
415+
int val;
416+
Assert.True(dictionary.TryGetValue(chained[0], out val));
417+
Assert.True(dictionary.TryGetValue(chained[1], out val));
418+
}
419+
420+
#endregion
223421
}
224422
}

src/System.Collections/tests/System.Collections.Tests.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
<Compile Include="$(CommonTestPath)\System\ObjectCloner.cs">
6868
<Link>Common\System\ObjectCloner.cs</Link>
6969
</Compile>
70+
<Compile Include="$(CommonTestPath)\System\RandomExtensions.cs">
71+
<Link>Common\System\RandomExtensions.cs</Link>
72+
</Compile>
7073
<Compile Include="$(CommonTestPath)\System\Collections\DictionaryExtensions.cs" Condition="'$(TargetGroup)'!='netcoreapp'">
7174
<Link>Common\System\Collections\DictionaryExtensions.cs</Link>
7275
</Compile>
@@ -163,4 +166,4 @@
163166
<EmbeddedResource Include="Resources\$(AssemblyName).rd.xml" />
164167
</ItemGroup>
165168
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
166-
</Project>
169+
</Project>

0 commit comments

Comments
 (0)