diff --git a/Core/src/Umbrella.Utilities/Numerics/Abstractions/IConcurrentRandomGenerator.cs b/Core/src/Umbrella.Utilities/Numerics/Abstractions/IConcurrentRandomGenerator.cs index c6cac7c5a..c1412db4c 100644 --- a/Core/src/Umbrella.Utilities/Numerics/Abstractions/IConcurrentRandomGenerator.cs +++ b/Core/src/Umbrella.Utilities/Numerics/Abstractions/IConcurrentRandomGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbrella.Utilities.Exceptions; namespace Umbrella.Utilities.Numerics.Abstractions { @@ -22,7 +23,8 @@ public interface IConcurrentRandomGenerator /// is less than one or is greater than the difference between and such that it will be /// impossible to generate a collection of the size of the specified value. /// - IReadOnlyCollection GenerateDistinctList(int min, int max, int count, bool shuffle); + /// An error has occurred while generating the collection. + IReadOnlyCollection GenerateDistinctCollection(int min, int max, int count, bool shuffle = false); /// /// Gets the next random number based on the specified and values. @@ -34,8 +36,16 @@ public interface IConcurrentRandomGenerator /// that is, the range of return values includes but not . If /// equals , is returned. /// Thrown if either parameter value is less than zero, or if is less than . + /// An error has occurred while generating the random number. int Next(int min = 0, int max = 0); + /// + /// Returns a non-negative random integer. + /// + /// A 32-bit signed integer that is greater than or equal to 0 and less than . + /// An error has occurred while generating the random number. + int Next(); + /// /// Returns a specified number of random elements from a sequence. /// diff --git a/Core/src/Umbrella.Utilities/Numerics/ConcurrentRandomGenerator.cs b/Core/src/Umbrella.Utilities/Numerics/ConcurrentRandomGenerator.cs index a4a3d797b..fca3e5348 100644 --- a/Core/src/Umbrella.Utilities/Numerics/ConcurrentRandomGenerator.cs +++ b/Core/src/Umbrella.Utilities/Numerics/ConcurrentRandomGenerator.cs @@ -6,6 +6,7 @@ using System.Security.Cryptography; using System.Threading; using Microsoft.Extensions.Logging; +using Umbrella.Utilities.Exceptions; using Umbrella.Utilities.Numerics.Abstractions; namespace Umbrella.Utilities.Numerics @@ -31,6 +32,23 @@ public ConcurrentRandomGenerator(ILogger logger) _threadLocalRandom = new ThreadLocal(CreateRandom); } + /// + /// Returns a non-negative random integer. + /// + /// A 32-bit signed integer that is greater than or equal to 0 and less than . + /// An error has occurred while generating the random number. + public int Next() + { + try + { + return _threadLocalRandom.Value.Next(); + } + catch (Exception exc) when (_log.WriteError(exc)) + { + throw new UmbrellaException("An error has occurred while generating the random number.", exc); + } + } + /// /// Gets the next random number based on the specified and values. /// @@ -41,10 +59,11 @@ public ConcurrentRandomGenerator(ILogger logger) /// that is, the range of return values includes but not . If /// equals , is returned. /// Thrown if either parameter value is less than zero, or if is less than . + /// An error has occurred while generating the random number. public int Next(int min = 0, int max = 0) { - Guard.ArgumentInRange(min, nameof(min), 0); - Guard.ArgumentInRange(max, nameof(max), 0); + Guard.ArgumentInRange(min, nameof(min), 0, int.MaxValue); + Guard.ArgumentInRange(max, nameof(max), 0, int.MaxValue); if (min == max) return min; @@ -53,19 +72,11 @@ public int Next(int min = 0, int max = 0) try { - Random random = _threadLocalRandom.Value; - - if (min > 0 && max > 0) - return random.Next(min, max); - - if (max > 0) - return random.Next(max); - - return random.Next(); + return _threadLocalRandom.Value.Next(min, max); } catch (Exception exc) when (_log.WriteError(exc, new { min, max })) { - throw; + throw new UmbrellaException("An error has occurred while generating the random number.", exc); } } @@ -83,7 +94,8 @@ public int Next(int min = 0, int max = 0) /// is less than one or is greater than the difference between and such that it will be /// impossible to generate a collection of the size of the specified value. /// - public IReadOnlyCollection GenerateDistinctList(int min, int max, int count, bool shuffle) + /// An error has occurred while generating the collection. + public IReadOnlyCollection GenerateDistinctCollection(int min, int max, int count, bool shuffle = false) { Guard.ArgumentInRange(min, nameof(min), 0); Guard.ArgumentInRange(max, nameof(max), 0); @@ -125,7 +137,7 @@ public IReadOnlyCollection GenerateDistinctList(int min, int max, int count } catch (Exception exc) when (_log.WriteError(exc, new { min, max, count, shuffle })) { - throw; + throw new UmbrellaException("An error has occurred while generating the collection.", exc); } } @@ -142,14 +154,15 @@ public IReadOnlyCollection GenerateDistinctList(int min, int max, int count public IEnumerable TakeRandom(IEnumerable source, int count, bool shuffle = false) { Guard.ArgumentNotNull(source, nameof(source)); - Guard.ArgumentInRange(count, nameof(count), 1); int sourceCount = source.Count(); if (sourceCount == 0) yield break; - var indexes = GenerateDistinctList(0, sourceCount, count, shuffle); + Guard.ArgumentInRange(count, nameof(count), 1, sourceCount); + + var indexes = GenerateDistinctCollection(0, sourceCount, count, shuffle); foreach (int idx in indexes) { diff --git a/Core/test/Umbrella.Utilities.Test/Numerics/ConcurrentRandomGeneratorTest.cs b/Core/test/Umbrella.Utilities.Test/Numerics/ConcurrentRandomGeneratorTest.cs new file mode 100644 index 000000000..152e5a0de --- /dev/null +++ b/Core/test/Umbrella.Utilities.Test/Numerics/ConcurrentRandomGeneratorTest.cs @@ -0,0 +1,193 @@ +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Moq; +using Umbrella.Utilities.Numerics; +using Xunit; + +namespace Umbrella.Utilities.Test.Numerics +{ + public class ConcurrentRandomGeneratorTest + { + private readonly ConcurrentRandomGenerator _concurrentRandomGenerator; + + public ConcurrentRandomGeneratorTest() + { + _concurrentRandomGenerator = CreateConcurrentRandomGenerator(); + } + + [Fact] + public void Next_Valid() + { + int num = _concurrentRandomGenerator.Next(); + + Assert.True(num > -1); + } + + [Fact] + public void Next_InvalidMin() + => Assert.Throws(() => _concurrentRandomGenerator.Next(-1)); + + [Fact] + public void Next_InvalidMax() + => Assert.Throws(() => _concurrentRandomGenerator.Next(0, -1)); + + [Fact] + public void Next_InvalidMinMax() + => Assert.Throws(() => _concurrentRandomGenerator.Next(4, 3)); + + [Theory] + [InlineData(0, 0)] + [InlineData(10, 10)] + public void Next_EqMinMax_Valid(int min, int max) + { + int num = _concurrentRandomGenerator.Next(min, max); + + Assert.Equal(min, num); + } + + [Theory] + [InlineData(0, 1)] + [InlineData(0, 2)] + [InlineData(1, 2)] + [InlineData(1, 3)] + [InlineData(0, 99)] + [InlineData(100, 300)] + public void Next_NEqMinMax_Valid(int min, int max) + { + int num = _concurrentRandomGenerator.Next(min, max); + + Assert.True(num >= min && num < max); + } + + [Fact] + public void GenerateDistinctList_InvalidMin() + => Assert.Throws(() => _concurrentRandomGenerator.GenerateDistinctCollection(-1, 0, 1)); + + [Fact] + public void GenerateDistinctList_InvalidMax() + => Assert.Throws(() => _concurrentRandomGenerator.GenerateDistinctCollection(0, -1, 1)); + + [Fact] + public void GenerateDistinctList_InvalidMinMax() + => Assert.Throws(() => _concurrentRandomGenerator.GenerateDistinctCollection(4, 3, 1)); + + [Theory] + [InlineData(0, 1, 0)] + [InlineData(9, 10, 2)] + [InlineData(10, 12, 3)] + public void GenerateDistinctList_InvalidCount(int min, int max, int count) + => Assert.Throws(() => _concurrentRandomGenerator.GenerateDistinctCollection(min, max, count)); + + [Theory] + [InlineData(0, 0, 1)] + [InlineData(10, 10, 1)] + public void GenerateDistinctList_EqMinMaxCount_Valid(int min, int max, int count) + { + var items = _concurrentRandomGenerator.GenerateDistinctCollection(min, max, count); + + Assert.Equal(count, items.Count); + Assert.Equal(count, items.Distinct().Count()); + } + + [Theory] + [InlineData(0, 9, 1, false)] + [InlineData(0, 4, 2, false)] + [InlineData(1, 5, 1, false)] + [InlineData(1, 10, 2, false)] + [InlineData(0, 99, 10, false)] + [InlineData(100, 300, 27, false)] + [InlineData(0, 9, 1, true)] + [InlineData(0, 4, 2, true)] + [InlineData(1, 5, 1, true)] + [InlineData(1, 10, 2, true)] + [InlineData(0, 99, 10, true)] + [InlineData(100, 300, 27, true)] + public void GenerateDistinctList_NEqMinMaxCount_Valid(int min, int max, int count, bool shuffle) + { + var items = _concurrentRandomGenerator.GenerateDistinctCollection(min, max, count, shuffle); + + Assert.Equal(count, items.Count); + Assert.Equal(count, items.Distinct().Count()); + } + + [Theory] + [InlineData(0, 1, 1, false)] + [InlineData(0, 2, 2, false)] + [InlineData(1, 2, 1, false)] + [InlineData(1, 3, 2, false)] + [InlineData(0, 99, 99, false)] + [InlineData(100, 300, 200, false)] + [InlineData(0, 1, 1, true)] + [InlineData(0, 2, 2, true)] + [InlineData(1, 2, 1, true)] + [InlineData(1, 3, 2, true)] + [InlineData(0, 99, 99, true)] + [InlineData(100, 300, 200, true)] + public void GenerateDistinctList_NEqMinMaxCountFullRange_Valid(int min, int max, int count, bool shuffle) + { + var items = _concurrentRandomGenerator.GenerateDistinctCollection(min, max, count, shuffle); + + Assert.Equal(count, items.Count); + Assert.Equal(count, items.Distinct().Count()); + } + + [Fact] + public void TakeRandom_Source_Null() + => Assert.Throws(() => _concurrentRandomGenerator.TakeRandom(null, 1).ToArray()); + + [Fact] + public void TakeRandom_Source_Empty() + { + string[] items = new string[0]; + + var result = _concurrentRandomGenerator.TakeRandom(items, 1); + + Assert.Empty(result); + } + + [Theory] + [InlineData(0)] // Zero not allowed + [InlineData(3)] // Greater than the source size + public void TakeRandom_Count_Invalid(int count) + => Assert.Throws(() => _concurrentRandomGenerator.TakeRandom(new[] { 1, 2 }, count).ToArray()); + + [Theory] + [InlineData(1, false)] + [InlineData(2, false)] + [InlineData(3, false)] + [InlineData(4, false)] + [InlineData(5, false)] + [InlineData(6, false)] + [InlineData(7, false)] + [InlineData(8, false)] + [InlineData(9, false)] + [InlineData(10, false)] + [InlineData(1, true)] + [InlineData(2, true)] + [InlineData(3, true)] + [InlineData(4, true)] + [InlineData(5, true)] + [InlineData(6, true)] + [InlineData(7, true)] + [InlineData(8, true)] + [InlineData(9, true)] + [InlineData(10, true)] + public void TakeRandom_Valid(int count, bool shuffle) + { + int[] items = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + + int[] result = _concurrentRandomGenerator.TakeRandom(items, count, shuffle).ToArray(); + + Assert.Equal(count, result.Length); + Assert.Equal(count, result.Distinct().Count()); + } + + private ConcurrentRandomGenerator CreateConcurrentRandomGenerator() + { + var loggerMock = new Mock>(); + + return new ConcurrentRandomGenerator(loggerMock.Object); + } + } +} \ No newline at end of file