# HG changeset patch # User Michael Johnson # Date 1739825403 21600 # Mon Feb 17 14:50:03 2025 -0600 # Node ID 5edfa95d833fae81f281e6613a304e2d7a604ae9 # Parent 445db1d9ac2e6f3237f6dba8a2e788870ccf22c3 # Parent b04c6e73da7e36d989b537e2897b20e3bdf73ba3 Merge 'Add extra tests around ShuffleInPlace' diff --git a/src/Benchmarks/Benchmarks.csproj b/src/Benchmarks/Benchmarks.csproj --- a/src/Benchmarks/Benchmarks.csproj +++ b/src/Benchmarks/Benchmarks.csproj @@ -12,9 +12,13 @@ 8 - + + + + + diff --git a/src/Benchmarks/Extensions.cs b/src/Benchmarks/Extensions.cs --- a/src/Benchmarks/Extensions.cs +++ b/src/Benchmarks/Extensions.cs @@ -5,9 +5,6 @@ namespace RandN.Benchmarks; -//[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net48)] -//[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net60)] -//[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.Net80)] public class Extensions { public const Int32 Iterations = 16384; diff --git a/src/RandN/RngExtensions.cs b/src/RandN/RngExtensions.cs --- a/src/RandN/RngExtensions.cs +++ b/src/RandN/RngExtensions.cs @@ -9,7 +9,8 @@ /// /// Various extension methods to simplify use of RNGs. /// -public static class RngExtensions { +public static class RngExtensions +{ /// /// Shuffles a list using the in-place Fisher-Yates shuffling algorithm. /// @@ -18,25 +19,24 @@ public static void ShuffleInPlace(this TRng rng, IList list) where TRng : notnull, IRng { - if (list.GetType() == typeof(T[])) + if (list is T[] array) { - ShuffleInPlace(rng, Unsafe.As(list).AsSpan()); + ShuffleInPlace(rng, array.AsSpan()); return; } -#if NET5_0_OR_GREATER - else if (list.GetType() == typeof(List)) +#if NET6_0_OR_GREATER + if (list is List concrete) { - ShuffleInPlace(rng, CollectionsMarshal.AsSpan(Unsafe.As>(list))); + ShuffleInPlace(rng, CollectionsMarshal.AsSpan(concrete)); return; } #endif // Fisher-Yates shuffle - for (Int32 i = list.Count - 1; i >= 1; i--) { + for (Int32 i = list.Count - 1; i >= 1; i--) + { var dist = Uniform.NewInclusive(0, i); var swapIndex = dist.Sample(rng); - T temp = list[swapIndex]; - list[swapIndex] = list[i]; - list[i] = temp; + (list[swapIndex], list[i]) = (list[i], list[swapIndex]); } } @@ -48,9 +48,10 @@ public static void ShuffleInPlace(this TRng rng, Span span) where TRng : notnull, IRng { - ref var first = ref MemoryMarshal.GetReference(span); ; + ref var first = ref MemoryMarshal.GetReference(span); // Fisher-Yates shuffle - for (Int32 i = span.Length - 1; i >= 1; i--) { + for (Int32 i = span.Length - 1; i >= 1; i--) + { var dist = Uniform.NewInclusive(0, i); var swapIndex = dist.Sample(rng); ref var right = ref Unsafe.Add(ref first, i); @@ -67,7 +68,8 @@ /// A new instance. public static TRng Create(this IReproducibleRngFactory factory, TSeedingRng seedingRng) where TRng : notnull, IRng - where TSeedingRng : notnull, IRng { + where TSeedingRng : notnull, IRng + { var seed = factory.CreateSeed(seedingRng); return factory.Create(seed); } diff --git a/src/Tests/RngExtensionTests.cs b/src/Tests/RngExtensionTests.cs --- a/src/Tests/RngExtensionTests.cs +++ b/src/Tests/RngExtensionTests.cs @@ -1,5 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; +using RandN.Rngs; using Xunit; namespace RandN; @@ -7,17 +10,27 @@ public sealed class RngExtensionTests { [Fact] - public void Shuffle() + public void SimpleShuffle() { - var list = new List { 1, 2, 3, 4, 5, 6, 7 }; + Int32[] expectedOrder = [2, 3, 4, 5, 6, 7, 1]; + + Span span = [1, 2, 3, 4, 5, 6, 7]; + var array = span.ToArray(); + var list = new List(array); + var notList = new NotList(new List(list)); var rng = new StepRng(0) { Increment = 0 }; + + rng.ShuffleInPlace(span); + Assert.True(span.SequenceEqual(expectedOrder)); + + rng.ShuffleInPlace(array); + Assert.Equal(expectedOrder, array); + rng.ShuffleInPlace(list); - var expectedOrder = new[] { 2, 3, 4, 5, 6, 7, 1 }; Assert.Equal(expectedOrder, list); - Span span = stackalloc Int32[] { 1, 2, 3, 4, 5, 6, 7 }; - rng.ShuffleInPlace(span); - Assert.True(span.SequenceEqual(expectedOrder)); + rng.ShuffleInPlace(notList); + Assert.Equal(expectedOrder, notList); Assert.Throws(() => rng.ShuffleInPlace(default(IList)!)); @@ -26,4 +39,66 @@ rng.ShuffleInPlace(new List()); rng.ShuffleInPlace(Span.Empty); } + + [Theory] + [InlineData(0, 10ul)] + [InlineData(1, 20ul)] + [InlineData(2, 30ul)] + [InlineData(3, 40ul)] + [InlineData(7, 50ul)] + [InlineData(15, 60ul)] + [InlineData(16, 65ul)] + [InlineData(16384, 123ul)] + [InlineData(32768, 456ul)] + [InlineData(65536, 789ul)] + public void CompareShuffle(Int32 count, UInt64 seed) + { + var array = Enumerable.Range(0, count).ToArray(); + var notList = new NotList(new List(array)); + + var arrayRng = Pcg32.Create(seed, 123); + var notListRng = Pcg32.Create(seed, 123); + + arrayRng.ShuffleInPlace(array); + notListRng.ShuffleInPlace(notList); + + Assert.Equal(array, notList); + } + + /// + /// This is used to ensure we test the non-span path for lists. + /// + /// + private sealed class NotList(IList wrapped) : IList + { + public IEnumerator GetEnumerator() => wrapped.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)wrapped).GetEnumerator(); + + public void Add(Int32 item) => wrapped.Add(item); + + public void Clear() => wrapped.Clear(); + + public Boolean Contains(Int32 item) => wrapped.Contains(item); + + public void CopyTo(Int32[] array, Int32 arrayIndex) => wrapped.CopyTo(array, arrayIndex); + + public Boolean Remove(Int32 item) => wrapped.Remove(item); + + public Int32 Count => wrapped.Count; + + public Boolean IsReadOnly => wrapped.IsReadOnly; + + public Int32 IndexOf(Int32 item) => wrapped.IndexOf(item); + + public void Insert(Int32 index, Int32 item) => wrapped.Insert(index, item); + + public void RemoveAt(Int32 index) => wrapped.RemoveAt(index); + + public Int32 this[Int32 index] + { + get => wrapped[index]; + set => wrapped[index] = value; + } + } }