Skip to content

Commit cc49fb1

Browse files
committed
Add multiple methods to randomly choose elements.
1 parent 2a99950 commit cc49fb1

File tree

2 files changed

+254
-1
lines changed

2 files changed

+254
-1
lines changed

src/SimSharp/Core/Environment.cs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Collections.Generic;
1010
using System.Diagnostics;
1111
using System.IO;
12+
using System.Linq;
1213
using System.Threading;
1314
using System.Threading.Tasks;
1415

@@ -620,6 +621,60 @@ public TimeSpan RandWeibull(TimeSpan alpha, TimeSpan beta) {
620621
return RandWeibull(Random, alpha, beta);
621622
}
622623

624+
/// <summary>
625+
/// This method chooses a single element from <paramref name="source"/> with equal probability.
626+
/// </summary>
627+
/// <param name="random">The random number generator to use.</param>
628+
/// <param name="source">The elements to choose from.</param>
629+
/// <typeparam name="T">The type of the elements to be chosen.</typeparam>
630+
/// <returns>The chosen element.</returns>
631+
public T RandChoice<T>(IRandom random, IList<T> source) {
632+
var idx = random.Next(source.Count);
633+
return source[idx];
634+
}
635+
/// <summary>
636+
/// Calls <see cref="RandChoice{T}(IRandom, IList{T})"/> with the default RNG instance <see cref="Random"/>.
637+
/// </summary>
638+
/// <param name="source">The elements to choose from.</param>
639+
/// <typeparam name="T">The type of the elements to be chosen.</typeparam>
640+
/// <returns>The chosen element.</returns>
641+
public T RandChoice<T>(IList<T> source) {
642+
return RandChoice(Random, source);
643+
}
644+
645+
/// <summary>
646+
/// This method chooses <paramref name="count"/> elements from <paramref name="source"/> with repetition.
647+
/// </summary>
648+
/// <remarks>
649+
/// Runtime complexity for selecting M out of a list of N elements is O(M).
650+
/// Order is not preserved, the items are returned in arbitrary order.
651+
///
652+
/// Parameter <paramref name="count"/> can be 0 in which case the enumerable will be empty.
653+
/// </remarks>
654+
/// <exception cref="ArgumentException">
655+
/// Thrown when <paramref name="count"/> is negative.
656+
/// </exception>
657+
/// <param name="random">The random number generator to use.</param>
658+
/// <param name="source">The elements to choose from.</param>
659+
/// <param name="count">The number of elements to choose.</param>
660+
/// <typeparam name="T">The type of the elements to be chosen.</typeparam>
661+
/// <returns>An enumeration of the elements.</returns>
662+
public IEnumerable<T> RandChoice<T>(IRandom random, IList<T> source, int count) {
663+
if (count < 0) throw new ArgumentException($"parameter {nameof(count)} is negative ({count})");
664+
for (var i = 0; i < count; i++) {
665+
yield return source[random.Next(source.Count)];
666+
}
667+
}
668+
/// <summary>
669+
/// Calls <see cref="RandChoice{T}(IRandom, IList{T}, int)"/> with the default RNG instance <see cref="Random"/>.
670+
/// </summary>
671+
/// <param name="source">The elements to choose from.</param>
672+
/// <param name="count">The number of elements to choose.</param>
673+
/// <typeparam name="T">The type of the elements to be chosen.</typeparam>
674+
/// <returns>The chosen element.</returns>
675+
public IEnumerable<T> RandChoice<T>(IList<T> source, int count) {
676+
return RandChoice(Random, source, count);
677+
}
623678

624679
/// <summary>
625680
/// Generates a random sample from a given source
@@ -682,6 +737,170 @@ public T RandChoice<T>(IList<T> source, IList<double> weights) {
682737
return RandChoice(Random, source, weights);
683738
}
684739

740+
/// <summary>
741+
/// This methods chooses a single element from <paramref name="source"/> randomly and by only enumerating
742+
/// the elements.
743+
/// </summary>
744+
/// <remarks>
745+
/// The preferred and faster method is <see cref="RandChoice{T}(IRandom, IList{T})"/>. Use of this method should
746+
/// be limited to cases where it is undesirable to reserve a contiguous block of memory for <paramref name="source"/>.
747+
///
748+
/// This method iterates over all elements and calls the RNG each time, thus runtime complexity is O(N).
749+
/// </remarks>
750+
/// <param name="random">The random number generator to use.</param>
751+
/// <param name="source">The elements to choose from.</param>
752+
/// <typeparam name="T">The type of the elements to be chosen.</typeparam>
753+
/// <returns>The chosen element.</returns>
754+
public T RandChoiceOnline<T>(IRandom random, IEnumerable<T> source) {
755+
var iter = source.GetEnumerator();
756+
if (!iter.MoveNext()) throw new ArgumentException($"{nameof(source)} is empty");
757+
var chosen = iter.Current;
758+
var count = 2;
759+
while (iter.MoveNext()) {
760+
if (count * Random.NextDouble() < 1) {
761+
chosen = iter.Current;
762+
}
763+
count++;
764+
}
765+
return chosen;
766+
}
767+
/// <summary>
768+
/// Calls <see cref="RandChoiceOnline{T}(IRandom, IEnumerable{T})"/> with the default RNG instance <see cref="Random"/>.
769+
/// </summary>
770+
/// <remarks>
771+
/// The preferred and faster method is <see cref="RandChoice{T}(IList{T})"/>. Use of this method should
772+
/// be limited to cases where it is undesirable to reserve a contiguous block of memory for <paramref name="source"/>.
773+
///
774+
/// This method iterates over all elements and calls the RNG each time, thus runtime complexity is O(N).
775+
/// </remarks>
776+
/// <param name="source">The elements to choose from.</param>
777+
/// <typeparam name="T">The type of the elements to be chosen.</typeparam>
778+
/// <returns>The chosen element.</returns>
779+
public T RandChoiceOnline<T>(IEnumerable<T> source) {
780+
return RandChoiceOnline<T>(Random, source);
781+
}
782+
783+
/// <summary>
784+
/// This methods chooses <paramref name="count"/> element from <paramref name="source"/> randomly and by only enumerating
785+
/// the elements.
786+
/// </summary>
787+
/// <remarks>
788+
/// The preferred and faster method is <see cref="RandChoice{T}(IRandom, IList{T}, int)"/>. Use of this method should
789+
/// be limited to cases where it is undesirable to reserve a contiguous block of memory for <paramref name="source"/>.
790+
/// However, the method itself reserves an array of length <paramref name="count"/> and uses a single pass of all elements
791+
/// in <parmaref name="source"/>.
792+
///
793+
/// Using a count of 0 is possible and will return an empty enumerable.
794+
///
795+
/// For selecting M from a source of N elements runtime complexity is O(N*M). The random number generator will also be called M*N times.
796+
/// </remarks>
797+
/// <exception cref="ArgumentException">Thrown when <paramref name="count"/> is negative.</exception>
798+
/// <param name="random">The random number generator to use.</param>
799+
/// <param name="source">The elements to choose from.</param>
800+
/// <param name="count">The number of elements to choose.</param>
801+
/// <typeparam name="T">The type of the elements to be chosen.</typeparam>
802+
/// <returns>An enumeration of the chosen elements.</returns>
803+
public IEnumerable<T> RandChoiceOnline<T>(IRandom random, IEnumerable<T> source, int count) {
804+
if (count <= 0) {
805+
if (count == 0) return Enumerable.Empty<T>();
806+
else throw new ArgumentException($"parameter {nameof(count)} is negative ({count})");
807+
}
808+
var iter = source.GetEnumerator();
809+
if (!iter.MoveNext()) throw new ArgumentException($"{nameof(source)} is empty");
810+
var chosen = new T[count];
811+
for (var c = 0; c < count; c++) chosen[c] = iter.Current;
812+
var element = 2;
813+
while (iter.MoveNext()) {
814+
for (var c = 0; c < count; c++) {
815+
if (element * Random.NextDouble() < 1) {
816+
chosen[c] = iter.Current;
817+
}
818+
}
819+
element++;
820+
}
821+
return chosen;
822+
}
823+
/// <summary>
824+
/// Calls <see cref="RandChoiceOnline{T}(IRandom, IEnumerable{T}, int)"/> with the default RNG instance <see cref="Random"/>.
825+
/// </summary>
826+
/// <remarks>
827+
/// The preferred and faster method is <see cref="RandChoice{T}(IList{T}, int)"/>. Use of this method should
828+
/// be limited to cases where it is undesirable to reserve a contiguous block of memory for <paramref name="source"/>.
829+
/// However, the method itself reserves an array of length <paramref name="count"/> and uses a single pass of all elements
830+
/// in <parmaref name="source"/>.
831+
///
832+
/// Using a count of 0 is possible and will return an empty enumerable.
833+
///
834+
/// For selecting M from a source of N elements runtime complexity is O(N*M). The random number generator will also be called M*N times.
835+
/// </remarks>
836+
/// <param name="source">The elements to choose from.</param>
837+
/// <param name="count">The number of elements to choose.</param>
838+
/// <typeparam name="T">The type of the elements to be chosen.</typeparam>
839+
/// <returns>An enumeration of the chosen elements.</returns>
840+
public IEnumerable<T> RandChoiceOnline<T>(IEnumerable<T> source, int count) {
841+
return RandChoiceOnline<T>(Random, source, count);
842+
}
843+
844+
/// <summary>
845+
/// This method chooses <paramref name="count"/> elements from <parameref name="source"/> such that
846+
/// no element is selected twice. Respectively, elements that are M times in the source may also
847+
/// be selected up to M times.
848+
/// </summary>
849+
/// <remarks>
850+
/// The method is implemented to iterate over all elements respectively until <paramref name="count"/> elements
851+
/// are selected. Runtime complexity for selecting M out of a list of N elements is thus O(N).
852+
///
853+
/// Order is preserved, the items are returned in the same relative order as they appear in <paramref name="source"/>.
854+
///
855+
/// Parameter <paramref name="count"/> can be 0 in which case the enumerable will be empty.
856+
/// </remarks>
857+
/// <exception cref="ArgumentException">
858+
/// Thrown when <paramref name="count"/> is negative or when there are not enough items in <paramerf name="source"/>
859+
/// to choose from.
860+
/// </exception>
861+
/// <param name="random">The random number generator to use.</param>
862+
/// <param name="source">The elements to choose from.</param>
863+
/// <param name="count">The number of elements to choose.</param>
864+
/// <returns>An enumeration of the elements.</returns>
865+
public IEnumerable<T> RandChoiceNoRepetition<T>(IRandom random, IEnumerable<T> source, int count) {
866+
if (count <= 0) {
867+
if (count == 0) yield break;
868+
else throw new ArgumentException($"parameter {nameof(count)} is negative ({count})");
869+
}
870+
var remaining = count;
871+
foreach (var s in source) {
872+
if (random.NextDouble() * remaining < count) {
873+
count--;
874+
yield return s;
875+
if (count <= 0) yield break;
876+
}
877+
remaining--;
878+
}
879+
throw new ArgumentException($"there are not enough items in {nameof(source)} to choose {count} from without repetition.");
880+
}
881+
/// <summary>
882+
/// Calls <see cref="RandChoiceNoRepetition{T}(IRandom, IEnumerable{T}, int)"/> with the default RNG instance <see cref="Random"/>.
883+
/// </summary>
884+
/// <remarks>
885+
/// The method is implemented to iterate over all elements respectively until <paramref name="count"/> elements
886+
/// are selected. Runtime complexity for selecting M out of a list of N elements is thus O(N).
887+
///
888+
/// Order is preserved, the items are returned in the same relative order as they appear in <paramref name="source"/>.
889+
///
890+
/// Parameter <paramref name="count"/> can be 0 in which case the enumerable will be empty.
891+
/// </remarks>
892+
/// <exception cref="ArgumentException">
893+
/// Thrown when <paramref name="count"/> is negative or when there are not enough items in <paramerf name="source"/>
894+
/// to choose from.
895+
/// </exception>
896+
/// <param name="source">The elements to choose from without repetition.</param>
897+
/// <param name="count">The number of elements that should be chosen.</param>
898+
/// <typeparam name="T">The type of elements to be chosen.</typeparam>
899+
/// <returns>An enumeration of the elements.</returns>
900+
public IEnumerable<T> RandChoiceNoRepetition<T>(IEnumerable<T> source, int count) {
901+
return RandChoiceNoRepetition<T>(Random, source, count);
902+
}
903+
685904
#endregion
686905

687906
#region Random timeouts

src/Tests/RandomTest.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Linq;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Threading;
35
using Xunit;
46

@@ -50,5 +52,37 @@ public void RandChoiceTestTotalWeightMoreThanOne() {
5052

5153
Assert.Equal(res1, res2);
5254
}
55+
56+
[Fact]
57+
public void RandChoiceTests() {
58+
var source = new [] { "a", "b", "c", "d", "e", "f", "g" };
59+
for (var i = 0; i < 50; i++) {
60+
var env = new Simulation(i);
61+
62+
Assert.Contains(env.RandChoice(source), source);
63+
foreach (var s in env.RandChoice(source, 5))
64+
Assert.Contains(s, source);
65+
Assert.Equal(4, env.RandChoice(source, 4).Count());
66+
Assert.Equal(20, env.RandChoice(source, 20).Count());
67+
Assert.Empty(env.RandChoice(source, 0));
68+
Assert.Throws<ArgumentException>(() => env.RandChoice(source, -1).ToList());
69+
70+
Assert.Contains(env.RandChoiceOnline(source), source);
71+
foreach (var s in env.RandChoiceOnline(source, 5))
72+
Assert.Contains(s, source);
73+
Assert.Equal(4, env.RandChoiceOnline(source, 4).Count());
74+
Assert.Equal(20, env.RandChoiceOnline(source, 20).Count());
75+
Assert.Empty(env.RandChoiceOnline(source, 0));
76+
Assert.Throws<ArgumentException>(() => env.RandChoiceOnline(source, -1).ToList());
77+
78+
Assert.Equal(source, env.RandChoiceNoRepetition(source, source.Length));
79+
var sample = env.RandChoiceNoRepetition(source, 4).ToList();
80+
Assert.Equal(source.Where(x => sample.Contains(x)), sample);
81+
Assert.Equal(5, env.RandChoiceNoRepetition(source, 5).Distinct().Count());
82+
Assert.Empty(env.RandChoiceNoRepetition(source, 0));
83+
Assert.Throws<ArgumentException>(() => env.RandChoiceNoRepetition(source, source.Length + 1).ToList());
84+
Assert.Throws<ArgumentException>(() => env.RandChoiceNoRepetition(source, -1).ToList());
85+
}
86+
}
5387
}
5488
}

0 commit comments

Comments
 (0)