0

I am trying to create a method where it will allow me to take X amount of random elements from a collection, I have coded one to get 1 random element, but I need to make the "amount" parameter matter, how can I make it take that into account?

I also need to add a Where, because I only need to grab elements where the class instance inside the collection (HashSet) has a public field called 'Enabled' and is equal to true.

private static readonly Random Random = new Random();
private static readonly object Sync = new object();

public static T RandomElement<T>(this IEnumerable<T> enumerable, int amount)
{
    if (enumerable == null)
        throw new ArgumentNullException(nameof(enumerable));

    var count = enumerable.Count();

    int ndx;
    lock (Sync)
        ndx = Random.Next(count); // returns non-negative number less than max

    return enumerable.ElementAt(ndx);
}
xozijow
  • 91
  • 1
  • 1
  • 4
  • [this](https://stackoverflow.com/questions/48087/select-n-random-elements-from-a-listt-in-c-sharp) will help you. – ibubi Oct 25 '17 at 11:06
  • What if the enumerable is infinite, or worse - changes in size? It wouldn't hurt limiting the parameter to `ICollection` instead of `IEnumerable`. – IS4 Oct 25 '17 at 11:06
  • 1
    `Skip` and `Take`. Be sure to check bounds. – Crowcoder Oct 25 '17 at 11:07

5 Answers5

1

Take a look at: https://stackoverflow.com/a/1653204/7866667 (lazily evaluated, efficient implementation for a shuffle extension method)

As to the second part of your question, you don't need to add a Where, you already have it - rolling it all into one method wouldn't be idiomatic LINQ which you might as well do!

Given the above, the call for what you're trying to do would be:

var chosenStuff = enumerable
    .Where(someCondition)
    .Shuffle()
    .Take(amount);
j4nw
  • 2,227
  • 11
  • 26
0

I have this vague feeling that MoreLinq has bits and pieces that either do what you want or have the sources that you can patch together to make what you want.
Its a great project. Check it out!.

Quibblesome
  • 25,225
  • 10
  • 61
  • 100
0

You could do the following

private static readonly Random Random = new Random();
private static readonly object Sync = new object();

public static IEnumerable<T> RandomElement<T>(this IEnumerable<T> enumerable, int amount)
{
    if (enumerable == null)
        throw new ArgumentNullException(nameof(enumerable));

    var count = enumerable.Count();

    int ndx;
    lock (Sync)
        ndx = Random.Next(count); // returns non-negative number less than max

    return enumerable.Skip(ndx - amount).Take(amount);
}

Note, this will return a collection in the original order but at a random position

Jamie Rees
  • 7,973
  • 2
  • 45
  • 83
0

Ignoring the chance of getting same object more than once due to 'randomness' you could do something along these lines:

    private static readonly Random Random = new Random();
    private static readonly object Sync = new object();

    public static IEnumerable<T> RandomElements<T>(this IEnumerable<T> enumerable, int amount, Func<T, bool>  filter)
    {
        if (enumerable == null)
        {
            throw new ArgumentNullException(nameof(enumerable));
        }

        var filtered = enumerable.Where(filter);

        var take = Math.Min(filtered.Count(), amount);

        for (var i = 0; i < take; i++)
        {
            lock (Sync)
            {
                var randomIndex = Random.Next(filtered.Count);
                yield return filtered.ElementAt(randomIndex);
            }
        }
    }

Invocation:

var randomElements = collection.RandomElements(10, (MyObject obj) => obj.Enabled);

As stated in another answer; passing the where clause could be replaced by just including the linq where clause in the invocation as follows:

var randomElements = collection.Where(x => x.Enabled).RandomElements(10);
Olivier
  • 571
  • 5
  • 8
0

Try this way of generating random element from sequence (RandomElement method is from C# in Depth):

  1. If source is ICollection/ICollection, you can get number of elements from it, generate random number and pick and element

  2. If it just a sequence and if you take count on it and then random element -e you will enumerate it twice. Instead you go through it one element by one (explicitly fetching iterator) and then you update idea of random element with element from iterator with probability 1/n where n is number of visited elements up to this point. In the end, each element has an equal change of being selected.

  3. Run this n times from another method.

    static class RandomExtensions
    {
        public static IList<T> PickNRandomElements<T>(this IEnumerable<T> source, Random random, int count)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }
            if (random == null)
            {
                throw new ArgumentNullException(nameof(random));
            }
    
            var result = new List<T>();
            for (var i = 0; i < count; i++)
            {
                result.Add(source.RandomElement(random));
            }
    
            return result;
        }
    
        private static T RandomElement<T>(this IEnumerable<T> source, Random random)
        {
            ICollection collection = source as ICollection;
            if (collection != null)
            {
                int count = collection.Count;
                if (count == 0)
                {
                    throw new InvalidOperationException("Sequence was empty.");
                }
                int index = random.Next(count);
                return source.ElementAt(index);
            }
    
            ICollection<T> genericCollection = source as ICollection<T>;
            if (genericCollection != null)
            {
                int count = genericCollection.Count;
                if (count == 0)
                {
                    throw new InvalidOperationException("Sequence was empty.");
                }
                int index = random.Next(count);
                return source.ElementAt(index);
            }
    
            using (IEnumerator<T> iterator = source.GetEnumerator())
            {
                if (!iterator.MoveNext())
                {
                    throw new InvalidOperationException("Sequence was empty.");
                }
                int countSoFar = 1;
                T current = iterator.Current;
                while (iterator.MoveNext())
                {
                    countSoFar++;
                    if (random.Next(countSoFar) == 0)
                    {
                        current = iterator.Current;
                    }
                }
                return current;
            }
        }
    }
    
Sergei G
  • 1,550
  • 3
  • 24
  • 44