Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Neo.Extensions/Collections/CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
// modifications are permitted.


using Neo.Extensions.Factories;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Neo.Extensions
{
Expand Down Expand Up @@ -76,5 +78,43 @@ public static IEnumerable<T[]> Chunk<T>(this IReadOnlyCollection<T>? source, int
yield return chunk;
}
}

/// <summary>
/// Randomly samples a specified number of elements from the collection using reservoir sampling algorithm.
/// This method ensures each element has an equal probability of being selected, regardless of the collection size.
/// If the count is greater than the collection size, the entire collection will be returned.
/// </summary>
/// <typeparam name="T">The type of the elements in the collection.</typeparam>
/// <param name="collection">The collection to sample from.</param>
/// <param name="count">The number of elements to sample.</param>
/// <returns>An array containing the randomly sampled elements.</returns>
/// <exception cref="ArgumentNullException">Thrown when the collection is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the count is less than 0</exception>
[return: NotNull]
public static T[] Sample<T>(this IReadOnlyCollection<T> collection, int count)
{
ArgumentNullException.ThrowIfNull(collection);
ArgumentOutOfRangeException.ThrowIfLessThan(count, 0, nameof(count));

if (count == 0) return [];

var reservoir = new T[Math.Min(count, collection.Count)];
var currentIndex = 0;
foreach (var item in collection)
{
if (currentIndex < reservoir.Length)
{
reservoir[currentIndex] = item;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have the same length there is no rand order

Copy link
Contributor Author

@Wi1l-B0t Wi1l-B0t Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have the same length there is no rand order

All peers selected in this case. and the items in a hashset is unordered

So order doesn't matter in this case, I think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So order doesn't matter in this case, I think.

Agree, although the logic changes, it's ok to me

}
else
{
var randomIndex = RandomNumberFactory.NextInt32(0, currentIndex + 1);
if (randomIndex < reservoir.Length) reservoir[randomIndex] = item;
}
currentIndex++;
}

return reservoir;
}
}
}
4 changes: 1 addition & 3 deletions src/Neo/Network/P2P/Peer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,7 @@ private void OnTimer()
if (UnconnectedPeers.Count == 0)
NeedMorePeers(Config.MinDesiredConnections - ConnectedPeers.Count);

var endpoints = UnconnectedPeers.OrderBy(u => RandomNumberFactory.NextInt32())
.Take(Config.MinDesiredConnections - ConnectedPeers.Count)
.ToArray();
var endpoints = UnconnectedPeers.Sample(Config.MinDesiredConnections - ConnectedPeers.Count);
ImmutableInterlocked.Update(ref UnconnectedPeers, p => p.Except(endpoints));
foreach (var endpoint in endpoints)
{
Expand Down
20 changes: 20 additions & 0 deletions tests/Neo.Extensions.Tests/Collections/UT_CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,25 @@ public void TestRemoveWhere()
Assert.AreEqual("a", dict[1]);
Assert.AreEqual("c", dict[3]);
}

[TestMethod]
public void TestSample()
{
var list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var sampled = list.Sample(5);
Assert.AreEqual(5, sampled.Length);
foreach (var item in sampled) Assert.Contains(item, list);

sampled = list.Sample(10);
Assert.AreEqual(10, sampled.Length);
foreach (var item in sampled) Assert.Contains(item, list);

sampled = list.Sample(0);
Assert.AreEqual(0, sampled.Length);

sampled = list.Sample(100);
Assert.AreEqual(10, sampled.Length);
foreach (var item in sampled) Assert.Contains(item, list);
}
}
}