-
Notifications
You must be signed in to change notification settings - Fork 0
fix: add validation for alphabet random, fix performance issue and more #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,19 @@ public static class ILibRandom | |
| "yellow", "white" | ||
| }; | ||
|
|
||
| // Cache byte array for performance | ||
| [ThreadStatic] | ||
| private static byte[]? _byteBuffer; | ||
|
|
||
| private static byte[] GetByteBuffer() | ||
| { | ||
| if (_byteBuffer == null) | ||
| { | ||
| _byteBuffer = new byte[8]; | ||
| } | ||
| return _byteBuffer; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a random element from the specified array | ||
| /// </summary> | ||
|
|
@@ -56,9 +69,13 @@ public static int IRandomInt(int min, int max) | |
| if (min > max) | ||
| throw new ArgumentException("min must be <= max"); | ||
|
|
||
| #if NET6_0_OR_GREATER | ||
| return System.Random.Shared.Next(min, max + 1); | ||
| #else | ||
| double range = (double)max - (double)min; | ||
| int offset = (int)(_random.NextDouble() * (range + 1.0)); | ||
| return min + offset; | ||
| #endif | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -87,9 +104,36 @@ public static char IRandomAlphabet(char min, char max) | |
| { | ||
| if (min > max) | ||
| throw new ArgumentException("min must be <= max"); | ||
| if (!char.IsLetter(min) || !char.IsLetter(max)) | ||
| throw new ArgumentException("Only alphabet characters allowed"); | ||
|
|
||
| // Validate both characters are letters | ||
| if (!char.IsLetter(min) || !char.IsLetter(max)) | ||
| throw new ArgumentException("Both min and max must be alphabet characters"); | ||
|
|
||
| // Validate the range only contains letters | ||
| // Check if min and max are both uppercase or both lowercase | ||
| bool minIsUpper = char.IsUpper(min); | ||
| bool maxIsUpper = char.IsUpper(max); | ||
|
|
||
| if (minIsUpper != maxIsUpper) | ||
| throw new ArgumentException("min and max must be both uppercase or both lowercase"); | ||
|
|
||
| // Validate range doesn't include non-letter characters | ||
| // For uppercase: A-Z only (65-90) | ||
| // For lowercase: a-z only (97-122) | ||
| int minValue = (int)min; | ||
| int maxValue = (int)max; | ||
|
|
||
| if (minIsUpper) | ||
| { | ||
| if (minValue < 'A' || maxValue > 'Z') | ||
| throw new ArgumentException("Range must be within A-Z for uppercase letters"); | ||
| } | ||
| else | ||
| { | ||
| if (minValue < 'a' || maxValue > 'z') | ||
| throw new ArgumentException("Range must be within a-z for lowercase letters"); | ||
| } | ||
|
|
||
| char result; | ||
| do | ||
| { | ||
|
|
@@ -124,25 +168,44 @@ public static bool IRandomBool() | |
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a random long between min and max | ||
| /// Returns a random long between min and max (inclusive) | ||
| /// </summary> | ||
| public static long IRandomLong(long min, long max) | ||
| { | ||
| if (min > max) | ||
| throw new ArgumentException("min must be <= max"); | ||
|
|
||
| if (min == max) | ||
| return min; | ||
|
|
||
| #if NET6_0_OR_GREATER | ||
| // .NET 6+ has built-in method for long range | ||
| return System.Random.Shared.NextInt64(min, max + 1); | ||
| #else | ||
| // For older frameworks, use rejection sampling to avoid bias | ||
| ulong range = (ulong)(max - min); | ||
|
|
||
| // Handle full range (ulong.MaxValue) | ||
| if (range == ulong.MaxValue) | ||
| { | ||
| byte[] buf = new byte[8]; | ||
| _random.NextBytes(buf); | ||
| return BitConverter.ToInt64(buf, 0); | ||
| byte[] buffer = GetByteBuffer(); | ||
| _random.NextBytes(buffer); | ||
| return BitConverter.ToInt64(buffer, 0); | ||
| } | ||
|
|
||
| byte[] bytes = new byte[8]; | ||
| _random.NextBytes(bytes); | ||
| ulong uval = BitConverter.ToUInt64(bytes, 0); | ||
|
|
||
| // Rejection sampling to eliminate bias | ||
| ulong limit = ulong.MaxValue - ulong.MaxValue % (range + 1); | ||
| byte[] bytes = GetByteBuffer(); | ||
| ulong uval; | ||
|
|
||
| do | ||
| { | ||
| _random.NextBytes(bytes); | ||
| uval = BitConverter.ToUInt64(bytes, 0); | ||
| } while (uval > limit); | ||
|
|
||
| return min + (long)(uval % (range + 1)); | ||
| #endif | ||
|
Comment on lines
+181
to
+208
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method has two issues:
For .NET 6+, we can handle the #if NET6_0_OR_GREATER
if (max < long.MaxValue)
{
return System.Random.Shared.NextInt64(min, max + 1);
}
ulong range = (ulong)(max - min);
if (range == ulong.MaxValue)
{
Span<byte> buffer = stackalloc byte[8];
System.Random.Shared.NextBytes(buffer);
return BitConverter.ToInt64(buffer);
}
ulong limit = ulong.MaxValue - ulong.MaxValue % (range + 1);
Span<byte> bytes = stackalloc byte[8];
ulong uval;
do
{
System.Random.Shared.NextBytes(bytes);
uval = BitConverter.ToUInt64(bytes);
} while (uval >= limit);
return min + (long)(uval % (range + 1));
#else
ulong range = (ulong)(max - min);
if (range == ulong.MaxValue)
{
byte[] buffer = GetByteBuffer();
_random.NextBytes(buffer);
return BitConverter.ToInt64(buffer, 0);
}
ulong limit = ulong.MaxValue - ulong.MaxValue % (range + 1);
byte[] bytes = GetByteBuffer();
ulong uval;
do
{
_random.NextBytes(bytes);
uval = BitConverter.ToUInt64(bytes, 0);
} while (uval >= limit);
return min + (long)(uval % (range + 1));
#endif |
||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -156,6 +219,50 @@ public static double IRandomDouble(double min = 0.0, double max = 1.0) | |
| return min + (_random.NextDouble() * (max - min)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a random decimal between min and max | ||
| /// </summary> | ||
| public static decimal IRandomDecimal(decimal min, decimal max) | ||
| { | ||
| if (min > max) | ||
| throw new ArgumentException("min must be <= max"); | ||
|
|
||
| if (min == max) | ||
| return min; | ||
|
|
||
| // Get a random double and convert to decimal | ||
| // Using 28-29 digits of precision (maximum for decimal) | ||
| double randomDouble = _random.NextDouble(); | ||
| decimal randomDecimal = (decimal)randomDouble; | ||
|
|
||
| // Scale to the range | ||
| decimal range = max - min; | ||
| return min + (randomDecimal * range); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a random decimal between min and max with specified precision | ||
| /// </summary> | ||
| public static decimal IRandomDecimal(decimal min, decimal max, int precision) | ||
| { | ||
| if (min > max) | ||
| throw new ArgumentException("min must be <= max"); | ||
|
|
||
| if (precision < 0 || precision > 28) | ||
| throw new ArgumentException("precision must be between 0 and 28"); | ||
|
|
||
| if (min == max) | ||
| return min; | ||
|
|
||
| // Generate random integer with specified precision | ||
| long multiplier = (long)Math.Pow(10, precision); | ||
| long minScaled = (long)(min * multiplier); | ||
| long maxScaled = (long)(max * multiplier); | ||
|
|
||
| long randomScaled = IRandomLong(minScaled, maxScaled); | ||
| return randomScaled / (decimal)multiplier; | ||
|
Comment on lines
+257
to
+263
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using A much safer and simpler approach is to generate the random decimal using decimal randomValue = IRandomDecimal(min, max);
return Math.Round(randomValue, precision, MidpointRounding.AwayFromZero); |
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a random item from a list | ||
| /// </summary> | ||
|
|
@@ -190,5 +297,28 @@ public static string IRandomConsoleColor() | |
| { | ||
| return IRandomFromArray(ConsoleColors); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a random element from an enumeration | ||
| /// </summary> | ||
| public static T IRandomEnum<T>() where T : Enum | ||
| { | ||
| var values = Enum.GetValues(typeof(T)); | ||
| return (T)values.GetValue(_random.Next(values.Length))!; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a random element from an enumeration with exclusion | ||
| /// </summary> | ||
| public static T IRandomEnum<T>(T exclude) where T : Enum | ||
| { | ||
| var values = Enum.GetValues(typeof(T)); | ||
| T result; | ||
| do | ||
| { | ||
| result = (T)values.GetValue(_random.Next(values.Length))!; | ||
| } while (result.Equals(exclude)); | ||
| return result; | ||
| } | ||
|
Comment on lines
+301
to
+322
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation has two main issues:
private static class EnumCache<T> where T : Enum
{
public static readonly T[] Values = (T[])Enum.GetValues(typeof(T));
}
/// <summary>
/// Returns a random element from an enumeration
/// </summary>
public static T IRandomEnum<T>() where T : Enum
{
var values = EnumCache<T>.Values;
if (values.Length == 0)
throw new ArgumentException("Enum has no values");
return values[_random.Next(values.Length)];
}
/// <summary>
/// Returns a random element from an enumeration with exclusion
/// </summary>
public static T IRandomEnum<T>(T exclude) where T : Enum
{
var values = EnumCache<T>.Values;
if (values.Length == 0)
throw new ArgumentException("Enum has no values");
bool hasOther = false;
foreach (var val in values)
{
if (!val.Equals(exclude))
{
hasOther = true;
break;
}
}
if (!hasOther)
throw new ArgumentException("All values in the enum are excluded");
T result;
do
{
result = values[_random.Next(values.Length)];
} while (result.Equals(exclude));
return result;
} |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
maxisint.MaxValue,max + 1will overflow toint.MinValue. This causesSystem.Random.Shared.Next(min, max + 1)to throw anArgumentOutOfRangeExceptionbecause the upper bound is less than the lower bound.By casting to
long, we can safely calculate the range(long)max - min + 1without any overflow, and then useNextInt64to get a uniform random value.