More RandomUtil work
This commit is contained in:
+154
-7
@@ -9,6 +9,12 @@ public class RandomUtil
|
||||
{
|
||||
private readonly Random _random = new();
|
||||
|
||||
/// <summary>
|
||||
/// The IEEE-754 standard for double-precision floating-point numbers limits the number of digits (including both
|
||||
/// integer + fractional parts) to about 15–17 significant digits. 15 is a safe upper bound, so we'll use that.
|
||||
/// </summary>
|
||||
public const int MaxSignificantDigits = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random integer between the specified minimum and maximum values, inclusive.
|
||||
/// </summary>
|
||||
@@ -39,7 +45,7 @@ public class RandomUtil
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random floating-point number within the specified range.
|
||||
/// Generates a random floating-point number within the specified range ~6-9 digits (4 bytes).
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum value of the range (inclusive).</param>
|
||||
/// <param name="max">The maximum value of the range (exclusive).</param>
|
||||
@@ -48,6 +54,17 @@ public class RandomUtil
|
||||
{
|
||||
return (float)GetSecureRandomNumber() * (max - min) + min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random floating-point number within the specified range ~15-17 digits (8 bytes).
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum value of the range (inclusive).</param>
|
||||
/// <param name="max">The maximum value of the range (exclusive).</param>
|
||||
/// <returns>A random floating-point number between `min` (inclusive) and `max` (exclusive).</returns>
|
||||
public double GetDouble(double min, double max)
|
||||
{
|
||||
return GetSecureRandomNumber() * (max - min) + min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random boolean value.
|
||||
@@ -110,9 +127,9 @@ public class RandomUtil
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a random string from the provided collection of strings.
|
||||
/// Returns a random type T from the provided collection of type T.
|
||||
/// </summary>
|
||||
/// <param name="collection"></param>
|
||||
/// <param name="collection">The collection to get the random element from</param>
|
||||
/// <typeparam name="T">The type of elements in the collection.</typeparam>
|
||||
/// <returns>A random element from the collection.</returns>
|
||||
/// <remarks>This was formerly getArrayValue() in the node server</remarks>
|
||||
@@ -132,6 +149,134 @@ public class RandomUtil
|
||||
{
|
||||
return GetCollectionValue(dictionary.Keys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random val from the given dictionary
|
||||
/// </summary>
|
||||
/// <param name="dictionary">The dictionary from which to retrieve a value.</param>
|
||||
/// <typeparam name="TKey">Type of key</typeparam>
|
||||
/// <typeparam name="TVal">Type of Value</typeparam>
|
||||
/// <returns>A random TVal representing one of the values of the dictionary.</returns>
|
||||
public TVal GetVal<TKey, TVal>(Dictionary<TKey, TVal> dictionary) where TKey : notnull
|
||||
{
|
||||
return GetCollectionValue(dictionary.Values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a normally distributed random number using the Box-Muller transform.
|
||||
/// </summary>
|
||||
/// <param name="mean">The mean (μ) of the normal distribution.</param>
|
||||
/// <param name="sigma">The standard deviation (σ) of the normal distribution.</param>
|
||||
/// <param name="attempt">The current attempt count to generate a valid number (default is 0).</param>
|
||||
/// <returns>A normally distributed random number.</returns>
|
||||
/// <remarks>
|
||||
/// This function uses the Box-Muller transform to generate a normally distributed random number.
|
||||
/// If the generated number is less than 0, it will recursively attempt to generate a valid number up to 100 times.
|
||||
/// If it fails to generate a valid number after 100 attempts, it will return a random float between 0.01 and twice the mean.
|
||||
/// </remarks>
|
||||
public double GetNormallyDistributedRandomNumber(double mean, double sigma, int attempt = 0)
|
||||
{
|
||||
double u, v;
|
||||
|
||||
do
|
||||
{
|
||||
u = GetSecureRandomNumber();
|
||||
} while (u == 0);
|
||||
|
||||
do
|
||||
{
|
||||
v = GetSecureRandomNumber();
|
||||
} while (v == 0);
|
||||
|
||||
// Apply the Box-Muller transform
|
||||
var w = Math.Sqrt(-2.0 * Math.Log(u)) * Math.Cos(2.0 * Math.PI * v);
|
||||
var valueDrawn = mean + w * sigma;
|
||||
|
||||
// Check if the generated value is valid
|
||||
if (valueDrawn < 0)
|
||||
{
|
||||
return attempt > 100
|
||||
? GetDouble(0.01f, mean * 2f)
|
||||
: GetNormallyDistributedRandomNumber(mean, sigma, attempt + 1);
|
||||
}
|
||||
|
||||
return valueDrawn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random integer between the specified range.
|
||||
/// </summary>
|
||||
/// <param name="low">The lower bound of the range (inclusive).</param>
|
||||
/// <param name="high">The upper bound of the range (exclusive). If not provided, the range will be from 0 to `low`.</param>
|
||||
/// <returns>A random integer within the specified range.</returns>
|
||||
public int RandInt(int low, int? high)
|
||||
{
|
||||
// Return a random integer from 0 to low if high is not provided
|
||||
if (high is null)
|
||||
{
|
||||
return _random.Next(0, low);
|
||||
}
|
||||
|
||||
// Return low directly when low and high are equal
|
||||
return low == high
|
||||
? low
|
||||
: _random.Next(low, (int)high);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random number between two given values with optional precision.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value to determine the range.</param>
|
||||
/// <param name="val2">The second value to determine the range. If not provided, 0 is used.</param>
|
||||
/// <param name="precision">
|
||||
/// The number of decimal places to round the result to. Must be a positive integer between 0
|
||||
/// and MaxSignificantDigits(15), inclusive. If not provided, precision is determined by the input values.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public double RandNum(double val1, double val2 = 0, byte? precision = null)
|
||||
{
|
||||
if (!double.IsFinite(val1) || !double.IsFinite(val2))
|
||||
{
|
||||
throw new ArgumentException("RandNum() parameters 'value1' and 'value2' must be finite numbers.");
|
||||
}
|
||||
|
||||
// Determine the range
|
||||
var min = Math.Min(val1, val2);
|
||||
var max = Math.Max(val1, val2);
|
||||
|
||||
// Validate and adjust precision
|
||||
if (precision is not null)
|
||||
{
|
||||
if (precision > MaxSignificantDigits)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(precision), "Must be less than 16");
|
||||
}
|
||||
|
||||
// Calculate the number of whole-number digits in the maximum absolute value of the range
|
||||
var maxAbsoluteValue = Math.Max(Math.Abs(min), Math.Abs(max));
|
||||
var wholeNumberDigits = (int)Math.Floor(Math.Log10(maxAbsoluteValue)) + 1;
|
||||
|
||||
var maxAllowedPrecision = Math.Max(0, MaxSignificantDigits - wholeNumberDigits);
|
||||
|
||||
if (precision > maxAllowedPrecision)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"RandNum() precision of {precision} exceeds the allowable precision ({maxAllowedPrecision}) for the given values."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var result = GetSecureRandomNumber() * (max - min) + min;
|
||||
|
||||
// Determine effective precision
|
||||
var maxPrecision = Math.Max(GetNumberPrecision(val1), GetNumberPrecision(val2));
|
||||
var effectivePrecision = precision ?? maxPrecision;
|
||||
|
||||
var factor = Math.Pow(2, effectivePrecision);
|
||||
|
||||
return Math.Round(result * factor) / factor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a secure random number between 0 (inclusive) and 1 (exclusive).
|
||||
@@ -158,8 +303,6 @@ public class RandomUtil
|
||||
|
||||
const ulong maxInt = 1UL << 48;
|
||||
|
||||
Console.WriteLine(integer);
|
||||
|
||||
return (double)Math.Abs(integer) / maxInt;
|
||||
}
|
||||
|
||||
@@ -168,8 +311,12 @@ public class RandomUtil
|
||||
/// </summary>
|
||||
/// <param name="num">The number to analyze.</param>
|
||||
/// <returns>The number of decimal places, or 0 if none exist.</returns>
|
||||
private static int GetNumberPrecision(double num)
|
||||
public int GetNumberPrecision(double num)
|
||||
{
|
||||
return num.ToString().Split('.')[1]?.Length ?? 0;
|
||||
var parts = num.ToString($"G{MaxSignificantDigits}").Split('.');
|
||||
|
||||
return parts.Length > 1
|
||||
? parts[1].Length
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public sealed class RandomUtilTests
|
||||
|
||||
if (result < 0 || result > 10)
|
||||
{
|
||||
Assert.Fail($"GetInt() out of range. Expected range [0, 10] but was {result}.");
|
||||
Assert.Fail($"GetInt(0, 10) out of range. Expected range [0, 10] but was {result}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public sealed class RandomUtilTests
|
||||
|
||||
if (result < 1 || result > 9)
|
||||
{
|
||||
Assert.Fail($"GetInt() out of range. Expected range [1, 9] but was {result}.");
|
||||
Assert.Fail($"GetInt(10) out of range. Expected range [1, 9] but was {result}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public sealed class RandomUtilTests
|
||||
|
||||
if (result < 0f || result >= 9f)
|
||||
{
|
||||
Assert.Fail($"GetFloat() out of range. Expected range [0.0f, 8.99f] but was {result}.");
|
||||
Assert.Fail($"GetFloat(0f, 10f) out of range. Expected range [0.0f, 9.999f] but was {result}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public sealed class RandomUtilTests
|
||||
expected,
|
||||
result,
|
||||
0.0001f,
|
||||
$"GetPercentOfValue() out of range. Expected: {expected}. Actual: {result}.");
|
||||
$"GetPercentOfValue(45.5f, 100f) out of range. Expected: {expected}. Actual: {result}.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -75,7 +75,7 @@ public sealed class RandomUtilTests
|
||||
expected,
|
||||
result,
|
||||
0.0001f,
|
||||
$"ReduceValueByPercent() out of range. Expected: {expected}. Actual: {result}.");
|
||||
$"ReduceValueByPercent(100f, 45.5f) out of range. Expected: {expected}. Actual: {result}.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -89,7 +89,7 @@ public sealed class RandomUtilTests
|
||||
Assert.AreEqual(
|
||||
expectedTrue,
|
||||
resultTrue,
|
||||
$"GetChance100() out of range. Expected: {expectedTrue}. Actual: {resultTrue}.");
|
||||
$"GetChance100(100f) out of range. Expected: {expectedTrue}. Actual: {resultTrue}.");
|
||||
}
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
@@ -100,7 +100,67 @@ public sealed class RandomUtilTests
|
||||
Assert.AreEqual(
|
||||
expectedFalse,
|
||||
resultFalse,
|
||||
$"GetChance100() out of range. Expected: {expectedFalse}. Actual: {resultFalse}.");
|
||||
$"GetChance100(0f) out of range. Expected: {expectedFalse}. Actual: {resultFalse}.");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Missing methods between these two
|
||||
|
||||
[TestMethod]
|
||||
public void RandIntTest()
|
||||
{
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var result = _randomUtil.RandInt(0, 10);
|
||||
|
||||
if (result < 0 || result > 9)
|
||||
{
|
||||
Assert.Fail($"RandInt(0, 10) out of range. Expected range [0, 9] but was {result}.");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var result = _randomUtil.RandInt(10, null);
|
||||
|
||||
if (result < 0 || result > 9)
|
||||
{
|
||||
Assert.Fail($"RandInt(10, null) out of range. Expected range [0, 9] but was {result}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RandNumTest()
|
||||
{
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var result = _randomUtil.RandNum(0, 10);
|
||||
|
||||
if (result < 0 || result > 9)
|
||||
{
|
||||
Assert.Fail($"RandNum(0, 10) out of range. Expected range [0, 9.999d] but was {result}.");
|
||||
}
|
||||
|
||||
if (_randomUtil.GetNumberPrecision(result) > RandomUtil.MaxSignificantDigits)
|
||||
{
|
||||
Assert.Fail($"RandNum(0, 10) precision of {result} exceeds the allowable precision ({RandomUtil.MaxSignificantDigits}) for the given values.");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
var result = _randomUtil.RandNum(10);
|
||||
|
||||
if (result < 0 || result > 9)
|
||||
{
|
||||
Assert.Fail($"RandNum(10) out of range. Expected range [0, 9.999d] but was {result}.");
|
||||
}
|
||||
|
||||
if (_randomUtil.GetNumberPrecision(result) > RandomUtil.MaxSignificantDigits)
|
||||
{
|
||||
Assert.Fail($"RandNum(10) precision of {result} exceeds the allowable precision ({RandomUtil.MaxSignificantDigits}) for the given values.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user